Compare commits

..

No commits in common. "5dc0b6d638915a7ae09da384da66bd7f18c98787" and "fd26344b5fb96dc757c3dc7aa523d2151b6d1e0e" have entirely different histories.

18 changed files with 109 additions and 567 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "ced" name = "ced"
version = "0.1.3" version = "0.0.9"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
@ -8,12 +8,7 @@ eframe = "0.32"
egui = "0.32" egui = "0.32"
egui_extras = { version = "0.32", features = ["syntect"] } egui_extras = { version = "0.32", features = ["syntect"] }
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0"
rfd = "0.15.4" rfd = "0.15.4"
toml = "0.9.2" toml = "0.9.2"
dirs = "6.0" dirs = "6.0"
libc = "0.2.174" libc = "0.2.174"
syntect = "5.2.0"
plist = "1.7.4"
diffy = "0.4.2"
uuid = { version = "1.0", features = ["v4"] }

View File

@ -9,7 +9,7 @@ There is a disturbing lack of simple GUI text editors available on Linux nativel
## Features ## Features
* Sane text editing with standard keybindings (`Ctrl+A`, `Ctrl+C`, etc.). * Sane text editing with standard keybindings (`Ctrl+A`, `Ctrl+C`, etc.).
* Choose between opening fresh every time, like Notepad, or maintaining a consistent state like Notepad++. * Opens with a blank slate for quick typing, remember Notepad?
* Separate UI zoom that doesn't affect font size (`Ctrl+Shift` + `+`/`-`). * Separate UI zoom that doesn't affect font size (`Ctrl+Shift` + `+`/`-`).
* Ricers rejoice, your `pywal` colors will be used! * Ricers rejoice, your `pywal` colors will be used!
* Weirdly smooth typing experience. * Weirdly smooth typing experience.
@ -39,7 +39,6 @@ sudo install -Dm644 ced.desktop /usr/share/applications/ced.desktop
Here is an example `config.toml`: Here is an example `config.toml`:
```toml ```toml
state_cache = true
auto_hide_toolbar = false auto_hide_toolbar = false
show_line_numbers = false show_line_numbers = false
word_wrap = false word_wrap = false
@ -47,18 +46,15 @@ theme = "System"
line_side = false line_side = false
font_family = "Monospace" font_family = "Monospace"
font_size = 16.0 font_size = 16.0
syntax_highlighting = true
``` ```
### Options ### Options
| Option | Default | Description | | Option | Default | Description |
|--------|---------|-------------| |--------|---------|-------------|
| `state_cache` | `false` | If `true`, open files will have their unsaved changes cached and will be automatically opened when starting a new session. |
| `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. | | `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. | | `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`. | | `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. | | `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. | | `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. | | `font_family` | `"Proportional"` | The font family used for the editor text. |
@ -69,7 +65,9 @@ syntax_highlighting = true
In order of importance. In order of importance.
| Feature | Info | | Feature | Info |
| ------- | ---- | | ------- | ---- |
| **LSP:** | Looking at allowing you to use/attach your own tools for this. | | **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. |
| **Choose Font** | More than just Monospace/Proportional. | | **Choose Font** | More than just Monospace/Proportional. |
| **Vim Mode:** | It's in-escapable. | | **Vim Mode:** | It's in-escapable. |
| **CLI Mode:** | 💀 | | **CLI Mode:** | 💀 |

View File

@ -5,8 +5,6 @@ use super::theme::Theme;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config { pub struct Config {
#[serde(default = "default_state_cache")]
pub state_cache: bool,
#[serde(default = "default_auto_hide_toolbar")] #[serde(default = "default_auto_hide_toolbar")]
pub auto_hide_toolbar: bool, pub auto_hide_toolbar: bool,
#[serde(default = "default_hide_tab_bar")] #[serde(default = "default_hide_tab_bar")]
@ -28,10 +26,6 @@ pub struct Config {
// pub vim_mode: bool, // pub vim_mode: bool,
} }
fn default_state_cache() -> bool {
false
}
fn default_auto_hide_toolbar() -> bool { fn default_auto_hide_toolbar() -> bool {
false false
} }
@ -60,7 +54,6 @@ fn default_syntax_highlighting() -> bool {
impl Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
Self { Self {
state_cache: default_state_cache(),
auto_hide_toolbar: default_auto_hide_toolbar(), auto_hide_toolbar: default_auto_hide_toolbar(),
hide_tab_bar: default_hide_tab_bar(), hide_tab_bar: default_hide_tab_bar(),
show_line_numbers: default_show_line_numbers(), show_line_numbers: default_show_line_numbers(),

View File

@ -5,7 +5,6 @@ mod editor;
mod find; mod find;
mod lifecycle; mod lifecycle;
mod processing; mod processing;
mod state_cache;
mod tabs; mod tabs;
mod ui; mod ui;

View File

@ -5,7 +5,6 @@ use crate::app::theme;
impl TextEditor { impl TextEditor {
pub fn from_config(config: Config) -> Self { pub fn from_config(config: Config) -> Self {
Self { Self {
state_cache: config.state_cache,
show_line_numbers: config.show_line_numbers, show_line_numbers: config.show_line_numbers,
word_wrap: config.word_wrap, word_wrap: config.word_wrap,
auto_hide_toolbar: config.auto_hide_toolbar, auto_hide_toolbar: config.auto_hide_toolbar,
@ -21,11 +20,6 @@ impl TextEditor {
pub fn from_config_with_context(config: Config, cc: &eframe::CreationContext<'_>) -> Self { pub fn from_config_with_context(config: Config, cc: &eframe::CreationContext<'_>) -> Self {
let mut editor = Self::from_config(config); let mut editor = Self::from_config(config);
if let Err(e) = editor.load_state_cache() {
eprintln!("Failed to load state cache: {e}");
}
theme::apply(editor.theme, &cc.egui_ctx); theme::apply(editor.theme, &cc.egui_ctx);
cc.egui_ctx.options_mut(|o| o.zoom_with_keyboard = false); cc.egui_ctx.options_mut(|o| o.zoom_with_keyboard = false);
@ -52,7 +46,6 @@ impl TextEditor {
pub fn get_config(&self) -> Config { pub fn get_config(&self) -> Config {
Config { Config {
state_cache: self.state_cache,
auto_hide_toolbar: self.auto_hide_toolbar, auto_hide_toolbar: self.auto_hide_toolbar,
show_line_numbers: self.show_line_numbers, show_line_numbers: self.show_line_numbers,
hide_tab_bar: self.hide_tab_bar, hide_tab_bar: self.hide_tab_bar,

View File

@ -8,7 +8,6 @@ impl Default for TextEditor {
Self { Self {
tabs: vec![Tab::new_empty(1)], tabs: vec![Tab::new_empty(1)],
active_tab_index: 0, active_tab_index: 0,
state_cache: false,
tab_counter: 1, tab_counter: 1,
show_about: false, show_about: false,
show_shortcuts: false, show_shortcuts: false,

View File

@ -34,7 +34,6 @@ pub struct TextEditor {
pub(crate) tabs: Vec<Tab>, pub(crate) tabs: Vec<Tab>,
pub(crate) active_tab_index: usize, pub(crate) active_tab_index: usize,
pub(crate) tab_counter: usize, pub(crate) tab_counter: usize,
pub(crate) state_cache: bool,
pub(crate) show_about: bool, pub(crate) show_about: bool,
pub(crate) show_shortcuts: bool, pub(crate) show_shortcuts: bool,
pub(crate) show_find: bool, pub(crate) show_find: bool,

View File

@ -30,6 +30,7 @@ impl TextEditor {
let search_slice = if search_content.is_char_boundary(start) { let search_slice = if search_content.is_char_boundary(start) {
&search_content[start..] &search_content[start..]
} else { } else {
// Find next valid boundary
while start < search_content.len() && !search_content.is_char_boundary(start) { while start < search_content.len() && !search_content.is_char_boundary(start) {
start += 1; start += 1;
} }
@ -44,6 +45,7 @@ impl TextEditor {
self.find_matches self.find_matches
.push((absolute_pos, absolute_pos + query.len())); .push((absolute_pos, absolute_pos + query.len()));
// Advance to next valid character boundary instead of just +1
start = absolute_pos + 1; start = absolute_pos + 1;
while start < search_content.len() && !search_content.is_char_boundary(start) { while start < search_content.len() && !search_content.is_char_boundary(start) {
start += 1; start += 1;

View File

@ -15,22 +15,16 @@ impl TextEditor {
} }
pub fn request_quit(&mut self, ctx: &egui::Context) { pub fn request_quit(&mut self, ctx: &egui::Context) {
if self.has_unsaved_changes() && !self.state_cache { if self.has_unsaved_changes() {
self.pending_unsaved_action = Some(UnsavedAction::Quit); self.pending_unsaved_action = Some(UnsavedAction::Quit);
} else { } else {
self.clean_quit_requested = true; self.clean_quit_requested = true;
if let Err(e) = self.save_state_cache() {
eprintln!("Failed to save state cache: {e}");
}
ctx.send_viewport_cmd(egui::ViewportCommand::Close); ctx.send_viewport_cmd(egui::ViewportCommand::Close);
} }
} }
pub fn force_quit(&mut self, ctx: &egui::Context) { pub fn force_quit(&mut self, ctx: &egui::Context) {
self.force_quit_confirmed = true; self.force_quit_confirmed = true;
if let Err(e) = self.save_state_cache() {
eprintln!("Failed to save state cache: {e}");
}
ctx.send_viewport_cmd(egui::ViewportCommand::Close); ctx.send_viewport_cmd(egui::ViewportCommand::Close);
} }
@ -71,40 +65,49 @@ impl TextEditor {
}; };
let visuals = &ctx.style().visuals; let visuals = &ctx.style().visuals;
let error_color = visuals.error_fg_color;
egui::Window::new(title) egui::Window::new(title)
.collapsible(false) .collapsible(false)
.resizable(false) .resizable(false)
.anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO)
.frame(egui::Frame {
fill: visuals.window_fill,
stroke: visuals.window_stroke,
corner_radius: egui::CornerRadius::same(8),
shadow: visuals.window_shadow,
inner_margin: egui::Margin::same(16),
outer_margin: egui::Margin::same(0),
})
.show(ctx, |ui| { .show(ctx, |ui| {
ui.vertical_centered(|ui| { ui.vertical(|ui| {
ui.add_space(8.0);
ui.label(egui::RichText::new(&confirmation_text).size(14.0)); ui.label(egui::RichText::new(&confirmation_text).size(14.0));
ui.add_space(4.0); ui.add_space(8.0);
for file in &files_to_list { for file in &files_to_list {
ui.label(egui::RichText::new(file).size(12.0).color(error_color)); ui.label(egui::RichText::new(format!("{file}")).size(18.0).weak());
} }
ui.add_space(12.0); ui.add_space(12.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("Cancel").clicked() { let cancel_fill = ui.visuals().widgets.inactive.bg_fill;
let cancel_stroke = ui.visuals().widgets.inactive.bg_stroke;
let cancel_button = egui::Button::new("Cancel")
.fill(cancel_fill)
.stroke(cancel_stroke);
if ui.add(cancel_button).clicked() {
cancel_action = true; cancel_action = true;
} }
ui.add_space(8.0); ui.add_space(8.0);
if ui let destructive_color = ui.visuals().error_fg_color;
.button(egui::RichText::new(&button_text).color(error_color)) let confirm_button = egui::Button::new(&button_text)
.clicked() .fill(destructive_color)
{ .stroke(egui::Stroke::new(1.0, destructive_color));
close_action_now = Some(action.to_owned());
if ui.add(confirm_button).clicked() {
close_action_now = Some(action);
} }
}); });
ui.add_space(8.0);
}); });
}); });
@ -114,17 +117,9 @@ impl TextEditor {
if let Some(action) = close_action_now { if let Some(action) = close_action_now {
match action { match action {
UnsavedAction::Quit => { UnsavedAction::Quit => self.force_quit(ctx),
if let Err(e) = self.save_state_cache() {
eprintln!("Failed to save state cache: {e}");
}
self.force_quit(ctx);
}
UnsavedAction::CloseTab(tab_index) => { UnsavedAction::CloseTab(tab_index) => {
self.close_tab(tab_index); self.close_tab(tab_index);
if let Err(e) = self.save_state_cache() {
eprintln!("Failed to save state cache: {e}");
}
} }
} }
self.pending_unsaved_action = None; self.pending_unsaved_action = None;

View File

@ -1,248 +0,0 @@
use super::editor::TextEditor;
use crate::app::tab::{Tab, compute_content_hash};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CachedTab {
pub diff_file: Option<PathBuf>, // Path to diff file for modified tabs
pub full_content: Option<String>, // This is used for 'new files' that don't have a path
pub file_path: Option<PathBuf>,
pub is_modified: bool,
pub title: String,
pub original_content_hash: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateCache {
pub tabs: Vec<CachedTab>,
pub active_tab_index: usize,
pub tab_counter: usize,
}
fn create_diff_file(diff_content: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
let diffs_dir = TextEditor::diffs_cache_dir().ok_or("Cannot determine cache directory")?;
std::fs::create_dir_all(&diffs_dir)?;
let diff_filename = format!("{}.diff", Uuid::new_v4());
let diff_path = diffs_dir.join(diff_filename);
std::fs::write(&diff_path, diff_content)?;
Ok(diff_path)
}
fn load_diff_file(diff_path: &PathBuf) -> Result<String, Box<dyn std::error::Error>> {
Ok(std::fs::read_to_string(diff_path)?)
}
impl From<&Tab> for CachedTab {
fn from(tab: &Tab) -> Self {
if let Some(file_path) = &tab.file_path {
let original_content = std::fs::read_to_string(file_path).unwrap_or_default();
let diff_file = if tab.is_modified {
let diff_content = diffy::create_patch(&original_content, &tab.content);
match create_diff_file(&diff_content.to_string()) {
Ok(path) => Some(path),
Err(e) => {
eprintln!("Warning: Failed to create diff file: {}", e);
None
}
}
} else {
None
};
Self {
diff_file,
full_content: None,
file_path: tab.file_path.clone(),
is_modified: tab.is_modified,
title: tab.title.clone(),
original_content_hash: tab.original_content_hash,
}
} else {
Self {
diff_file: None,
full_content: Some(tab.content.clone()),
file_path: None,
is_modified: tab.is_modified,
title: tab.title.clone(),
original_content_hash: tab.original_content_hash,
}
}
}
}
impl From<CachedTab> for Tab {
fn from(cached: CachedTab) -> Self {
if let Some(file_path) = cached.file_path {
let original_content = std::fs::read_to_string(&file_path).unwrap_or_default();
let current_content = if let Some(diff_path) = cached.diff_file {
match load_diff_file(&diff_path) {
Ok(diff_content) => {
match diffy::Patch::from_str(&diff_content) {
Ok(patch) => {
match diffy::apply(&original_content, &patch) {
Ok(content) => content,
Err(_) => {
eprintln!("Warning: Failed to apply diff for {}, using original content",
file_path.display());
original_content
}
}
}
Err(_) => {
eprintln!("Warning: Failed to parse diff for {}, using original content",
file_path.display());
original_content
}
}
}
Err(e) => {
eprintln!("Warning: Failed to load diff file {:?}: {}, using original content",
diff_path, e);
original_content
}
}
} else {
original_content
};
let original_hash = compute_content_hash(&std::fs::read_to_string(&file_path).unwrap_or_default());
let expected_hash = cached.original_content_hash;
let mut tab = Tab::new_with_file(current_content, file_path);
tab.title = cached.title;
if original_hash != expected_hash {
tab.is_modified = true;
} else {
tab.is_modified = cached.is_modified;
tab.original_content_hash = cached.original_content_hash;
}
tab
} else {
let content = cached.full_content.unwrap_or_default();
let mut tab = Tab::new_empty(1);
tab.content = content;
tab.title = cached.title;
tab.is_modified = cached.is_modified;
tab.original_content_hash = cached.original_content_hash;
tab
}
}
}
impl TextEditor {
pub fn state_cache_path() -> Option<PathBuf> {
let cache_dir = if let Some(cache_dir) = dirs::cache_dir() {
cache_dir.join(env!("CARGO_PKG_NAME"))
} else if let Some(home_dir) = dirs::home_dir() {
home_dir.join(".cache").join(env!("CARGO_PKG_NAME"))
} else {
return None;
};
Some(cache_dir.join("state.json"))
}
pub fn diffs_cache_dir() -> Option<PathBuf> {
let cache_dir = if let Some(cache_dir) = dirs::cache_dir() {
cache_dir.join(env!("CARGO_PKG_NAME"))
} else if let Some(home_dir) = dirs::home_dir() {
home_dir.join(".cache").join(env!("CARGO_PKG_NAME"))
} else {
return None;
};
Some(cache_dir.join("diffs"))
}
fn cleanup_orphaned_diffs(active_diff_files: &[PathBuf]) -> Result<(), Box<dyn std::error::Error>> {
if let Some(diffs_dir) = Self::diffs_cache_dir() {
if diffs_dir.exists() {
for entry in std::fs::read_dir(diffs_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("diff") {
if !active_diff_files.contains(&path) {
let _ = std::fs::remove_file(path);
}
}
}
}
}
Ok(())
}
pub fn load_state_cache(&mut self) -> Result<(), Box<dyn std::error::Error>> {
if !self.state_cache {
return Ok(());
}
let cache_path = Self::state_cache_path().ok_or("Cannot determine cache directory")?;
if !cache_path.exists() {
return Ok(());
}
let content = std::fs::read_to_string(&cache_path)?;
let state_cache: StateCache = serde_json::from_str(&content)?;
if !state_cache.tabs.is_empty() {
self.tabs = state_cache.tabs.into_iter().map(Tab::from).collect();
self.active_tab_index = std::cmp::min(state_cache.active_tab_index, self.tabs.len() - 1);
self.tab_counter = state_cache.tab_counter;
self.text_needs_processing = true;
}
Ok(())
}
pub fn save_state_cache(&self) -> Result<(), Box<dyn std::error::Error>> {
if !self.state_cache {
return Ok(());
}
let cache_path = Self::state_cache_path().ok_or("Cannot determine cache directory")?;
if let Some(parent) = cache_path.parent() {
std::fs::create_dir_all(parent)?;
}
let state_cache = StateCache {
tabs: self.tabs.iter().map(CachedTab::from).collect(),
active_tab_index: self.active_tab_index,
tab_counter: self.tab_counter,
};
let active_diff_files: Vec<PathBuf> = state_cache.tabs
.iter()
.filter_map(|tab| tab.diff_file.clone())
.collect();
let _ = Self::cleanup_orphaned_diffs(&active_diff_files);
let content = serde_json::to_string_pretty(&state_cache)?;
std::fs::write(&cache_path, content)?;
Ok(())
}
pub fn clear_state_cache() -> Result<(), Box<dyn std::error::Error>> {
if let Some(cache_path) = Self::state_cache_path() {
if cache_path.exists() {
std::fs::remove_file(cache_path)?;
}
}
if let Some(diffs_dir) = Self::diffs_cache_dir() {
if diffs_dir.exists() {
let _ = std::fs::remove_dir_all(diffs_dir);
}
}
Ok(())
}
}

View File

@ -18,10 +18,6 @@ impl TextEditor {
self.update_find_matches(); self.update_find_matches();
} }
self.text_needs_processing = true; self.text_needs_processing = true;
if let Err(e) = self.save_state_cache() {
eprintln!("Failed to save state cache: {e}");
}
} }
pub fn close_tab(&mut self, tab_index: usize) { pub fn close_tab(&mut self, tab_index: usize) {
@ -36,10 +32,6 @@ impl TextEditor {
self.update_find_matches(); self.update_find_matches();
} }
self.text_needs_processing = true; self.text_needs_processing = true;
if let Err(e) = self.save_state_cache() {
eprintln!("Failed to save state cache: {e}");
}
} }
} }
@ -50,10 +42,6 @@ impl TextEditor {
self.update_find_matches(); self.update_find_matches();
} }
self.text_needs_processing = true; self.text_needs_processing = true;
if let Err(e) = self.save_state_cache() {
eprintln!("Failed to save state cache: {e}");
}
} }
} }
} }

View File

@ -23,6 +23,7 @@ impl TextEditor {
} }
} }
/// Get the configured font ID based on the editor's font settings
pub fn get_font_id(&self) -> egui::FontId { pub fn get_font_id(&self) -> egui::FontId {
let font_family = match self.font_family.as_str() { let font_family = match self.font_family.as_str() {
"Monospace" => egui::FontFamily::Monospace, "Monospace" => egui::FontFamily::Monospace,
@ -31,11 +32,13 @@ impl TextEditor {
egui::FontId::new(self.font_size, font_family) egui::FontId::new(self.font_size, font_family)
} }
/// Immediately apply theme and save to configuration
pub fn set_theme(&mut self, ctx: &egui::Context) { pub fn set_theme(&mut self, ctx: &egui::Context) {
theme::apply(self.theme, ctx); theme::apply(self.theme, ctx);
self.save_config(); self.save_config();
} }
/// Apply font settings with immediate text reprocessing
pub fn apply_font_settings(&mut self, ctx: &egui::Context) { pub fn apply_font_settings(&mut self, ctx: &egui::Context) {
let font_family = match self.font_family.as_str() { let font_family = match self.font_family.as_str() {
"Monospace" => egui::FontFamily::Monospace, "Monospace" => egui::FontFamily::Monospace,
@ -53,18 +56,21 @@ impl TextEditor {
self.save_config(); self.save_config();
} }
/// Apply font settings with immediate text reprocessing
pub fn apply_font_settings_with_ui(&mut self, ctx: &egui::Context, ui: &egui::Ui) { pub fn apply_font_settings_with_ui(&mut self, ctx: &egui::Context, ui: &egui::Ui) {
self.apply_font_settings(ctx); self.apply_font_settings(ctx);
self.reprocess_text_for_font_change(ui); self.reprocess_text_for_font_change(ui);
self.font_settings_changed = false; self.font_settings_changed = false;
} }
/// Trigger immediate text reprocessing when font settings change
pub fn reprocess_text_for_font_change(&mut self, ui: &egui::Ui) { pub fn reprocess_text_for_font_change(&mut self, ui: &egui::Ui) {
if let Some(active_tab) = self.get_active_tab() { if let Some(active_tab) = self.get_active_tab() {
self.process_text_for_rendering(&active_tab.content.to_string(), ui); self.process_text_for_rendering(&active_tab.content.to_string(), ui);
} }
} }
/// Calculates the available width for the text editor, accounting for line numbers and separator
pub fn calculate_editor_dimensions(&self, ui: &egui::Ui) -> EditorDimensions { pub fn calculate_editor_dimensions(&self, ui: &egui::Ui) -> EditorDimensions {
let total_available_width = ui.available_width(); let total_available_width = ui.available_width();
@ -108,6 +114,7 @@ impl TextEditor {
} }
} }
/// Calculate the available width for non-word-wrapped content based on processed text data
pub fn calculate_content_based_width(&self, ui: &egui::Ui) -> f32 { pub fn calculate_content_based_width(&self, ui: &egui::Ui) -> f32 {
let processing_result = self.get_text_processing_result(); let processing_result = self.get_text_processing_result();

View File

@ -5,7 +5,8 @@ use std::path::PathBuf;
pub fn compute_content_hash(content: &str) -> u64 { pub fn compute_content_hash(content: &str) -> u64 {
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
content.hash(&mut hasher); content.hash(&mut hasher);
hasher.finish() let hash = hasher.finish();
hash
} }
#[derive(Clone)] #[derive(Clone)]

View File

@ -1,7 +1,5 @@
use eframe::egui; use eframe::egui;
use plist::{Dictionary, Value}; use egui_extras::syntax_highlighting::CodeTheme;
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)] #[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, Default)]
pub enum Theme { pub enum Theme {
@ -198,102 +196,11 @@ fn detect_system_dark_mode() -> bool {
} }
} }
fn egui_color_to_syntect(color: egui::Color32) -> SyntectColor { pub fn create_code_theme_from_visuals(visuals: &egui::Visuals, font_size: f32) -> CodeTheme {
SyntectColor { if visuals.dark_mode {
r: color.r(), CodeTheme::dark(font_size)
g: color.g(), } else {
b: color.b(), CodeTheme::light(font_size)
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();
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));
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));
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));
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));
root_dict.insert("settings".to_string(), Value::Array(settings_array));
Value::Dictionary(root_dict)
}

View File

@ -43,10 +43,6 @@ pub(crate) fn open_file(app: &mut TextEditor) {
if app.show_find && !app.find_query.is_empty() { if app.show_find && !app.find_query.is_empty() {
app.update_find_matches(); app.update_find_matches();
} }
if let Err(e) = app.save_state_cache() {
eprintln!("Failed to save state cache: {e}");
}
} }
Err(err) => { Err(err) => {
eprintln!("Failed to open file: {err}"); eprintln!("Failed to open file: {err}");
@ -85,10 +81,6 @@ pub(crate) fn save_to_path(app: &mut TextEditor, path: PathBuf) {
active_tab.file_path = Some(path.to_path_buf()); active_tab.file_path = Some(path.to_path_buf());
active_tab.title = title.to_string(); active_tab.title = title.to_string();
active_tab.mark_as_saved(); active_tab.mark_as_saved();
if let Err(e) = app.save_state_cache() {
eprintln!("Failed to save state cache: {e}");
}
} }
Err(err) => { Err(err) => {
eprintln!("Failed to save file: {err}"); eprintln!("Failed to save file: {err}");

View File

@ -4,6 +4,7 @@ use egui_extras::syntax_highlighting::{self};
use super::find_highlight; use super::find_highlight;
pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::Response { 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 _current_match_position = app.get_current_match_position();
let show_find = app.show_find; let show_find = app.show_find;
@ -100,15 +101,11 @@ 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 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 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 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 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) syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, &language)
} else { } else {
syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, "") syntax_highlighting::highlight(ui.ctx(), &ui.style().clone(), &theme, text, "")
@ -158,12 +155,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
None None
}; };
if content_changed {
if let Err(e) = app.save_state_cache() {
eprintln!("Failed to save state cache: {e}");
}
}
if content_changed && app.show_find && !app.find_query.is_empty() { if content_changed && app.show_find && !app.find_query.is_empty() {
app.update_find_matches(); app.update_find_matches();
} }

View File

@ -1,11 +1,5 @@
pub fn get_language_from_extension(file_path: Option<&std::path::Path>) -> String { pub fn get_language_from_extension(file_path: Option<&std::path::Path>) -> String {
let default_lang = "txt".to_string(); if let Some(path) = file_path {
let path = match file_path {
Some(p) => p,
None => return default_lang,
};
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) { if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
match extension.to_lowercase().as_str() { match extension.to_lowercase().as_str() {
"rs" => "rs".to_string(), "rs" => "rs".to_string(),
@ -39,17 +33,23 @@ pub fn get_language_from_extension(file_path: Option<&std::path::Path>) -> Strin
"vim" => "vim".to_string(), "vim" => "vim".to_string(),
"dockerfile" => "dockerfile".to_string(), "dockerfile" => "dockerfile".to_string(),
"makefile" => "makefile".to_string(), "makefile" => "makefile".to_string(),
_ => default_lang, _ => "txt".to_string(),
} }
} else if let Some(filename) = path.file_name().and_then(|name| name.to_str()) { } 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() { match filename.to_lowercase().as_str() {
"dockerfile" => "dockerfile".to_string(), "dockerfile" => "dockerfile".to_string(),
"makefile" => "makefile".to_string(), "makefile" => "makefile".to_string(),
"cargo.toml" | "pyproject.toml" => "toml".to_string(), "cargo.toml" | "pyproject.toml" => "toml".to_string(),
"package.json" => "json".to_string(), "package.json" => "json".to_string(),
_ => default_lang, _ => "txt".to_string(),
} }
} else { } else {
default_lang "txt".to_string()
}
}
} else {
"txt".to_string()
} }
} }

View File

@ -26,77 +26,6 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
}) })
.show(ctx, |ui| { .show(ctx, |ui| {
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
ui.heading("General Settings");
ui.add_space(8.0);
ui.horizontal(|ui| {
if ui
.checkbox(&mut app.state_cache, "Cache State")
.on_hover_text(
"Unsaved changes will be cached between sessions"
)
.changed()
{
app.save_config();
if !app.state_cache {
if let Err(e) = TextEditor::clear_state_cache() {
eprintln!("Failed to clear state cache: {e}");
}
}
}
});
ui.add_space(4.0);
ui.horizontal(|ui| {
if ui
.checkbox(&mut app.show_line_numbers, "Show Line Numbers")
.changed()
{
app.save_config();
}
if ui
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
.changed()
{
app.save_config();
}
});
ui.add_space(4.0);
ui.horizontal(|ui| {
if ui
.checkbox(&mut app.word_wrap, "Word Wrap")
.changed()
{
app.save_config();
}
});
ui.add_space(4.0);
ui.horizontal(|ui| {
if ui
.checkbox(&mut app.auto_hide_toolbar, "Auto Hide Toolbar")
.on_hover_text("Hide the menu bar until you move your mouse to the top")
.changed()
{
app.save_config();
}
if ui
.checkbox(&mut app.hide_tab_bar, "Hide Tab Bar")
.on_hover_text("Hide the tab bar and show tab title in menu bar instead")
.changed()
{
app.save_config();
}
});
ui.add_space(12.0);
ui.separator();
ui.heading("Font Settings"); ui.heading("Font Settings");
ui.add_space(8.0); ui.add_space(8.0);
@ -131,7 +60,7 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
}); });
if changed { if changed {
app.apply_font_settings(ctx); app.apply_font_settings_with_ui(ctx, ui);
} }
}); });
@ -170,15 +99,17 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
let clamped_size = new_size.clamp(8.0, 32.0); let clamped_size = new_size.clamp(8.0, 32.0);
if (app.font_size - clamped_size).abs() > 0.1 { if (app.font_size - clamped_size).abs() > 0.1 {
app.font_size = clamped_size; app.font_size = clamped_size;
app.apply_font_settings(ctx); app.apply_font_settings_with_ui(ctx, ui);
} }
} }
app.font_size_input = None; app.font_size_input = None;
} }
}); });
ui.add_space(8.0); ui.add_space(12.0);
ui.separator();
ui.add_space(8.0);
ui.label("Preview:"); ui.label("Preview:");
ui.add_space(4.0); ui.add_space(4.0);