From 4651d7caf4a6cbe25c1130acab19968c8223c877 Mon Sep 17 00:00:00 2001 From: candle Date: Wed, 23 Jul 2025 11:46:54 -0400 Subject: [PATCH] checkpoint for theming --- Cargo.toml | 2 + README.md | 5 +- src/app/theme.rs | 111 ++++++++++++++++++++++++++++-- src/ui/central_panel/editor.rs | 13 ++-- src/ui/central_panel/languages.rs | 100 +++++++++++++-------------- 5 files changed, 168 insertions(+), 63 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b38d858..7af4b35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,5 @@ rfd = "0.15.4" toml = "0.9.2" dirs = "6.0" libc = "0.2.174" +syntect = "5.2.0" +plist = "1.7.4" diff --git a/README.md b/README.md index 10272ef..50117b3 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ theme = "System" line_side = false font_family = "Monospace" font_size = 16.0 +syntax_highlighting = true ``` ### Options @@ -55,6 +56,7 @@ font_size = 16.0 | `auto_hide_toolbar` | `false` | If `true`, the menu bar at the top will be hidden. Move your mouse to the top of the window to reveal it. | | `hide_tab_bar` | 'true' | If `false`, a separate tab bar will be drawn below the toolbar. | | `show_line_numbers` | `false` | If `true`, line numbers will be displayed on the side specified by `line_side`. | +| `syntax_highlighting` | `false` | If `true`, text will be highlighted based on detected language. | | `line_side` | `false` | If `false`, line numbers are on the left. If `true`, they are on the right. | | `word_wrap` | `false` | If `true`, lines will wrap when they reach the edge of the window. | | `font_family` | `"Proportional"` | The font family used for the editor text. | @@ -65,9 +67,8 @@ font_size = 16.0 In order of importance. | Feature | Info | | ------- | ---- | -| **Find/Replace:** | Functioning. | | **State/Cache:** | A toggleable option to keep an application state and prevent "Quit without saving" warnings. | -| **Syntax Highlighting/LSP:** | Looking at allowing you to use/attach your own tools for this. | +| **LSP:** | Looking at allowing you to use/attach your own tools for this. | | **Choose Font** | More than just Monospace/Proportional. | | **Vim Mode:** | It's in-escapable. | | **CLI Mode:** | 💀 | diff --git a/src/app/theme.rs b/src/app/theme.rs index 52d1f6d..0a6dac4 100644 --- a/src/app/theme.rs +++ b/src/app/theme.rs @@ -1,5 +1,7 @@ use eframe::egui; -use egui_extras::syntax_highlighting::CodeTheme; +use plist::{Dictionary, Value}; +use std::collections::BTreeMap; +use syntect::highlighting::{Theme as SyntectTheme, ThemeSet, ThemeSettings, Color as SyntectColor, UnderlineOption}; #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, Default)] pub enum Theme { @@ -196,11 +198,108 @@ fn detect_system_dark_mode() -> bool { } } -pub fn create_code_theme_from_visuals(visuals: &egui::Visuals, font_size: f32) -> CodeTheme { - if visuals.dark_mode { - CodeTheme::dark(font_size) - } else { - CodeTheme::light(font_size) +fn egui_color_to_syntect(color: egui::Color32) -> SyntectColor { + SyntectColor { + r: color.r(), + g: color.g(), + b: color.b(), + a: color.a(), } } +pub fn create_code_theme_from_visuals(visuals: &egui::Visuals, font_size: f32) -> ThemeSet { + let text_color = visuals.override_text_color.unwrap_or(visuals.text_color()); + let bg_color = visuals.extreme_bg_color; + let selection_color = visuals.selection.bg_fill; + let comment_color = blend_colors(text_color, bg_color, 0.6); + let keyword_color = if visuals.dark_mode { + blend_colors(egui::Color32::from_rgb(100, 149, 237), text_color, 0.8) // CornflowerBlue-like + } else { + blend_colors(egui::Color32::from_rgb(0, 0, 139), text_color, 0.8) // DarkBlue-like + }; + let string_color = if visuals.dark_mode { + blend_colors(egui::Color32::from_rgb(144, 238, 144), text_color, 0.8) // LightGreen-like + } else { + blend_colors(egui::Color32::from_rgb(0, 128, 0), text_color, 0.8) // Green-like + }; + let number_color = if visuals.dark_mode { + blend_colors(egui::Color32::from_rgb(255, 165, 0), text_color, 0.8) // Orange-like + } else { + blend_colors(egui::Color32::from_rgb(165, 42, 42), text_color, 0.8) // Brown-like + }; + let function_color = if visuals.dark_mode { + blend_colors(egui::Color32::from_rgb(255, 20, 147), text_color, 0.8) // DeepPink-like + } else { + blend_colors(egui::Color32::from_rgb(128, 0, 128), text_color, 0.8) // Purple-like + }; + + let plist_theme = build_custom_theme_plist("System", &format!("{:?}", bg_color), &format!("{:?}", text_color), &format!("{:?}", comment_color), &format!("{:?}", string_color), &format!("{:?}", keyword_color)); + let file = std::fs::File::create("system.tmTheme").unwrap(); + let writer = std::io::BufWriter::new(file); + + let _ =plist::to_writer_xml(writer, &plist_theme); + + let loaded_file = std::fs::File::open("system.tmTheme").unwrap(); + let mut loaded_reader = std::io::BufReader::new(loaded_file); + let loaded_theme = ThemeSet::load_from_reader(&mut loaded_reader).unwrap(); + let mut set = ThemeSet::new(); + set.add_from_folder(".").unwrap(); + return set; + +} + +fn build_custom_theme_plist( + theme_name: &str, + background_color: &str, + foreground_color: &str, + comment_color: &str, + string_color: &str, + keyword_color: &str, +) -> Value { + let mut root_dict = Dictionary::new(); + root_dict.insert("name".to_string(), Value::String(theme_name.to_string())); + + let mut settings_array = Vec::new(); + + // Global settings + let mut global_settings_dict = Dictionary::new(); + let mut inner_global_settings = Dictionary::new(); + inner_global_settings.insert("background".to_string(), Value::String(background_color.to_string())); + inner_global_settings.insert("foreground".to_string(), Value::String(foreground_color.to_string())); + global_settings_dict.insert("settings".to_string(), Value::Dictionary(inner_global_settings)); + settings_array.push(Value::Dictionary(global_settings_dict)); + + // Comment scope + let mut comment_scope_dict = Dictionary::new(); + comment_scope_dict.insert("name".to_string(), Value::String("Comment".to_string())); + comment_scope_dict.insert("scope".to_string(), Value::String("comment".to_string())); + let mut comment_settings = Dictionary::new(); + comment_settings.insert("foreground".to_string(), Value::String(comment_color.to_string())); + comment_settings.insert("fontStyle".to_string(), Value::String("italic".to_string())); + comment_scope_dict.insert("settings".to_string(), Value::Dictionary(comment_settings)); + settings_array.push(Value::Dictionary(comment_scope_dict)); + + // String scope + let mut string_scope_dict = Dictionary::new(); + string_scope_dict.insert("name".to_string(), Value::String("String".to_string())); + string_scope_dict.insert("scope".to_string(), Value::String("string".to_string())); + let mut string_settings = Dictionary::new(); + string_settings.insert("foreground".to_string(), Value::String(string_color.to_string())); + string_scope_dict.insert("settings".to_string(), Value::Dictionary(string_settings)); + settings_array.push(Value::Dictionary(string_scope_dict)); + + // Keyword scope + let mut keyword_scope_dict = Dictionary::new(); + keyword_scope_dict.insert("name".to_string(), Value::String("Keyword".to_string())); + keyword_scope_dict.insert("scope".to_string(), Value::String("keyword".to_string())); + let mut keyword_settings = Dictionary::new(); + keyword_settings.insert("foreground".to_string(), Value::String(keyword_color.to_string())); + keyword_scope_dict.insert("settings".to_string(), Value::Dictionary(keyword_settings)); + settings_array.push(Value::Dictionary(keyword_scope_dict)); + + // Add more scopes as needed... + + root_dict.insert("settings".to_string(), Value::Array(settings_array)); + + Value::Dictionary(root_dict) +} \ No newline at end of file diff --git a/src/ui/central_panel/editor.rs b/src/ui/central_panel/editor.rs index 0995422..fb8867b 100644 --- a/src/ui/central_panel/editor.rs +++ b/src/ui/central_panel/editor.rs @@ -4,7 +4,6 @@ use egui_extras::syntax_highlighting::{self}; use super::find_highlight; - pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::Response { let _current_match_position = app.get_current_match_position(); let show_find = app.show_find; @@ -101,22 +100,26 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R }; let language = super::languages::get_language_from_extension(active_tab.file_path.as_deref()); - let theme = crate::app::theme::create_code_theme_from_visuals(ui.visuals(), font_size); - let mut layouter = |ui: &egui::Ui, string: &dyn egui::TextBuffer, wrap_width: f32| { + // let syntect_theme = + // crate::app::theme::create_code_theme_from_visuals(ui.visuals(), font_size); let text = string.as_str(); + let theme = egui_extras::syntax_highlighting::CodeTheme::dark(font_size); let mut layout_job = if syntax_highlighting_enabled && language != "txt" { + // let mut settings = egui_extras::syntax_highlighting::SyntectSettings::default(); + // settings.ts = syntect_theme; + // syntax_highlighting::highlight_with(ui.ctx(), &ui.style().clone(), &theme, text, &language, &settings) syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, &language) } else { syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, "") }; - + if syntax_highlighting_enabled && language != "txt" { for section in &mut layout_job.sections { section.format.font_id = font_id.clone(); } } - + layout_job.wrap.max_width = wrap_width; ui.fonts(|f| f.layout_job(layout_job)) }; diff --git a/src/ui/central_panel/languages.rs b/src/ui/central_panel/languages.rs index 73525c8..d95574a 100644 --- a/src/ui/central_panel/languages.rs +++ b/src/ui/central_panel/languages.rs @@ -1,55 +1,55 @@ pub fn get_language_from_extension(file_path: Option<&std::path::Path>) -> String { - if let Some(path) = file_path { - if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) { - match extension.to_lowercase().as_str() { - "rs" => "rs".to_string(), - "py" => "py".to_string(), - "js" => "js".to_string(), - "ts" => "ts".to_string(), - "tsx" => "tsx".to_string(), - "jsx" => "jsx".to_string(), - "c" => "c".to_string(), - "cpp" | "cc" | "cxx" => "cpp".to_string(), - "h" | "hpp" => "cpp".to_string(), - "java" => "java".to_string(), - "go" => "go".to_string(), - "php" => "php".to_string(), - "rb" => "rb".to_string(), - "cs" => "cs".to_string(), - "swift" => "swift".to_string(), - "kt" => "kt".to_string(), - "scala" => "scala".to_string(), - "sh" | "bash" | "zsh" | "fish" => "sh".to_string(), - "html" | "htm" => "html".to_string(), - "xml" => "xml".to_string(), - "css" => "css".to_string(), - "scss" | "sass" => "scss".to_string(), - "json" => "json".to_string(), - "yaml" | "yml" => "yaml".to_string(), - "toml" => "toml".to_string(), - "md" | "markdown" => "md".to_string(), - "sql" => "sql".to_string(), - "lua" => "lua".to_string(), - "vim" => "vim".to_string(), - "dockerfile" => "dockerfile".to_string(), - "makefile" => "makefile".to_string(), - _ => "txt".to_string(), - } - } else { - // Check filename for special cases - if let Some(filename) = path.file_name().and_then(|name| name.to_str()) { - match filename.to_lowercase().as_str() { - "dockerfile" => "dockerfile".to_string(), - "makefile" => "makefile".to_string(), - "cargo.toml" | "pyproject.toml" => "toml".to_string(), - "package.json" => "json".to_string(), - _ => "txt".to_string(), - } - } else { - "txt".to_string() - } + let default_lang = "txt".to_string(); + + let path = match file_path { + Some(p) => p, + None => return default_lang, + }; + + if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) { + match extension.to_lowercase().as_str() { + "rs" => "rs".to_string(), + "py" => "py".to_string(), + "js" => "js".to_string(), + "ts" => "ts".to_string(), + "tsx" => "tsx".to_string(), + "jsx" => "jsx".to_string(), + "c" => "c".to_string(), + "cpp" | "cc" | "cxx" => "cpp".to_string(), + "h" | "hpp" => "cpp".to_string(), + "java" => "java".to_string(), + "go" => "go".to_string(), + "php" => "php".to_string(), + "rb" => "rb".to_string(), + "cs" => "cs".to_string(), + "swift" => "swift".to_string(), + "kt" => "kt".to_string(), + "scala" => "scala".to_string(), + "sh" | "bash" | "zsh" | "fish" => "sh".to_string(), + "html" | "htm" => "html".to_string(), + "xml" => "xml".to_string(), + "css" => "css".to_string(), + "scss" | "sass" => "scss".to_string(), + "json" => "json".to_string(), + "yaml" | "yml" => "yaml".to_string(), + "toml" => "toml".to_string(), + "md" | "markdown" => "md".to_string(), + "sql" => "sql".to_string(), + "lua" => "lua".to_string(), + "vim" => "vim".to_string(), + "dockerfile" => "dockerfile".to_string(), + "makefile" => "makefile".to_string(), + _ => default_lang, + } + } else if let Some(filename) = path.file_name().and_then(|name| name.to_str()) { + match filename.to_lowercase().as_str() { + "dockerfile" => "dockerfile".to_string(), + "makefile" => "makefile".to_string(), + "cargo.toml" | "pyproject.toml" => "toml".to_string(), + "package.json" => "json".to_string(), + _ => default_lang, } } else { - "txt".to_string() + default_lang } }