Skip to content

Commit febb5fe

Browse files
authored
Merge pull request #67 from mapitman/66-add-support-for-theming
Add bundled themes and write theme files on startup
2 parents 33301b7 + e2aa4fc commit febb5fe

33 files changed

+695
-98
lines changed

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,6 @@ FodyWeavers.xsd
411411
## Visual studio for Mac
412412
##
413413

414-
415414
# globs
416415
Makefile.in
417416
*.userprefs
@@ -438,7 +437,6 @@ test-results/
438437
# Icon must end with two \r
439438
Icon
440439

441-
442440
# Thumbnails
443441
._*
444442

.vscode/settings.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
{
22
"omnisharp.enableEditorConfigSupport": true,
33
"omnisharp.enableRoslynAnalyzers": true,
4-
"dotnet.defaultSolution": "media-encoding.sln"
4+
"dotnet.defaultSolution": "RipSharp.sln",
5+
"chat.tools.terminal.autoApprove": {
6+
"gh": true
7+
}
58
}

EXAMPLES.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,6 @@ dotnet run --project src/RipSharp -- --mode tv --disc disc:1 --title "Friends" -
107107

108108
## Advanced Examples
109109

110-
111-
112110
## Specific Scenarios
113111

114112
### 4K UltraHD Blu-Ray

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,51 @@ macOS:
206206

207207
If no config file exists, RipSharp creates one in the first personal location above (for example, `$XDG_CONFIG_HOME/ripsharp/config.yaml` on Linux).
208208

209+
## Theming
210+
211+
Themes are loaded from a YAML file located under a `themes` subdirectory in the config directory and bound to options. On startup, RipSharp writes bundled themes into that directory if they are missing and does not overwrite existing files. Set the theme name in your config file:
212+
213+
```yaml
214+
theme: "catppuccin mocha"
215+
```
216+
217+
Built-in themes:
218+
219+
- "catppuccin latte"
220+
- "catppuccin frappe"
221+
- "catppuccin macchiato"
222+
- "catppuccin mocha"
223+
- "dracula"
224+
- "nord"
225+
- "tokyo-night"
226+
- "gruvbox dark"
227+
- "gruvbox light"
228+
229+
Theme file format (YAML):
230+
231+
```yaml
232+
theme:
233+
colors:
234+
success: "#94e2d5"
235+
error: "#f38ba8"
236+
warning: "#f9e2af"
237+
info: "#89b4fa"
238+
accent: "#89dceb"
239+
muted: "#6c7086"
240+
highlight: "#cba6f7"
241+
emojis:
242+
success: ""
243+
error: ""
244+
warning: "⚠️"
245+
insert_disc: "💿"
246+
disc_detected: "📀"
247+
scan: "🔍"
248+
disc_type: "💽"
249+
title_found: "🎞️"
250+
tv: "📺"
251+
movie: "🎬"
252+
```
253+
209254
## Building
210255
211256
```bash
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Collections.Generic;
2+
using System.IO;
3+
4+
using AwesomeAssertions;
5+
6+
using Xunit;
7+
8+
namespace BugZapperLabs.RipSharp.Tests.Core;
9+
10+
public class ThemeFileLocatorTests
11+
{
12+
[Fact]
13+
public void ResolveThemePath_WhenRelative_ReturnsThemePathUnderConfigThemes()
14+
{
15+
var configPath = Path.Combine("/home/tester", ".config", "ripsharp", "config.yaml");
16+
17+
var result = ThemeFileLocator.ResolveThemePath("custom.yaml", configPath);
18+
19+
result.Should().Be(Path.Combine("/home/tester", ".config", "ripsharp", "themes", "custom.yaml"));
20+
}
21+
22+
[Theory]
23+
[InlineData("catppuccin mocha", "catppuccin-mocha.yaml")]
24+
[InlineData("catppuccin-mocha", "catppuccin-mocha.yaml")]
25+
[InlineData("catppuccin_mocha", "catppuccin-mocha.yaml")]
26+
public void ResolveThemePath_WhenNameProvided_NormalizesToFileName(string themeName, string expectedFileName)
27+
{
28+
var configPath = Path.Combine("/home/tester", ".config", "ripsharp", "config.yaml");
29+
30+
var result = ThemeFileLocator.ResolveThemePath(themeName, configPath);
31+
32+
result.Should().Be(Path.Combine("/home/tester", ".config", "ripsharp", "themes", expectedFileName));
33+
}
34+
35+
[Fact]
36+
public void ResolveThemePath_WhenMissing_UsesDefaultThemeFile()
37+
{
38+
var configPath = Path.Combine("/home/tester", ".config", "ripsharp", "config.yaml");
39+
40+
var result = ThemeFileLocator.ResolveThemePath(null, configPath);
41+
42+
result.Should().Be(Path.Combine("/home/tester", ".config", "ripsharp", "themes", "catppuccin-mocha.yaml"));
43+
}
44+
45+
[Fact]
46+
public void EnsureBundledThemeFiles_WritesThemesIntoConfigThemesDirectory()
47+
{
48+
var configPath = Path.Combine("/home/tester", ".config", "ripsharp", "config.yaml");
49+
var files = new Dictionary<string, string>();
50+
var directories = new HashSet<string>();
51+
var bundledThemes = new Dictionary<string, string>
52+
{
53+
["catppuccin-mocha.yaml"] = "theme:\n colors:\n",
54+
["catppuccin-latte.yaml"] = "theme:\n colors:\n"
55+
};
56+
57+
ThemeFileLocator.EnsureBundledThemeFiles(
58+
configPath,
59+
path => files.ContainsKey(path),
60+
(path, content) => files[path] = content,
61+
path => directories.Add(path),
62+
bundledThemes);
63+
64+
var expectedThemeDir = Path.Combine("/home/tester", ".config", "ripsharp", "themes");
65+
var expectedMochaPath = Path.Combine(expectedThemeDir, "catppuccin-mocha.yaml");
66+
var expectedLattePath = Path.Combine(expectedThemeDir, "catppuccin-latte.yaml");
67+
68+
directories.Should().Contain(expectedThemeDir);
69+
files.Should().ContainKey(expectedMochaPath);
70+
files.Should().ContainKey(expectedLattePath);
71+
files[expectedMochaPath].Should().NotBeNullOrWhiteSpace();
72+
files[expectedLattePath].Should().NotBeNullOrWhiteSpace();
73+
}
74+
}

src/RipSharp.Tests/Metadata/MetadataServiceTests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public async Task LookupAsync_Fallbacks_WhenNoApiKeys()
2424
Environment.SetEnvironmentVariable("TMDB_API_KEY", null);
2525
var notifier = Substitute.For<IConsoleWriter>();
2626
var providers = new List<IMetadataProvider>();
27-
var svc = new MetadataService(providers, notifier);
27+
var svc = new MetadataService(providers, notifier, ThemeProvider.CreateDefault());
2828

2929
var md = await svc.LookupAsync("SIMPSONS_WS", isTv: false, year: null);
3030

@@ -46,7 +46,7 @@ public async Task LookupAsync_ReturnsFromFirstProvider_WhenMatch()
4646
provider2.Name.Returns("Provider2");
4747

4848
var providers = new List<IMetadataProvider> { provider1, provider2 };
49-
var svc = new MetadataService(providers, notifier);
49+
var svc = new MetadataService(providers, notifier, ThemeProvider.CreateDefault());
5050

5151
var result = await svc.LookupAsync("test", isTv: false, year: null);
5252

@@ -70,7 +70,7 @@ public async Task LookupAsync_TriesSecondProvider_WhenFirstReturnsNull()
7070
provider2.LookupAsync("test", false, null).Returns(new ContentMetadata { Title = "Test Movie", Year = 2021, Type = "movie" });
7171

7272
var providers = new List<IMetadataProvider> { provider1, provider2 };
73-
var svc = new MetadataService(providers, notifier);
73+
var svc = new MetadataService(providers, notifier, ThemeProvider.CreateDefault());
7474

7575
var result = await svc.LookupAsync("test", isTv: false, year: null);
7676

@@ -91,7 +91,7 @@ public async Task LookupAsync_UsesTitleVariations_WhenOriginalFails()
9191
provider.LookupAsync("MOVIE_TITLE", Arg.Any<bool>(), Arg.Any<int?>()).Returns(new ContentMetadata { Title = "Movie Title", Year = 2023, Type = "movie" });
9292

9393
var providers = new List<IMetadataProvider> { provider };
94-
var svc = new MetadataService(providers, notifier);
94+
var svc = new MetadataService(providers, notifier, ThemeProvider.CreateDefault());
9595

9696
var result = await svc.LookupAsync("MOVIE_TITLE_2023", isTv: false, year: null);
9797

@@ -112,7 +112,7 @@ public async Task LookupAsync_ShowsDifferentMessage_ForSimplifiedTitle()
112112
provider.LookupAsync("SIMPSONS", Arg.Any<bool>(), Arg.Any<int?>()).Returns(new ContentMetadata { Title = "The Simpsons", Year = 1989, Type = "tv" });
113113

114114
var providers = new List<IMetadataProvider> { provider };
115-
var svc = new MetadataService(providers, notifier);
115+
var svc = new MetadataService(providers, notifier, ThemeProvider.CreateDefault());
116116

117117
await svc.LookupAsync("SIMPSONS_WS", isTv: true, year: null);
118118

@@ -131,7 +131,7 @@ public async Task LookupAsync_ShowsNormalMessage_ForOriginalTitle()
131131
provider.LookupAsync("Test Movie", Arg.Any<bool>(), Arg.Any<int?>()).Returns(new ContentMetadata { Title = "Test Movie", Year = 2020, Type = "movie" });
132132

133133
var providers = new List<IMetadataProvider> { provider };
134-
var svc = new MetadataService(providers, notifier);
134+
var svc = new MetadataService(providers, notifier, ThemeProvider.CreateDefault());
135135

136136
await svc.LookupAsync("Test Movie", isTv: false, year: null);
137137

src/RipSharp.Tests/Services/DiscRipperTitleSuffixTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,9 @@ private static DiscRipper CreateRipper(IEncoderService encoder)
9292
var userPrompt = Substitute.For<IUserPrompt>();
9393
var episodeTitles = Substitute.For<ITvEpisodeTitleProvider>();
9494
var progressDisplay = Substitute.For<IProgressDisplay>();
95+
var theme = ThemeProvider.CreateDefault();
9596

96-
return new DiscRipper(scanner, encoder, metadataService, makeMkv, notifier, userPrompt, episodeTitles, progressDisplay);
97+
return new DiscRipper(scanner, encoder, metadataService, makeMkv, notifier, userPrompt, episodeTitles, progressDisplay, theme);
9798
}
9899

99100
private static async Task<List<object>> InvokeBuildTitlePlansAsync(
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using Microsoft.Extensions.Options;
2+
3+
using Spectre.Console;
4+
5+
using Xunit;
6+
7+
namespace BugZapperLabs.RipSharp.Tests.Utilities;
8+
9+
public class ThemeProviderTests
10+
{
11+
[Fact]
12+
public void Colors_AreLoadedFromOptions()
13+
{
14+
var options = Options.Create(new ThemeOptions
15+
{
16+
Colors = new ThemeColors
17+
{
18+
Success = "#010203"
19+
}
20+
});
21+
22+
var provider = new ThemeProvider(options);
23+
24+
provider.SuccessColor.Should().Be(new Color(1, 2, 3));
25+
}
26+
27+
[Fact]
28+
public void Emojis_AreLoadedFromOptions()
29+
{
30+
var options = Options.Create(new ThemeOptions
31+
{
32+
Emojis = new ThemeEmojis
33+
{
34+
Warning = "!"
35+
}
36+
});
37+
38+
var provider = new ThemeProvider(options);
39+
40+
provider.Emojis.Warning.Should().Be("!");
41+
}
42+
}

src/RipSharp/Core/ConfigFileLocator.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@ internal static class ConfigFileLocator
2424
" default_path: \"disc:0\"\n" +
2525
" default_temp_dir: \"/tmp/makemkv\"\n\n" +
2626
"output:\n" +
27-
" movies_dir: \"~/Movies\"\n" +
28-
" tv_dir: \"~/TV\"\n\n" +
27+
" movies_dir: \"~/Videos/Movies\"\n" +
28+
" tv_dir: \"~/Videos/TV\"\n\n" +
2929
"encoding:\n" +
3030
" include_english_subtitles: true\n" +
3131
" include_stereo_audio: true\n" +
3232
" include_surround_audio: true\n\n" +
3333
"metadata:\n" +
34-
" lookup_enabled: true\n";
34+
" lookup_enabled: true\n\n" +
35+
"theme: \"catppuccin mocha\"\n";
3536

3637
internal static ConfigSearchContext CreateContext()
3738
{

src/RipSharp/Core/Program.cs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using Microsoft.Extensions.DependencyInjection;
55
using Microsoft.Extensions.Hosting;
66

7-
87
namespace BugZapperLabs.RipSharp.Core;
98

109
public class Program
@@ -36,16 +35,18 @@ public static async Task<int> Main(string[] args)
3635
private static async Task<int> RunAsync(string[] args, CursorManager cursorManager)
3736
{
3837
var options = RipOptions.ParseArgs(args);
38+
var defaultTheme = ThemeProvider.CreateDefault();
39+
var consoleWriter = new ConsoleWriter(defaultTheme);
3940

4041
if (options.ShowHelp)
4142
{
42-
RipOptions.DisplayHelp(new ConsoleWriter());
43+
RipOptions.DisplayHelp(consoleWriter);
4344
return 0;
4445
}
4546

4647
if (options.ShowVersion)
4748
{
48-
Console.WriteLine($"ripsharp {GetVersion()}");
49+
consoleWriter.Plain($"ripsharp {GetVersion()}");
4950
return 0;
5051
}
5152

@@ -56,7 +57,7 @@ private static async Task<int> RunAsync(string[] args, CursorManager cursorManag
5657

5758
if (missingTools.Count > 0)
5859
{
59-
var prereqWriter = new ConsoleWriter();
60+
var prereqWriter = consoleWriter;
6061
prereqWriter.Error("Missing required prerequisites:");
6162
foreach (var tool in missingTools)
6263
{
@@ -105,11 +106,32 @@ private static async Task<int> RunAsync(string[] args, CursorManager cursorManag
105106
cfg.AddYamlFile(configPath, optional: false, reloadOnChange: true);
106107
}
107108

109+
ThemeFileLocator.EnsureBundledThemeFiles(
110+
configPath,
111+
File.Exists,
112+
File.WriteAllText,
113+
path => Directory.CreateDirectory(path));
114+
115+
var tempConfig = cfg.Build();
116+
var themeSetting = tempConfig.GetValue<string>("theme");
117+
if (string.IsNullOrWhiteSpace(themeSetting))
118+
{
119+
themeSetting = tempConfig.GetSection("theme").GetValue<string>("path");
120+
}
121+
var resolvedThemePath = ThemeFileLocator.ResolveThemePath(themeSetting, configPath);
122+
123+
if (!string.IsNullOrWhiteSpace(resolvedThemePath))
124+
{
125+
cfg.AddYamlFile(resolvedThemePath, optional: true, reloadOnChange: true);
126+
}
127+
108128
cfg.AddEnvironmentVariables();
109129
})
110130
.ConfigureServices((ctx, services) =>
111131
{
112132
services.Configure<AppConfig>(ctx.Configuration);
133+
services.Configure<ThemeOptions>(ctx.Configuration.GetSection("theme"));
134+
services.AddSingleton<IThemeProvider, ThemeProvider>();
113135
services.AddSingleton<IConsoleWriter, ConsoleWriter>();
114136
services.AddSingleton<IProgressDisplay, SpectreProgressDisplay>();
115137
services.AddSingleton<IUserPrompt, ConsoleUserPrompt>();
@@ -155,6 +177,7 @@ private static async Task<int> RunAsync(string[] args, CursorManager cursorManag
155177

156178
var ripper = host.Services.GetRequiredService<IDiscRipper>();
157179
var writer = host.Services.GetRequiredService<IConsoleWriter>();
180+
var theme = host.Services.GetRequiredService<IThemeProvider>();
158181

159182
List<string> files;
160183
try
@@ -163,7 +186,7 @@ private static async Task<int> RunAsync(string[] args, CursorManager cursorManag
163186
}
164187
catch (OperationCanceledException)
165188
{
166-
writer.Warning("\n⚠️ Operation interrupted by user");
189+
writer.Warning($"\n{theme.Emojis.Warning} Operation interrupted by user");
167190
return 130;
168191
}
169192

0 commit comments

Comments
 (0)