colors #6

Merged
candle merged 11 commits from colors into master 2025-07-26 15:58:05 +00:00
10 changed files with 102 additions and 44 deletions
Showing only changes of commit 5dc0b6d638 - Show all commits

View File

@ -1,6 +1,6 @@
[package]
name = "ced"
version = "0.0.9"
version = "0.1.3"
edition = "2024"
[dependencies]
@ -16,3 +16,4 @@ 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
* Sane text editing with standard keybindings (`Ctrl+A`, `Ctrl+C`, etc.).
* Opens with a blank slate for quick typing, remember Notepad?
* Choose between opening fresh every time, like Notepad, or maintaining a consistent state like Notepad++.
* Separate UI zoom that doesn't affect font size (`Ctrl+Shift` + `+`/`-`).
* Ricers rejoice, your `pywal` colors will be used!
* Weirdly smooth typing experience.
@ -39,6 +39,7 @@ sudo install -Dm644 ced.desktop /usr/share/applications/ced.desktop
Here is an example `config.toml`:
```toml
state_cache = true
auto_hide_toolbar = false
show_line_numbers = false
word_wrap = false
@ -53,6 +54,7 @@ syntax_highlighting = true
| 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. |
| `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`. |
@ -67,7 +69,6 @@ syntax_highlighting = true
In order of importance.
| Feature | Info |
| ------- | ---- |
| **State/Cache:** | A toggleable option to keep an application state and prevent "Quit without saving" warnings. |
| **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. |

View File

@ -22,7 +22,6 @@ impl TextEditor {
pub fn from_config_with_context(config: Config, cc: &eframe::CreationContext<'_>) -> Self {
let mut editor = Self::from_config(config);
// Load state cache if enabled
if let Err(e) = editor.load_state_cache() {
eprintln!("Failed to load state cache: {e}");
}

View File

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

View File

@ -2,10 +2,11 @@ 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: Option<String>,
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,
@ -20,18 +21,40 @@ pub struct StateCache {
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 = if tab.is_modified {
Some(diffy::create_patch(&original_content, &tab.content).to_string())
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,
diff_file,
full_content: None,
file_path: tab.file_path.clone(),
is_modified: tab.is_modified,
@ -40,7 +63,7 @@ impl From<&Tab> for CachedTab {
}
} else {
Self {
diff: None,
diff_file: None,
full_content: Some(tab.content.clone()),
file_path: None,
is_modified: tab.is_modified,
@ -55,8 +78,10 @@ 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_str) = cached.diff {
match diffy::Patch::from_str(&diff_str) {
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,
@ -73,6 +98,13 @@ impl From<CachedTab> for Tab {
original_content
}
}
}
Err(e) => {
eprintln!("Warning: Failed to load diff file {:?}: {}, using original content",
diff_path, e);
original_content
}
}
} else {
original_content
};
@ -116,6 +148,35 @@ impl TextEditor {
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(());
@ -157,6 +218,12 @@ impl TextEditor {
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)?;
@ -169,6 +236,13 @@ impl TextEditor {
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

@ -19,7 +19,6 @@ impl TextEditor {
}
self.text_needs_processing = true;
// Save state cache after adding new tab
if let Err(e) = self.save_state_cache() {
eprintln!("Failed to save state cache: {e}");
}
@ -38,7 +37,6 @@ impl TextEditor {
}
self.text_needs_processing = true;
// Save state cache after closing tab
if let Err(e) = self.save_state_cache() {
eprintln!("Failed to save state cache: {e}");
}
@ -53,7 +51,6 @@ impl TextEditor {
}
self.text_needs_processing = true;
// Save state cache after switching tabs
if let Err(e) = self.save_state_cache() {
eprintln!("Failed to save state cache: {e}");
}

View File

@ -23,7 +23,6 @@ impl TextEditor {
}
}
/// Get the configured font ID based on the editor's font settings
pub fn get_font_id(&self) -> egui::FontId {
let font_family = match self.font_family.as_str() {
"Monospace" => egui::FontFamily::Monospace,
@ -32,13 +31,11 @@ impl TextEditor {
egui::FontId::new(self.font_size, font_family)
}
/// Immediately apply theme and save to configuration
pub fn set_theme(&mut self, ctx: &egui::Context) {
theme::apply(self.theme, ctx);
self.save_config();
}
/// Apply font settings with immediate text reprocessing
pub fn apply_font_settings(&mut self, ctx: &egui::Context) {
let font_family = match self.font_family.as_str() {
"Monospace" => egui::FontFamily::Monospace,
@ -56,21 +53,18 @@ impl TextEditor {
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) {
self.apply_font_settings(ctx);
self.reprocess_text_for_font_change(ui);
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) {
if let Some(active_tab) = self.get_active_tab() {
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 {
let total_available_width = ui.available_width();
@ -114,7 +108,6 @@ 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 {
let processing_result = self.get_text_processing_result();

View File

@ -261,7 +261,6 @@ fn build_custom_theme_plist(
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()));
@ -269,7 +268,6 @@ fn build_custom_theme_plist(
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()));
@ -279,7 +277,6 @@ fn build_custom_theme_plist(
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()));
@ -288,7 +285,6 @@ fn build_custom_theme_plist(
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()));
@ -297,8 +293,6 @@ fn build_custom_theme_plist(
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)

View File

@ -158,7 +158,6 @@ pub(super) fn editor_view_ui(ui: &mut egui::Ui, app: &mut TextEditor) -> egui::R
None
};
// Save state cache when content changes (after releasing the borrow)
if content_changed {
if let Err(e) = app.save_state_cache() {
eprintln!("Failed to save state cache: {e}");

View File

@ -32,9 +32,9 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
ui.horizontal(|ui| {
if ui
.checkbox(&mut app.state_cache, "State Cache")
.checkbox(&mut app.state_cache, "Cache State")
.on_hover_text(
"Save and restore open tabs and unsaved changes between sessions"
"Unsaved changes will be cached between sessions"
)
.changed()
{
@ -56,6 +56,12 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
{
app.save_config();
}
if ui
.checkbox(&mut app.syntax_highlighting, "Syntax Highlighting")
.changed()
{
app.save_config();
}
});
ui.add_space(4.0);
@ -79,11 +85,6 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
{
app.save_config();
}
});
ui.add_space(4.0);
ui.horizontal(|ui| {
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")
@ -91,6 +92,7 @@ pub(crate) fn preferences_window(app: &mut TextEditor, ctx: &egui::Context) {
{
app.save_config();
}
});
ui.add_space(12.0);