Skip to content

Commit 3cf080c

Browse files
Merge pull request #50 from enzalito/code-themes
add support for custom syntect themes
2 parents 418b55e + 7327b50 commit 3cf080c

4 files changed

Lines changed: 100 additions & 46 deletions

File tree

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,30 @@ search_current_fg = "Black"
476476

477477
</details>
478478

479+
### Custom Code Blocks Theme
480+
481+
Customize syntax highlighting in code blocks using Sublime Text `.tmTheme` files.
482+
483+
```toml
484+
[ui]
485+
code_theme = "base16-ocean.dark" # Default theme
486+
```
487+
488+
**Built-in themes:** `base16-ocean.dark`, `base16-ocean.light`, `base16-eighties.dark`, `base16-mocha.dark`, `InspiredGitHub`, `Solarized (dark)`, `Solarized (light)`
489+
490+
**Using custom themes:**
491+
492+
1. Create the `code-themes` directory next to your `config.toml file`
493+
494+
2. Add your `.tmTheme` files to the directory
495+
496+
3. Reference the theme by name (filename without extension):
497+
498+
```toml
499+
[ui]
500+
code_theme = "MyCustomTheme"
501+
```
502+
479503
### CLI Overrides
480504

481505
Override settings for a single session:

src/config.rs

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ use std::path::PathBuf;
88

99
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1010
pub struct Config {
11+
#[serde(skip)]
12+
pub path: Option<PathBuf>,
13+
1114
#[serde(default)]
1215
pub ui: UiConfig,
1316

@@ -38,6 +41,9 @@ pub struct UiConfig {
3841
#[serde(default = "default_theme")]
3942
pub theme: String,
4043

44+
#[serde(default = "default_code_theme")]
45+
pub code_theme: String,
46+
4147
#[serde(default = "default_outline_width")]
4248
pub outline_width: u16,
4349

@@ -241,6 +247,7 @@ impl Default for UiConfig {
241247
fn default() -> Self {
242248
Self {
243249
theme: default_theme(),
250+
code_theme: default_code_theme(),
244251
outline_width: default_outline_width(),
245252
tree_style: default_tree_style(),
246253
}
@@ -264,6 +271,10 @@ fn default_theme() -> String {
264271
"OceanDark".to_string()
265272
}
266273

274+
fn default_code_theme() -> String {
275+
"base16-ocean.dark".to_string()
276+
}
277+
267278
fn default_outline_width() -> u16 {
268279
30
269280
}
@@ -284,55 +295,56 @@ impl Config {
284295
/// - macOS: ~/Library/Application Support/treemd/config.toml
285296
/// - Linux: ~/.config/treemd/config.toml
286297
/// - Windows: %APPDATA%/treemd/config.toml
287-
pub fn config_path() -> Option<PathBuf> {
298+
fn config_path() -> Option<PathBuf> {
288299
dirs::config_dir().map(|p| p.join("treemd").join("config.toml"))
289300
}
290301

291-
/// Load config from file, or return default if file doesn't exist
302+
/// Resolve the config file path
292303
/// On macOS, checks ~/.config/treemd first, then falls back to ~/Library/Application Support
293-
pub fn load() -> Self {
304+
fn resolve_config_path() -> Option<PathBuf> {
294305
#[cfg(target_os = "macos")]
295-
{
296-
// Prefer XDG-style path on macOS for CLI tools
297-
if let Some(xdg_path) = Self::xdg_config_path()
298-
&& let Ok(contents) = fs::read_to_string(&xdg_path)
299-
{
300-
match toml::from_str(&contents) {
301-
Ok(config) => return config,
302-
Err(e) => {
303-
eprintln!(
304-
"warning: failed to parse config {}: {} (using defaults)",
305-
xdg_path.display(),
306-
e
307-
);
308-
return Self::default();
309-
}
310-
}
311-
}
312-
}
306+
return Self::xdg_config_path().or_else(Self::config_path);
313307

314-
// Fall back to platform-specific path
308+
#[cfg(not(target_os = "macos"))]
315309
Self::config_path()
316-
.and_then(|path| {
317-
let contents = fs::read_to_string(&path).ok()?;
318-
match toml::from_str(&contents) {
319-
Ok(config) => Some(config),
320-
Err(e) => {
321-
eprintln!(
322-
"warning: failed to parse config {}: {} (using defaults)",
323-
path.display(),
324-
e
325-
);
326-
None
327-
}
328-
}
310+
}
311+
312+
/// Load the configuration file, falling back to `Default` on error.
313+
fn load_from_path(path: &PathBuf) -> Self {
314+
let Ok(content) = fs::read_to_string(&path) else {
315+
return Self::default();
316+
};
317+
318+
match toml::from_str::<Self>(&content) {
319+
Ok(config) => return config,
320+
Err(e) => {
321+
eprintln!(
322+
"warning: failed to parse config {}: {} (using defaults)",
323+
path.display(),
324+
e
325+
);
326+
return Self::default();
327+
}
328+
};
329+
}
330+
331+
/// Resolve and load the configuration file, falling back to `Default` if any step fails.
332+
pub fn load() -> Self {
333+
Self::resolve_config_path()
334+
.map(|path| {
335+
let mut config = Self::load_from_path(&path);
336+
config.path = Some(path.to_owned());
337+
config
329338
})
330339
.unwrap_or_default()
331340
}
332341

333342
/// Save config to file
334343
pub fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
335-
let path = Self::config_path().ok_or("Could not determine config directory")?;
344+
let path = self
345+
.path
346+
.as_ref()
347+
.ok_or("Could not determine config directory")?;
336348

337349
// Create parent directory if it doesn't exist
338350
if let Some(parent) = path.parent() {
@@ -398,4 +410,13 @@ impl Config {
398410
pub fn is_compact_tree(&self) -> bool {
399411
self.ui.tree_style == "compact"
400412
}
413+
414+
/// Get the path of the directory that contains the user's sublime color schemes
415+
/// (used for syntax highlighting in code blocks)
416+
pub fn code_theme_dir_path(&self) -> Option<PathBuf> {
417+
self.path
418+
.as_ref()
419+
.and_then(|path| path.parent())
420+
.map(|parent| parent.join("code-themes"))
421+
}
401422
}

src/tui/app.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,11 @@ impl App {
503503
.with_color_mode(color_mode, current_theme)
504504
.with_custom_colors(&config.theme, color_mode);
505505

506+
// Load sublime color scheme directory
507+
let code_theme_dir = config.code_theme_dir_path();
508+
// Load sublime color scheme name (for code higlighting)
509+
let code_theme = config.ui.code_theme.as_str();
510+
506511
// Load outline width from config
507512
let outline_width = config.ui.outline_width;
508513

@@ -531,7 +536,7 @@ impl App {
531536
show_search: false,
532537
outline_search_active: false,
533538
search_query: String::new(),
534-
highlighter: SyntaxHighlighter::new(),
539+
highlighter: SyntaxHighlighter::new(code_theme, code_theme_dir),
535540
show_outline: true,
536541
outline_width,
537542
config_has_custom_outline_width,

src/tui/syntax.rs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::path::PathBuf;
2+
13
use ratatui::style::{Color, Modifier, Style};
24
use ratatui::text::{Line, Span};
35
use syntect::easy::HighlightLines;
@@ -11,10 +13,18 @@ pub struct SyntaxHighlighter {
1113
}
1214

1315
impl SyntaxHighlighter {
14-
pub fn new() -> Self {
16+
pub fn new(theme: &str, theme_dir: Option<PathBuf>) -> Self {
1517
let syntax_set = SyntaxSet::load_defaults_newlines();
16-
let theme_set = ThemeSet::load_defaults();
17-
let theme = theme_set.themes["base16-ocean.dark"].clone();
18+
let mut theme_set = ThemeSet::load_defaults();
19+
if let Some(dir) = theme_dir {
20+
// Load the user themes if any (silently ignore errors)
21+
let _ = theme_set.add_from_folder(dir);
22+
}
23+
24+
let theme = theme_set.themes.get(theme).cloned().unwrap_or_else(|| {
25+
// Fallback to the first theme (we know there is one since `load_defaults was used`)
26+
theme_set.themes.first_entry().unwrap().get().clone()
27+
});
1828

1929
Self { syntax_set, theme }
2030
}
@@ -82,9 +92,3 @@ impl SyntaxHighlighter {
8292
.to_lowercase()
8393
}
8494
}
85-
86-
impl Default for SyntaxHighlighter {
87-
fn default() -> Self {
88-
Self::new()
89-
}
90-
}

0 commit comments

Comments
 (0)