Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ pack: build

# Install ripsharp as a global dotnet tool from local package output
install: pack
@dotnet tool update --global BugZapperLabs.RipSharp --add-source src/RipSharp/bin/Release \
|| dotnet tool install --global BugZapperLabs.RipSharp --add-source src/RipSharp/bin/Release
@dotnet tool uninstall --global BugZapperLabs.RipSharp >/dev/null 2>&1 || true
@dotnet tool install --global BugZapperLabs.RipSharp --add-source src/RipSharp/bin/Release

# Clean build outputs
clean:
Expand Down
67 changes: 67 additions & 0 deletions src/RipSharp.Tests/Core/RipOptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,73 @@ public void ParseArgs_WithHelpAmongOtherArgs_SetsShowHelpTrue()
result.ShowHelp.Should().BeTrue();
}

[Fact]
public void DisplayHelp_AlignsOptionDescriptions()
{
var writer = Substitute.For<IConsoleWriter>();
var lines = new List<string>();

void Capture(Action<IConsoleWriter> setup)
{
setup(writer);
}

Capture(w => w.When(c => c.Info(Arg.Any<string>()))
.Do(call => lines.Add(call.Arg<string>())));
Capture(w => w.When(c => c.Success(Arg.Any<string>()))
.Do(call => lines.Add(call.Arg<string>())));
Capture(w => w.When(c => c.Warning(Arg.Any<string>()))
.Do(call => lines.Add(call.Arg<string>())));
Capture(w => w.When(c => c.Error(Arg.Any<string>()))
.Do(call => lines.Add(call.Arg<string>())));
Capture(w => w.When(c => c.Muted(Arg.Any<string>()))
.Do(call => lines.Add(call.Arg<string>())));
Capture(w => w.When(c => c.Accent(Arg.Any<string>()))
.Do(call => lines.Add(call.Arg<string>())));
Capture(w => w.When(c => c.Highlight(Arg.Any<string>()))
.Do(call => lines.Add(call.Arg<string>())));
Capture(w => w.When(c => c.Plain(Arg.Any<string>()))
.Do(call => lines.Add(call.Arg<string>())));

RipOptions.DisplayHelp(writer);

lines.Should().Contain(" ripsharp (OPTIONS)");

var optionLines = lines
.Where(line => line.StartsWith(" --") || line.StartsWith(" -h") || line.StartsWith(" -v"))
.ToList();

optionLines.Should().NotBeEmpty();

var descriptionStart = optionLines
.Select(GetDescriptionStartIndex)
.Distinct()
.Single();

var detailLines = lines
.Where(line => line.Contains("- auto:") || line.Contains("- movie:") || line.Contains("- series:"))
.ToList();

detailLines.Should().NotBeEmpty();
detailLines.All(line => line.IndexOf('-') == descriptionStart).Should().BeTrue();

static int GetDescriptionStartIndex(string line)
{
var index = line.LastIndexOf(" ", StringComparison.Ordinal);
if (index < 0)
{
return line.Length;
}

while (index < line.Length && line[index] == ' ')
{
index++;
}

return index;
}
}

[Fact]
public void ParseArgs_WithVersionShortFlag_SetsShowVersionTrue()
{
Expand Down
85 changes: 51 additions & 34 deletions src/RipSharp/Core/RipOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,48 +94,65 @@ private static string GetTempDirectoryName()

public static void DisplayHelp(IConsoleWriter writer)
{
writer.Plain("ripsharp - DVD/Blu-Ray/UHD disc ripping tool");
const int optionWidth = 26;
var detailIndent = new string(' ', 4 + optionWidth + 1);

void OptionLine(string option, string description)
{
writer.Highlight($" {option.PadRight(optionWidth)} {description}");
}

void OptionDetail(string description)
{
writer.Muted($"{detailIndent}{description}");
}

writer.Accent("ripsharp - DVD/Blu-Ray/UHD disc ripping tool");
writer.Plain("");
writer.Plain("USAGE:");
writer.Plain(" dotnet run --project src/RipSharp -- [OPTIONS]");
writer.Accent("USAGE:");
writer.Plain(" ripsharp (OPTIONS)");
writer.Plain("");
writer.Plain("REQUIRED OPTIONS:");
writer.Plain(" --output PATH Output directory for ripped files");
writer.Accent("REQUIRED OPTIONS:");
OptionLine("--output PATH", "Output directory for ripped files");
writer.Plain("");
writer.Plain("OPTIONS:");
writer.Plain(" --mode auto|movie|tv Content type detection (default: auto)");
writer.Plain(" - auto: Automatically detect movie vs TV series");
writer.Plain(" - movie: Treat as single movie");
writer.Plain(" - tv: Treat as TV series");
writer.Plain(" --disc PATH Optical drive path (default: disc:0)");
writer.Plain(" --temp PATH Temporary ripping directory (default: {output}/<RANDOM_STRING>)");
writer.Plain(" --title TEXT Custom title for file naming");
writer.Plain(" --year YYYY Release year (movies only)");
writer.Plain(" --season N Season number (TV only, default: 1)");
writer.Plain(" --episode-start N Starting episode number (TV only, default: 1)");
writer.Plain(" --disc-type TYPE Override disc type: dvd|bd|uhd (auto-detect by default)");
writer.Plain(" --sequential Disable parallel processing (rip all, then encode all)");
writer.Plain(" --debug Enable debug logging");
writer.Plain(" -h, --help Show this help message");
writer.Plain(" -v, --version Show the application version");
writer.Accent("OPTIONS:");
OptionLine("--mode auto|movie|series", "Content type detection (default: auto)");
OptionDetail("- auto: Automatically detect movie vs series");
OptionDetail("- movie: Treat as single movie");
OptionDetail("- series: Treat as series");
OptionLine("--disc PATH", "Optical drive path (default: disc:0)");
OptionLine("--temp PATH", "Temporary ripping directory");
OptionDetail("Default: {output}/<RANDOM_STRING>");
OptionLine("--title TEXT", "Custom title for file naming");
OptionLine("--year YYYY", "Release year (movies only)");
OptionLine("--season N", "Season number (TV only, default: 1)");
OptionLine("--episode-start N", "Starting episode number (TV only, default: 1)");
OptionLine("--disc-type TYPE", "Override disc type: dvd|bd|uhd");
OptionDetail("Default: auto-detect");
OptionLine("--sequential", "Disable parallel processing");
OptionDetail("Rip all, then encode all");
OptionLine("--debug", "Enable debug logging");
OptionLine("-h, --help", "Show this help message");
OptionLine("-v, --version", "Show the application version");
writer.Plain("");
writer.Plain("EXAMPLES:");
writer.Plain(" # Rip with auto-detection (recommended)");
writer.Plain(" dotnet run --project src/RipSharp -- --output ~/Movies --title \"The Matrix\" --year 1999");
writer.Accent("EXAMPLES:");
writer.Muted(" # Rip with auto-detection (recommended)");
writer.Plain(" ripsharp --output ~/Movies --title \"The Matrix\" --year 1999");
writer.Plain("");
writer.Plain(" # Rip a movie (explicit)");
writer.Plain(" dotnet run --project src/RipSharp -- --output ~/Movies --mode movie --title \"The Matrix\" --year 1999");
writer.Muted(" # Rip a movie (explicit)");
writer.Plain(" ripsharp --output ~/Movies --mode movie --title \"The Matrix\" --year 1999");
writer.Plain("");
writer.Plain(" # Rip a TV season (explicit)");
writer.Plain(" dotnet run --project src/RipSharp -- --output ~/TV --mode tv --title \"Breaking Bad\" --season 1");
writer.Muted(" # Rip a series season (explicit)");
writer.Plain(" ripsharp --output ~/Series --mode series --title \"Breaking Bad\" --season 1");
writer.Plain("");
writer.Plain(" # Use second disc drive");
writer.Plain(" dotnet run --project src/RipSharp -- --output ~/Movies --disc disc:1");
writer.Muted(" # Use second disc drive");
writer.Plain(" ripsharp --output ~/Movies --disc disc:1");
writer.Plain("");
writer.Plain("ENVIRONMENT VARIABLES:");
writer.Plain(" TMDB_API_KEY TMDB API key for metadata lookup (recommended)");
writer.Plain(" OMDB_API_KEY OMDB API key for metadata lookup (optional)");
writer.Accent("ENVIRONMENT VARIABLES:");
writer.Highlight(" TMDB_API_KEY TMDB API key for metadata lookup (recommended)");
writer.Highlight(" TVDB_API_KEY TVDB API key for metadata lookup (optional)");
writer.Highlight(" OMDB_API_KEY OMDB API key for metadata lookup (optional)");
writer.Plain("");
writer.Plain("For more information, visit: https://github.com/mapitman/ripsharp");
writer.Muted("For more information, visit: https://github.com/mapitman/ripsharp");
}
}