From b19905ce9472fcfd074f9a817a03eb9ddabbe5eb Mon Sep 17 00:00:00 2001 From: Penwing Date: Wed, 24 Jan 2024 16:30:28 +0100 Subject: [PATCH] fixed linebreak bug --- README.md | 3 +- calcifer_save.json | 2 +- src/calcifer.rs | 179 +------- src/calcifer/app_base.rs | 152 +++++++ src/calcifer/code_editor/mod.rs | 458 +++++++++++---------- src/calcifer/code_editor/themes/sonokai.rs | 36 +- src/calcifer_base.rs | 137 ++++++ src/calcifer_save.json | 2 +- src/main.rs | 7 +- src/tools/mod.rs | 162 +------- src/tools/search.rs | 4 +- src/tools/tabs.rs | 73 ++++ src/tools/terminal.rs | 68 +++ 13 files changed, 714 insertions(+), 569 deletions(-) create mode 100644 src/calcifer/app_base.rs create mode 100644 src/calcifer_base.rs create mode 100644 src/tools/tabs.rs create mode 100644 src/tools/terminal.rs diff --git a/README.md b/README.md index 874e4cb..1d401a4 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Added indent recognition (when there is a line break, the indentation level is k # 1.0.3 : Added testing -Added Ctrl+T : turn 4 spaces into tab across the whole document +Added Ctrl+T : refresh current tab Added Time debug Added Tree toggle for performance Added Alt+Arrows to move through tabs @@ -32,3 +32,4 @@ Added Zoom Added cd Added terminal color + diff --git a/calcifer_save.json b/calcifer_save.json index 19fa8a3..1336658 100644 --- a/calcifer_save.json +++ b/calcifer_save.json @@ -1 +1 @@ -{"tabs":["/home/penwing/Documents/notes/victory2.txt","/home/penwing/Documents/projects/rust/calcifer/src/calcifer/code_editor/themes/sonokai.rs"],"theme":4} \ No newline at end of file +{"tabs":["/home/penwing/Documents/notes/victory2.txt","/home/penwing/Documents/projects/rust/calcifer/src/calcifer/code_editor/themes/sonokai.rs"],"theme":6} \ No newline at end of file diff --git a/src/calcifer.rs b/src/calcifer.rs index 485016e..da48a29 100644 --- a/src/calcifer.rs +++ b/src/calcifer.rs @@ -1,17 +1,21 @@ use eframe::egui; use egui::{text::CCursor, text_edit::CCursorRange}; -use std::{env, path::Path, path::PathBuf, cmp::max, io, fs, cmp::min}; +use std::{env, path::Path, cmp::max}; + use crate::tools; +use crate::Calcifer; use crate::TIME_LABELS; use crate::PATH_ROOT; +use crate::MAX_TABS; pub mod code_editor; use code_editor::CodeEditor; use code_editor::themes::DEFAULT_THEMES; +mod app_base; -impl super::Calcifer { +impl Calcifer { pub fn draw_settings(&mut self, ctx: &egui::Context) { egui::TopBottomPanel::top("settings") .resizable(false) @@ -19,7 +23,7 @@ impl super::Calcifer { ui.horizontal(|ui| { if ui.add(egui::Button::new("open file")).clicked() { if let Some(path) = rfd::FileDialog::new().set_directory(Path::new(&PATH_ROOT)).pick_file() { - self.selected_tab = self.open_file(&path); + self.open_file(Some(&path)); } } ui.separator(); @@ -53,6 +57,7 @@ impl super::Calcifer { }); } + pub fn draw_tree_panel(&mut self, ctx: &egui::Context) { if !self.tree_display { return @@ -64,7 +69,8 @@ impl super::Calcifer { ui.separator(); }); } - + + pub fn draw_terminal_panel(&mut self, ctx: &egui::Context) { egui::TopBottomPanel::bottom("terminal") .default_height(super::TERMINAL_HEIGHT.clone()) @@ -113,6 +119,7 @@ impl super::Calcifer { }); } + pub fn draw_tab_panel(&mut self, ctx: &egui::Context) { egui::TopBottomPanel::top("tabs") .resizable(false) @@ -137,16 +144,17 @@ impl super::Calcifer { } ui.separator(); } - if tools::TabNumber::from_index(self.tabs.len()) != tools::TabNumber::None { + if self.tabs.len() < MAX_TABS { ui.selectable_value(&mut self.selected_tab, tools::TabNumber::Open, "+"); } if self.selected_tab == tools::TabNumber::Open { - self.selected_tab = self.new_tab(); + self.open_file(None); } }); }); } - + + pub fn draw_content_panel(&mut self, ctx: &egui::Context) { egui::CentralPanel::default().show(ctx, |ui| { ui.horizontal(|ui| { @@ -159,16 +167,14 @@ impl super::Calcifer { ui.label("Picked file:"); ui.monospace(self.tabs[self.selected_tab.to_index()].path.to_string_lossy().to_string()); }); - ui.separator(); - if self.selected_tab == tools::TabNumber::None { - return - } + ui.separator(); self.draw_code_file(ui); }); } + fn draw_code_file(&mut self, ui: &mut egui::Ui) { let current_tab = &mut self.tabs[self.selected_tab.to_index()]; let lines = current_tab.code.chars().filter(|&c| c == '\n').count() + 1; @@ -190,155 +196,4 @@ impl super::Calcifer { .with_numlines(true) .show(ui, &mut current_tab.code, &mut current_tab.saved, &mut current_tab.last_cursor, &mut current_tab.scroll_offset, override_cursor); } - - pub fn save_tab(&self) -> Option { - if self.tabs[self.selected_tab.to_index()].path.file_name().expect("Could not get Tab Name").to_string_lossy().to_string() == "untitled" { - return self.save_tab_as(); - } else { - if let Err(err) = fs::write(&self.tabs[self.selected_tab.to_index()].path, &self.tabs[self.selected_tab.to_index()].code) { - eprintln!("Error writing file: {}", err); - return None; - } - return Some(self.tabs[self.selected_tab.to_index()].path.clone()) - } - } - - pub fn save_tab_as(&self) -> Option { - if let Some(path) = rfd::FileDialog::new().save_file() { - if let Err(err) = fs::write(&path, &self.tabs[self.selected_tab.to_index()].code) { - eprintln!("Error writing file: {}", err); - return None; - } - return Some(path); - } - return None - } - - pub fn handle_save_file(&mut self, path_option : Option) { - if let Some(path) = path_option { - println!("File saved successfully at: {:?}", path); - self.tabs[self.selected_tab.to_index()].path = path; - self.tabs[self.selected_tab.to_index()].saved = true; - } else { - println!("File save failed."); - } - } - - pub fn from_app_state(app_state: tools::AppState) -> Self { - let mut new = Self { - theme: DEFAULT_THEMES[min(app_state.theme, DEFAULT_THEMES.len() - 1)], - tabs: Vec::new(), - ..Default::default() - }; - - for path in app_state.tabs { - if path.file_name().expect("Could not get Tab Name").to_string_lossy().to_string() != "untitled" { - new.open_file(&path); - } - } - - if new.tabs == vec![] { - new.new_tab(); - } - - new - } - - pub fn save_state(&self) { - let mut state_theme : usize = 0; - if let Some(theme) = DEFAULT_THEMES.iter().position(|&r| r == self.theme) { - state_theme = theme; - } - - let mut state_tabs = vec![]; - - for tab in &self.tabs { - state_tabs.push(tab.path.clone()); - } - let app_state = tools::AppState { - tabs: state_tabs, - theme: state_theme, - }; - - let _ = tools::save_state(&app_state, super::SAVE_PATH); - } - - pub fn indent_with_tabs(&mut self) { - let current_tab = &mut self.tabs[self.selected_tab.to_index()]; - current_tab.code = current_tab.code.replace(" ", "\t") - } - - pub fn move_through_tabs(&mut self, forward : bool) { - let new_index = if forward { - (self.selected_tab.to_index() + 1) % self.tabs.len() - } else { - self.selected_tab.to_index().checked_sub(1).unwrap_or(self.tabs.len() - 1) - }; - self.selected_tab = tools::TabNumber::from_index(new_index); - } - - fn list_files(&mut self, ui: &mut egui::Ui, path: &Path) -> io::Result<()> { - if let Some(name) = path.file_name() { - if path.is_dir() { - egui::CollapsingHeader::new(name.to_string_lossy()).show(ui, |ui| { - let mut paths: Vec<_> = fs::read_dir(&path).expect("Failed to read dir").map(|r| r.unwrap()).collect(); - - // Sort the vector using the custom sorting function - paths.sort_by(|a, b| tools::sort_directories_first(a, b)); - - for result in paths { - //let result = path_result.expect("Failed to get path"); - //let full_path = result.path(); - let _ = self.list_files(ui, &result.path()); - } - }); - } else { - //ui.label(name.to_string_lossy()); - if ui.button(name.to_string_lossy()).clicked() { - self.selected_tab = self.open_file(&path); - } - } - } - Ok(()) - } - - fn open_file(&mut self, path: &Path) -> tools::TabNumber { - if tools::TabNumber::from_index(self.tabs.len()) == tools::TabNumber::None { - return tools::TabNumber::None - } - - let new_tab = tools::Tab { - path: path.into(), - code: fs::read_to_string(path).expect("Not able to read the file").replace(" ", "\t"), - language: path.to_str().unwrap().split('.').last().unwrap().into(), - saved: true, - ..tools::Tab::default() - }; - self.tabs.push(new_tab); - - return tools::TabNumber::from_index(self.tabs.len() - 1) - } - - fn new_tab(&mut self) -> tools::TabNumber { - self.tabs.push(tools::Tab::default()); - return tools::TabNumber::from_index(self.tabs.len() - 1) - } - - fn delete_tab(&mut self, index : usize) -> tools::TabNumber { - self.tabs.remove(index); - return tools::TabNumber::from_index(min(index, self.tabs.len() - 1)) - } - - fn toggle(&self, ui: &mut egui::Ui, display : bool, title : &str) -> bool { - let text = if display.clone() { - format!("hide {}", title) - } else { - format!("show {}", title) - }; - - if ui.add(egui::Button::new(text)).clicked() { - return !display - } - return display - } } diff --git a/src/calcifer/app_base.rs b/src/calcifer/app_base.rs new file mode 100644 index 0000000..deabd67 --- /dev/null +++ b/src/calcifer/app_base.rs @@ -0,0 +1,152 @@ +use std::{path::PathBuf, fs, path::Path, cmp::min, io}; +use eframe::egui; + +use crate::Calcifer; +use crate::tools; +use crate::PATH_ROOT; +use crate::DEFAULT_THEMES; +use crate::MAX_TABS; +use crate::SAVE_PATH; + + +impl Calcifer { + pub fn save_tab(&self) -> Option { + if self.tabs[self.selected_tab.to_index()].path.file_name().expect("Could not get Tab Name").to_string_lossy().to_string() == "untitled" { + return self.save_tab_as(); + } else { + if let Err(err) = fs::write(&self.tabs[self.selected_tab.to_index()].path, &self.tabs[self.selected_tab.to_index()].code) { + eprintln!("Error writing file: {}", err); + return None; + } + return Some(self.tabs[self.selected_tab.to_index()].path.clone()) + } + } + + + pub fn save_tab_as(&self) -> Option { + if let Some(path) = rfd::FileDialog::new().set_directory(Path::new(&PATH_ROOT)).save_file() { + if let Err(err) = fs::write(&path, &self.tabs[self.selected_tab.to_index()].code) { + eprintln!("Error writing file: {}", err); + return None; + } + return Some(path); + } + return None + } + + + pub fn handle_save_file(&mut self, path_option : Option) { + if let Some(path) = path_option { + println!("File saved successfully at: {:?}", path); + self.tabs[self.selected_tab.to_index()].path = path; + self.tabs[self.selected_tab.to_index()].saved = true; + } else { + println!("File save failed."); + } + } + + + pub fn from_app_state(app_state: tools::AppState) -> Self { + let mut new = Self { + theme: DEFAULT_THEMES[min(app_state.theme, DEFAULT_THEMES.len() - 1)], + tabs: Vec::new(), + ..Default::default() + }; + + for path in app_state.tabs { + if path.file_name().expect("Could not get Tab Name").to_string_lossy().to_string() != "untitled" { + new.open_file(Some(&path)); + } + } + + if new.tabs == vec![] { + new.open_file(None); + } + + new + } + + + pub fn save_state(&self) { + let mut state_theme : usize = 0; + if let Some(theme) = DEFAULT_THEMES.iter().position(|&r| r == self.theme) { + state_theme = theme; + } + + let mut state_tabs = vec![]; + + for tab in &self.tabs { + state_tabs.push(tab.path.clone()); + } + let app_state = tools::AppState { + tabs: state_tabs, + theme: state_theme, + }; + + let _ = tools::save_state(&app_state, SAVE_PATH); + } + + + pub fn move_through_tabs(&mut self, forward : bool) { + let new_index = if forward { + (self.selected_tab.to_index() + 1) % self.tabs.len() + } else { + self.selected_tab.to_index().checked_sub(1).unwrap_or(self.tabs.len() - 1) + }; + self.selected_tab = tools::TabNumber::from_index(new_index); + } + + + pub fn list_files(&mut self, ui: &mut egui::Ui, path: &Path) -> io::Result<()> { + if let Some(name) = path.file_name() { + if path.is_dir() { + egui::CollapsingHeader::new(name.to_string_lossy()).show(ui, |ui| { + let mut paths: Vec<_> = fs::read_dir(&path).expect("Failed to read dir").map(|r| r.unwrap()).collect(); + + paths.sort_by(|a, b| tools::sort_directories_first(a, b)); + + for result in paths { + let _ = self.list_files(ui, &result.path()); + } + }); + } else { + if ui.button(name.to_string_lossy()).clicked() { + self.open_file(Some(path)); + } + } + } + Ok(()) + } + + + pub fn open_file(&mut self, path_option: Option<&Path>) { + if self.tabs.len() < MAX_TABS { + if let Some(path) = path_option { + self.tabs.push(tools::Tab::new(path.to_path_buf())); + } else { + self.tabs.push(tools::Tab::default()); + } + self.selected_tab = tools::TabNumber::from_index(self.tabs.len() - 1); + } + } + + + pub fn delete_tab(&mut self, index : usize) -> tools::TabNumber { + self.tabs.remove(index); + return tools::TabNumber::from_index(min(index, self.tabs.len() - 1)) + } + + + pub fn toggle(&self, ui: &mut egui::Ui, display : bool, title : &str) -> bool { + let text = if display.clone() { + format!("hide {}", title) + } else { + format!("show {}", title) + }; + + if ui.add(egui::Button::new(text)).clicked() { + return !display + } + return display + } +} \ No newline at end of file diff --git a/src/calcifer/code_editor/mod.rs b/src/calcifer/code_editor/mod.rs index 70845ae..0bfcea0 100644 --- a/src/calcifer/code_editor/mod.rs +++ b/src/calcifer/code_editor/mod.rs @@ -15,254 +15,254 @@ use std::ops::{Bound, RangeBounds}; trait StringUtils { - fn substring(&self, start: usize, len: usize) -> &str; - fn slice(&self, range: impl RangeBounds) -> &str; + fn substring(&self, start: usize, len: usize) -> &str; + fn slice(&self, range: impl RangeBounds) -> &str; fn char_at(&self, index: usize) -> char; } impl StringUtils for str { - fn substring(&self, start: usize, len: usize) -> &str { - let mut char_pos = 0; - let mut byte_start = 0; - let mut it = self.chars(); - loop { - if char_pos == start { break; } - if let Some(c) = it.next() { - char_pos += 1; - byte_start += c.len_utf8(); - } - else { break; } - } - char_pos = 0; - let mut byte_end = byte_start; - loop { - if char_pos == len { break; } - if let Some(c) = it.next() { - char_pos += 1; - byte_end += c.len_utf8(); - } - else { break; } - } - &self[byte_start..byte_end] - } + fn substring(&self, start: usize, len: usize) -> &str { + let mut char_pos = 0; + let mut byte_start = 0; + let mut it = self.chars(); + loop { + if char_pos == start { break; } + if let Some(c) = it.next() { + char_pos += 1; + byte_start += c.len_utf8(); + } + else { break; } + } + char_pos = 0; + let mut byte_end = byte_start; + loop { + if char_pos == len { break; } + if let Some(c) = it.next() { + char_pos += 1; + byte_end += c.len_utf8(); + } + else { break; } + } + &self[byte_start..byte_end] + } - fn slice(&self, range: impl RangeBounds) -> &str { - let start = match range.start_bound() { - Bound::Included(bound) | Bound::Excluded(bound) => *bound, - Bound::Unbounded => 0, - }; - let len = match range.end_bound() { - Bound::Included(bound) => *bound + 1, - Bound::Excluded(bound) => *bound, - Bound::Unbounded => self.len(), - } - start; - self.substring(start, len) - } + fn slice(&self, range: impl RangeBounds) -> &str { + let start = match range.start_bound() { + Bound::Included(bound) | Bound::Excluded(bound) => *bound, + Bound::Unbounded => 0, + }; + let len = match range.end_bound() { + Bound::Included(bound) => *bound + 1, + Bound::Excluded(bound) => *bound, + Bound::Unbounded => self.len(), + } - start; + self.substring(start, len) + } fn char_at(&self, index: usize) -> char { - self.chars().nth(index).unwrap_or('\0') // '\0' is used as the default value - } + self.chars().nth(index).unwrap_or('\0') // '\0' is used as the default value + } } #[derive(Clone, Debug, PartialEq)] /// CodeEditor struct which stores settings for highlighting. pub struct CodeEditor { - id: String, - theme: ColorTheme, - syntax: Syntax, - numlines: bool, - fontsize: f32, - rows: usize, - vscroll: bool, - stick_to_bottom: bool, - shrink: bool, + id: String, + theme: ColorTheme, + syntax: Syntax, + numlines: bool, + fontsize: f32, + rows: usize, + vscroll: bool, + stick_to_bottom: bool, + shrink: bool, } impl Hash for CodeEditor { - fn hash(&self, state: &mut H) { - self.theme.hash(state); - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - (self.fontsize as u32).hash(state); - self.syntax.hash(state); - } + fn hash(&self, state: &mut H) { + self.theme.hash(state); + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + (self.fontsize as u32).hash(state); + self.syntax.hash(state); + } } impl Default for CodeEditor { - fn default() -> CodeEditor { - CodeEditor { - id: String::from("Code Editor"), - theme: ColorTheme::GRUVBOX, - syntax: Syntax::rust(), - numlines: true, - fontsize: 10.0, - rows: 10, - vscroll: true, - stick_to_bottom: false, - shrink: false, - } - } + fn default() -> CodeEditor { + CodeEditor { + id: String::from("Code Editor"), + theme: ColorTheme::GRUVBOX, + syntax: Syntax::rust(), + numlines: true, + fontsize: 10.0, + rows: 10, + vscroll: true, + stick_to_bottom: false, + shrink: false, + } + } } impl CodeEditor { - pub fn id_source(self, id_source: impl Into) -> Self { - CodeEditor { - id: id_source.into(), - ..self - } - } + pub fn id_source(self, id_source: impl Into) -> Self { + CodeEditor { + id: id_source.into(), + ..self + } + } - /// Minimum number of rows to show. - /// - /// **Default: 10** - pub fn with_rows(self, rows: usize) -> Self { - CodeEditor { rows, ..self } - } + /// Minimum number of rows to show. + /// + /// **Default: 10** + pub fn with_rows(self, rows: usize) -> Self { + CodeEditor { rows, ..self } + } - /// Use custom Color Theme - /// - /// **Default: Gruvbox** - pub fn with_theme(self, theme: ColorTheme) -> Self { - CodeEditor { theme, ..self } - } + /// Use custom Color Theme + /// + /// **Default: Gruvbox** + pub fn with_theme(self, theme: ColorTheme) -> Self { + CodeEditor { theme, ..self } + } - /// Use custom font size - /// - /// **Default: 10.0** - pub fn with_fontsize(self, fontsize: f32) -> Self { - CodeEditor { fontsize, ..self } - } + /// Use custom font size + /// + /// **Default: 10.0** + pub fn with_fontsize(self, fontsize: f32) -> Self { + CodeEditor { fontsize, ..self } + } - #[cfg(feature = "egui")] - /// Use UI font size - pub fn with_ui_fontsize(self, ui: &mut egui::Ui) -> Self { - CodeEditor { - fontsize: egui::TextStyle::Monospace.resolve(ui.style()).size, - ..self - } - } + #[cfg(feature = "egui")] + /// Use UI font size + pub fn with_ui_fontsize(self, ui: &mut egui::Ui) -> Self { + CodeEditor { + fontsize: egui::TextStyle::Monospace.resolve(ui.style()).size, + ..self + } + } - /// Show or hide lines numbering - /// - /// **Default: true** - pub fn with_numlines(self, numlines: bool) -> Self { - CodeEditor { numlines, ..self } - } + /// Show or hide lines numbering + /// + /// **Default: true** + pub fn with_numlines(self, numlines: bool) -> Self { + CodeEditor { numlines, ..self } + } - /// Use custom syntax for highlighting - /// - /// **Default: Rust** - pub fn with_syntax(self, syntax: Syntax) -> Self { - CodeEditor { syntax, ..self } - } + /// Use custom syntax for highlighting + /// + /// **Default: Rust** + pub fn with_syntax(self, syntax: Syntax) -> Self { + CodeEditor { syntax, ..self } + } - /// Turn on/off scrolling on the vertical axis. - /// - /// **Default: true** - pub fn vscroll(self, vscroll: bool) -> Self { - CodeEditor { vscroll, ..self } - } - /// Should the containing area shrink if the content is small? - /// - /// **Default: false** - pub fn auto_shrink(self, shrink: bool) -> Self { - CodeEditor { shrink, ..self } - } + /// Turn on/off scrolling on the vertical axis. + /// + /// **Default: true** + pub fn vscroll(self, vscroll: bool) -> Self { + CodeEditor { vscroll, ..self } + } + /// Should the containing area shrink if the content is small? + /// + /// **Default: false** + pub fn auto_shrink(self, shrink: bool) -> Self { + CodeEditor { shrink, ..self } + } - /// Stick to bottom - /// The scroll handle will stick to the bottom position even while the content size - /// changes dynamically. This can be useful to simulate terminal UIs or log/info scrollers. - /// The scroll handle remains stuck until user manually changes position. Once "unstuck" - /// it will remain focused on whatever content viewport the user left it on. If the scroll - /// handle is dragged to the bottom it will again become stuck and remain there until manually - /// pulled from the end position. - /// - /// **Default: false** - pub fn stick_to_bottom(self, stick_to_bottom: bool) -> Self { - CodeEditor { - stick_to_bottom, - ..self - } - } + /// Stick to bottom + /// The scroll handle will stick to the bottom position even while the content size + /// changes dynamically. This can be useful to simulate terminal UIs or log/info scrollers. + /// The scroll handle remains stuck until user manually changes position. Once "unstuck" + /// it will remain focused on whatever content viewport the user left it on. If the scroll + /// handle is dragged to the bottom it will again become stuck and remain there until manually + /// pulled from the end position. + /// + /// **Default: false** + pub fn stick_to_bottom(self, stick_to_bottom: bool) -> Self { + CodeEditor { + stick_to_bottom, + ..self + } + } - pub fn format(&self, ty: TokenType) -> egui::text::TextFormat { - let font_id = egui::FontId::monospace(self.fontsize); - let color = self.theme.type_color(ty); - egui::text::TextFormat::simple(font_id, color) - } + pub fn format(&self, ty: TokenType) -> egui::text::TextFormat { + let font_id = egui::FontId::monospace(self.fontsize); + let color = self.theme.type_color(ty); + egui::text::TextFormat::simple(font_id, color) + } - fn numlines_show(&self, ui: &mut egui::Ui, text: &str) { - let total = if text.ends_with('\n') || text.is_empty() { - text.lines().count() + 1 - } else { - text.lines().count() - } - .max(self.rows); - let max_indent = total.to_string().len(); - let mut counter = (1..=total) - .map(|i| { - let label = i.to_string(); - format!( - "{}{label}", - " ".repeat(max_indent.saturating_sub(label.len())) - ) - }) - .collect::>() - .join("\n"); + fn numlines_show(&self, ui: &mut egui::Ui, text: &str) { + let total = if text.ends_with('\n') || text.is_empty() { + text.lines().count() + 1 + } else { + text.lines().count() + } + .max(self.rows); + let max_indent = total.to_string().len(); + let mut counter = (1..=total) + .map(|i| { + let label = i.to_string(); + format!( + "{}{label}", + " ".repeat(max_indent.saturating_sub(label.len())) + ) + }) + .collect::>() + .join("\n"); - #[allow(clippy::cast_precision_loss)] - let width = max_indent as f32 * self.fontsize * 0.5; + #[allow(clippy::cast_precision_loss)] + let width = max_indent as f32 * self.fontsize * 0.5; - let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| { - let layout_job = egui::text::LayoutJob::single_section( - string.to_string(), - egui::TextFormat::simple( - egui::FontId::monospace(self.fontsize), - self.theme.type_color(TokenType::Comment(true)), - ), - ); - ui.fonts(|f| f.layout_job(layout_job)) - }; + let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| { + let layout_job = egui::text::LayoutJob::single_section( + string.to_string(), + egui::TextFormat::simple( + egui::FontId::monospace(self.fontsize), + self.theme.type_color(TokenType::Comment(true)), + ), + ); + ui.fonts(|f| f.layout_job(layout_job)) + }; - ui.add( - egui::TextEdit::multiline(&mut counter) - .id_source(format!("{}_numlines", self.id)) - .font(egui::TextStyle::Monospace) - .interactive(false) - .frame(false) - .desired_rows(self.rows) - .desired_width(width) - .layouter(&mut layouter), - ); - } + ui.add( + egui::TextEdit::multiline(&mut counter) + .id_source(format!("{}_numlines", self.id)) + .font(egui::TextStyle::Monospace) + .interactive(false) + .frame(false) + .desired_rows(self.rows) + .desired_width(width) + .layouter(&mut layouter), + ); + } - /// Show Code Editor - pub fn show(&mut self, ui: &mut egui::Ui, text: &mut String, saved: &mut bool, last_cursor: &mut Option, vertical_offset: &mut f32, override_cursor: Option) { - //let mut text_edit_output: Option = None; - let mut code_editor = |ui: &mut egui::Ui| { - ui.horizontal_top(|h| { - self.theme.modify_style(h, self.fontsize); - if self.numlines { - self.numlines_show(h, text); - } - egui::ScrollArea::horizontal() - .id_source(format!("{}_inner_scroll", self.id)) - .show(h, |ui| { - let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| { - let layout_job = highlight(ui.ctx(), self, string); - ui.fonts(|f| f.layout_job(layout_job)) - }; + /// Show Code Editor + pub fn show(&mut self, ui: &mut egui::Ui, text: &mut String, saved: &mut bool, last_cursor: &mut Option, vertical_offset: &mut f32, override_cursor: Option) { + //let mut text_edit_output: Option = None; + let mut code_editor = |ui: &mut egui::Ui| { + ui.horizontal_top(|h| { + self.theme.modify_style(h, self.fontsize); + if self.numlines { + self.numlines_show(h, text); + } + egui::ScrollArea::horizontal() + .id_source(format!("{}_inner_scroll", self.id)) + .show(h, |ui| { + let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| { + let layout_job = highlight(ui.ctx(), self, string); + ui.fonts(|f| f.layout_job(layout_job)) + }; let previous_text = text.clone(); let mut output = egui::TextEdit::multiline(text) - .id_source(&self.id) - .lock_focus(true) - .desired_rows(self.rows) - .frame(true) - .desired_width(if self.shrink { 0.0 } else { f32::MAX }) - .layouter(&mut layouter) - .show(ui); + .id_source(&self.id) + .lock_focus(true) + .desired_rows(self.rows) + .frame(true) + .desired_width(if self.shrink { 0.0 } else { f32::MAX }) + .layouter(&mut layouter) + .show(ui); let mut get_new_cursor : bool = true; let mut extend : isize = 0; @@ -310,10 +310,10 @@ impl CodeEditor { let mut start = min(cursor_range.primary.index, cursor_range.secondary.index); let end = max(cursor_range.primary.index, cursor_range.secondary.index); let extended = match end as isize + extend { - // Check for overflow or negative result - value if value < 0 => 0, - value => value as usize, - }; + // Check for overflow or negative result + value if value < 0 => 0, + value => value as usize, + }; if start == end { start = extended; } @@ -331,22 +331,22 @@ impl CodeEditor { *saved = false; } //text_edit_output = Some(output); - }); - }); - }; - if self.vscroll { - let scroll_area = egui::ScrollArea::vertical() - .id_source(format!("{}_outer_scroll", self.id)) - .stick_to_bottom(self.stick_to_bottom) + }); + }); + }; + if self.vscroll { + let scroll_area = egui::ScrollArea::vertical() + .id_source(format!("{}_outer_scroll", self.id)) + .stick_to_bottom(self.stick_to_bottom) .vertical_scroll_offset(vertical_offset.clone()) - .show(ui, code_editor); + .show(ui, code_editor); *vertical_offset = scroll_area.state.offset.y.clone(); - } else { - code_editor(ui); - } + } else { + code_editor(ui); + } - //text_edit_output.expect("TextEditOutput should exist at this point") - } + //text_edit_output.expect("TextEditOutput should exist at this point") + } fn toggle_start_of_line(&self, cursor_range : CCursorRange, text : String, head : &str) -> (String, isize) { let mut substring = self.get_selection_substring(text.clone(), cursor_range.clone()).clone(); @@ -417,8 +417,12 @@ impl CodeEditor { fn new_line(&self, cursor_range : CCursorRange, text : String) -> (String, isize) { let cursor = min(cursor_range.primary.index, cursor_range.secondary.index); + + if cursor < 2 { + return (text.clone().to_string(), 1 as isize); + } - let mut last_line_break = max(0, cursor - 1); + let mut last_line_break = cursor - 1; while last_line_break > 0 && text.char_at(last_line_break) != '\n' { last_line_break -= 1; @@ -431,6 +435,6 @@ impl CodeEditor { new_text.push_str(&"\t".repeat(new_indent_depth.clone())); new_text.push_str(text.clone().slice((cursor + 1)..)); - (new_text.clone().to_string(), (new_indent_depth + 1) as isize) + return (new_text.clone().to_string(), (new_indent_depth + 1) as isize); } } diff --git a/src/calcifer/code_editor/themes/sonokai.rs b/src/calcifer/code_editor/themes/sonokai.rs index 646e904..911026e 100644 --- a/src/calcifer/code_editor/themes/sonokai.rs +++ b/src/calcifer/code_editor/themes/sonokai.rs @@ -1,22 +1,22 @@ use super::ColorTheme; impl ColorTheme { - /// Original Author: sainnhe - /// Modified by p4ymak - pub const SONOKAI: ColorTheme = ColorTheme { - name: "Sonokai", - dark: true, - bg: "#2c2e34", // bg0 - cursor: "#76cce0", // blue - selection: "#444852", // bg5 - comments: "#7f8490", // gray - functions: "#9ed072", // green - keywords: "#fc5d7c", // red - literals: "#e2e2e3", // foreground - numerics: "#b39df3", // purple - punctuation: "#7f8490", // gray - strs: "#e7c664", // yellow - types: "#399ee6", // blue - special: "#f39660", // orange - }; + /// Original Author: sainnhe + /// Modified by p4ymak + pub const SONOKAI: ColorTheme = ColorTheme { + name: "Sonokai", + dark: true, + bg: "#2c2e34", // bg0 + cursor: "#76cce0", // blue + selection: "#444852", // bg5 + comments: "#7f8490", // gray + functions: "#9ed072", // green + keywords: "#fc5d7c", // red + literals: "#e2e2e3", // foreground + numerics: "#b39df3", // purple + punctuation: "#7f8490", // gray + strs: "#e7c664", // yellow + types: "#399ee6", // blue + special: "#f39660", // orange + }; } diff --git a/src/calcifer_base.rs b/src/calcifer_base.rs new file mode 100644 index 0000000..2f021e6 --- /dev/null +++ b/src/calcifer_base.rs @@ -0,0 +1,137 @@ +// Hello there, Master + +impl super::Calcifer { + pub fn save_tab(&self) -> Option { + if self.tabs[self.selected_tab.to_index()].path.file_name().expect("Could not get Tab Name").to_string_lossy().to_string() == "untitled" { + return self.save_tab_as(); + } else { + if let Err(err) = fs::write(&self.tabs[self.selected_tab.to_index()].path, &self.tabs[self.selected_tab.to_index()].code) { + eprintln!("Error writing file: {}", err); + return None; + } + return Some(self.tabs[self.selected_tab.to_index()].path.clone()) + } + } + + pub fn save_tab_as(&self) -> Option { + if let Some(path) = rfd::FileDialog::new().set_directory(Path::new(&PATH_ROOT)).save_file() { + if let Err(err) = fs::write(&path, &self.tabs[self.selected_tab.to_index()].code) { + eprintln!("Error writing file: {}", err); + return None; + } + return Some(path); + } + return None + } + + pub fn handle_save_file(&mut self, path_option : Option) { + if let Some(path) = path_option { + println!("File saved successfully at: {:?}", path); + self.tabs[self.selected_tab.to_index()].path = path; + self.tabs[self.selected_tab.to_index()].saved = true; + } else { + println!("File save failed."); + } + } + + pub fn from_app_state(app_state: tools::AppState) -> Self { + let mut new = Self { + theme: DEFAULT_THEMES[min(app_state.theme, DEFAULT_THEMES.len() - 1)], + tabs: Vec::new(), + ..Default::default() + }; + + for path in app_state.tabs { + if path.file_name().expect("Could not get Tab Name").to_string_lossy().to_string() != "untitled" { + new.open_file(Some(&path)); + } + } + + if new.tabs == vec![] { + new.open_file(None); + } + + new + } + + pub fn save_state(&self) { + let mut state_theme : usize = 0; + if let Some(theme) = DEFAULT_THEMES.iter().position(|&r| r == self.theme) { + state_theme = theme; + } + + let mut state_tabs = vec![]; + + for tab in &self.tabs { + state_tabs.push(tab.path.clone()); + } + let app_state = tools::AppState { + tabs: state_tabs, + theme: state_theme, + }; + + let _ = tools::save_state(&app_state, super::SAVE_PATH); + } + + pub fn move_through_tabs(&mut self, forward : bool) { + let new_index = if forward { + (self.selected_tab.to_index() + 1) % self.tabs.len() + } else { + self.selected_tab.to_index().checked_sub(1).unwrap_or(self.tabs.len() - 1) + }; + self.selected_tab = tools::TabNumber::from_index(new_index); + } + + fn list_files(&mut self, ui: &mut egui::Ui, path: &Path) -> io::Result<()> { + if let Some(name) = path.file_name() { + if path.is_dir() { + egui::CollapsingHeader::new(name.to_string_lossy()).show(ui, |ui| { + let mut paths: Vec<_> = fs::read_dir(&path).expect("Failed to read dir").map(|r| r.unwrap()).collect(); + + // Sort the vector using the custom sorting function + paths.sort_by(|a, b| tools::sort_directories_first(a, b)); + + for result in paths { + //let result = path_result.expect("Failed to get path"); + //let full_path = result.path(); + let _ = self.list_files(ui, &result.path()); + } + }); + } else { + //ui.label(name.to_string_lossy()); + if ui.button(name.to_string_lossy()).clicked() { + self.open_file(Some(path)); + } + } + } + Ok(()) + } + + fn open_file(&mut self, path_option: Option<&Path>) { + if self.tabs.len() < MAX_TABS { + if let Some(path) = path_option { + self.tabs.push(tools::Tab::new(path.to_path_buf())); + } else { + self.tabs.push(tools::Tab::default()); + } + self.selected_tab = tools::TabNumber::from_index(self.tabs.len() - 1); + } + } + + fn delete_tab(&mut self, index : usize) -> tools::TabNumber { + self.tabs.remove(index); + return tools::TabNumber::from_index(min(index, self.tabs.len() - 1)) + } + + fn toggle(&self, ui: &mut egui::Ui, display : bool, title : &str) -> bool { + let text = if display.clone() { + format!("hide {}", title) + } else { + format!("show {}", title) + }; + + if ui.add(egui::Button::new(text)).clicked() { + return !display + } + return display + } \ No newline at end of file diff --git a/src/calcifer_save.json b/src/calcifer_save.json index 19fa8a3..1336658 100644 --- a/src/calcifer_save.json +++ b/src/calcifer_save.json @@ -1 +1 @@ -{"tabs":["/home/penwing/Documents/notes/victory2.txt","/home/penwing/Documents/projects/rust/calcifer/src/calcifer/code_editor/themes/sonokai.rs"],"theme":4} \ No newline at end of file +{"tabs":["/home/penwing/Documents/notes/victory2.txt","/home/penwing/Documents/projects/rust/calcifer/src/calcifer/code_editor/themes/sonokai.rs"],"theme":6} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index f4a2ec8..9595233 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,11 +15,10 @@ const TIME_LABELS : [&str; 5] = ["settings", "tree", "terminal", "tabs", "conten const MAX_FPS : f32 = 30.0; const PATH_ROOT : &str = "/home/penwing/Documents/"; const DISPLAY_PATH_DEPTH : usize = 3; +const MAX_TABS : usize = 20; fn main() -> Result<(), eframe::Error> { - tools::loaded(); - let icon_data = tools::load_icon(); env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). @@ -73,7 +72,7 @@ struct Calcifer { impl Default for Calcifer { fn default() -> Self { Self { - selected_tab: tools::TabNumber::Zero, + selected_tab: tools::TabNumber::from_index(0), tabs: vec![tools::Tab::default()], command: String::new(), @@ -103,7 +102,7 @@ impl eframe::App for Calcifer { let mut watch = time::Instant::now(); if ctx.input( |i| i.key_pressed(egui::Key::T) && i.modifiers.ctrl) { - self.indent_with_tabs(); + self.tabs[self.selected_tab.to_index()].refresh(); } if ctx.input( |i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) { diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 12bb8d1..81fafad 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -1,13 +1,19 @@ -use std::{env, process::Command, cmp::Ordering, path::PathBuf, path::Path, fs::read_to_string, fs::write, path::Component, ffi::OsStr}; +use std::{cmp::Ordering, path::PathBuf, path::Path, fs::read_to_string, fs::write, path::Component, ffi::OsStr}; use crate::calcifer::code_editor::Syntax; use eframe::egui; -use egui::text_edit::CCursorRange; use serde::{Serialize, Deserialize}; use crate::DISPLAY_PATH_DEPTH; -//pub mod themes; +//my tools; pub mod search; +pub mod terminal; +pub use terminal::*; + +pub mod tabs; +pub use tabs::*; + + pub trait View { fn ui(&mut self, ui: &mut egui::Ui, tabs: &mut Vec, selected_tab: &mut TabNumber); @@ -15,124 +21,12 @@ pub trait View { /// Something to view pub trait Demo { - /// Is the demo enabled for this integraton? - fn is_enabled(&self, _ctx: &egui::Context) -> bool { - true - } - - /// `&'static` so we can also use it as a key to store open/close state. fn name(&self) -> &str; //'static - /// Show windows, etc fn show(&mut self, ctx: &egui::Context, open: &mut bool, tabs: &mut Vec, selected_tab: &mut TabNumber); } -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum TabNumber { - None, - Open, - Zero, - One, - Two, - Three, - Four, - Five, - Six, - Seven, -} - - -impl TabNumber { - pub fn from_index(n : usize) -> TabNumber { - match n { - 0 => TabNumber::Zero, - 1 => TabNumber::One, - 2 => TabNumber::Two, - 3 => TabNumber::Three, - 4 => TabNumber::Four, - 5 => TabNumber::Five, - 6 => TabNumber::Six, - 7 => TabNumber::Seven, - _ => TabNumber::None, - } - } - pub fn to_index(&self) -> usize { - match self { - TabNumber::Zero => 0, - TabNumber::One => 1, - TabNumber::Two => 2, - TabNumber::Three => 3, - TabNumber::Four => 4, - TabNumber::Five => 5, - TabNumber::Six => 6, - TabNumber::Seven => 7, - _ => 0, - } - } -} - -#[derive(Clone, PartialEq)] -pub struct Tab { - pub path : PathBuf, - pub code : String, - pub language : String, - pub saved : bool, - pub scroll_offset : f32, - pub last_cursor : Option, -} - -impl Default for Tab { - fn default() -> Self { - Self { - path: "untitled".into(), - code: "// Hello there, Master".into(), - language: "rs".into(), - saved: false, - scroll_offset: 0.0, - last_cursor: None, - } - } -} - - -impl Tab { - pub fn get_name(&self) -> String { - self.path.file_name().expect("Could not get Tab Name").to_string_lossy().to_string() - } -} - -#[derive(Clone, PartialEq)] -pub struct CommandEntry { - pub env: String, - pub command: String, - pub output: String, - pub error: String, -} - -impl CommandEntry { - pub fn new(command: String) -> Self { - CommandEntry { - env: format_path(&env::current_dir().expect("Could not find Shell Environnment")), - command, - output: String::new(), - error: String::new(), - } - } - - pub fn run(&mut self) -> Self { - let output = Command::new("sh") - .arg("-c") - .arg(self.command.clone()) - .output() - .expect("failed to execute process"); - self.output = (&String::from_utf8_lossy(&output.stdout)).trim_end_matches('\n').to_string(); - self.error = (&String::from_utf8_lossy(&output.stderr)).trim_end_matches('\n').to_string(); - - self.clone() - } -} - #[derive(Serialize, Deserialize, Debug, PartialEq)] @@ -158,10 +52,6 @@ pub fn load_state(file_path: &str) -> Result { } -pub fn loaded() { - println!("Tools loaded"); -} - pub fn load_icon() -> egui::IconData { let (icon_rgba, icon_width, icon_height) = { let icon = include_bytes!("../../assets/icon.png"); @@ -190,40 +80,6 @@ pub fn to_syntax(language : &str) -> Syntax { } -pub fn run_command(command: String) -> CommandEntry { - let mut entry = CommandEntry::new(command); - - if entry.command.len() < 2 { - return entry.run(); - } - - if &entry.command[..2] != "cd" { - return entry.run() - } - - if entry.command.len() < 4 { - entry.error = "Invalid cd, should provide path".to_string(); - return entry - } - - let path_append = entry.command[3..].replace("~", "/home/penwing"); - let path = Path::new(&path_append); - - if format!("{}", path.display()) == "/" { - entry.error = "Root access denied".to_string(); - return entry - } - - if env::set_current_dir(path).is_ok() { - entry.output = format!("Moved to : {}", path.display()); - } else { - entry.error = format!("Could not find path : {}", path.display()); - } - - return entry -} - - pub fn sort_directories_first(a: &std::fs::DirEntry, b: &std::fs::DirEntry) -> Ordering { let a_is_dir = a.path().is_dir(); let b_is_dir = b.path().is_dir(); diff --git a/src/tools/search.rs b/src/tools/search.rs index 1b56ea6..508dec3 100644 --- a/src/tools/search.rs +++ b/src/tools/search.rs @@ -1,5 +1,5 @@ use eframe::egui; -use crate::tools::{View, Demo, Tab, TabNumber}; +use crate::tools::{View, Demo, tabs::Tab, tabs::TabNumber}; use std::{cmp::min}; use crate::RED; @@ -24,7 +24,7 @@ pub struct Selection { impl Default for Selection { fn default() -> Self { Self { - tab: TabNumber::Zero, + tab: TabNumber::from_index(0), start: 0, end: 0, } diff --git a/src/tools/tabs.rs b/src/tools/tabs.rs new file mode 100644 index 0000000..3cf3d3c --- /dev/null +++ b/src/tools/tabs.rs @@ -0,0 +1,73 @@ +use std::{fs::read_to_string, path::PathBuf}; +use eframe::egui::text_edit::CCursorRange; + +use crate::MAX_TABS; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum TabNumber { + Open, + Number(u8), // Using a range for numeric values +} + +impl TabNumber { + pub fn from_index(n: usize) -> TabNumber { + match n { + 0..=MAX_TABS => TabNumber::Number(n as u8), + _ => TabNumber::Number(0), + } + } + + pub fn to_index(&self) -> usize { + match self { + TabNumber::Number(n) => *n as usize, + _ => 0, + } + } +} + + +#[derive(Clone, PartialEq)] +pub struct Tab { + pub path : PathBuf, + pub code : String, + pub language : String, + pub saved : bool, + pub scroll_offset : f32, + pub last_cursor : Option, +} + +impl Default for Tab { + fn default() -> Self { + Self { + path: "untitled".into(), + code: "// Hello there, Master".into(), + language: "rs".into(), + saved: false, + scroll_offset: 0.0, + last_cursor: None, + } + } +} + + +impl Tab { + pub fn new(path : PathBuf) -> Self { + Self { + path: path.clone().into(), + code: read_to_string(path.clone()).expect("Not able to read the file").replace(&" ".repeat(4), "\t"), + language: path.to_str().unwrap().split('.').last().unwrap().into(), + saved: true, + scroll_offset: 0.0, + last_cursor: None, + } + + } + pub fn get_name(&self) -> String { + self.path.file_name().expect("Could not get Tab Name").to_string_lossy().to_string() + } + + pub fn refresh(&mut self) { + self.code = read_to_string(self.path.clone()).expect("Not able to read the file").replace(&" ".repeat(4), "\t"); + println!("refreshed {}", self.path.display()); + } +} \ No newline at end of file diff --git a/src/tools/terminal.rs b/src/tools/terminal.rs new file mode 100644 index 0000000..a63cca2 --- /dev/null +++ b/src/tools/terminal.rs @@ -0,0 +1,68 @@ +use crate::tools::format_path; +use std::{process::Command, env, path::Path}; + +#[derive(Clone, PartialEq)] +pub struct CommandEntry { + pub env: String, + pub command: String, + pub output: String, + pub error: String, +} + + +impl CommandEntry { + pub fn new(command: String) -> Self { + CommandEntry { + env: format_path(&env::current_dir().expect("Could not find Shell Environnment")), + command, + output: String::new(), + error: String::new(), + } + } + + pub fn run(&mut self) -> Self { + let output = Command::new("sh") + .arg("-c") + .arg(self.command.clone()) + .output() + .expect("failed to execute process"); + self.output = (&String::from_utf8_lossy(&output.stdout)).trim_end_matches('\n').to_string(); + self.error = (&String::from_utf8_lossy(&output.stderr)).trim_end_matches('\n').to_string(); + + self.clone() + } +} + + +pub fn run_command(command: String) -> CommandEntry { + let mut entry = CommandEntry::new(command); + + if entry.command.len() < 2 { + return entry.run(); + } + + if &entry.command[..2] != "cd" { + return entry.run() + } + + if entry.command.len() < 4 { + entry.error = "Invalid cd, should provide path".to_string(); + return entry + } + + let path_append = entry.command[3..].replace("~", "/home/penwing"); + let path = Path::new(&path_append); + + if format!("{}", path.display()) == "/" { + entry.error = "Root access denied".to_string(); + return entry + } + + if env::set_current_dir(path).is_ok() { + entry.output = format!("Moved to : {}", path.display()); + } else { + entry.error = format!("Could not find path : {}", path.display()); + } + + return entry +} \ No newline at end of file