From df18044c645ce5bd9601d1b3e8d877f381b47143 Mon Sep 17 00:00:00 2001 From: Penwing Date: Tue, 30 Jan 2024 11:38:17 +0100 Subject: [PATCH] better file structure and clean up --- src/calcifer.rs | 314 ---------------- src/calcifer/app_base.rs | 218 ----------- src/core/app.rs | 223 ++++++++++++ src/core/mod.rs | 9 + src/core/state.rs | 58 +++ src/core/ui.rs | 338 ++++++++++++++++++ .../code_editor => editor}/highlighting.rs | 0 src/{calcifer/code_editor => editor}/mod.rs | 0 .../code_editor => editor}/syntax/asm.rs | 0 .../code_editor => editor}/syntax/lua.rs | 0 .../code_editor => editor}/syntax/mod.rs | 0 .../code_editor => editor}/syntax/python.rs | 0 .../code_editor => editor}/syntax/rust.rs | 0 .../code_editor => editor}/syntax/shell.rs | 0 .../code_editor => editor}/syntax/sql.rs | 0 src/{calcifer/code_editor => editor}/tests.rs | 0 .../code_editor => editor}/themes/ayu.rs | 0 .../code_editor => editor}/themes/fantasy.rs | 0 .../code_editor => editor}/themes/github.rs | 0 .../code_editor => editor}/themes/gruvbox.rs | 0 .../code_editor => editor}/themes/mod.rs | 0 .../code_editor => editor}/themes/sonokai.rs | 0 src/main.rs | 67 ++-- src/panels/file_tree.rs | 114 ++++++ src/panels/mod.rs | 9 + src/{tools => panels}/tabs.rs | 0 src/panels/terminal/linux_terminal.rs | 183 ++++++++++ src/panels/terminal/mod.rs | 10 + .../terminal}/windows_terminal.rs | 0 src/{tools => sub_windows}/confirm.rs | 0 src/sub_windows/mod.rs | 12 + src/{tools => sub_windows}/search.rs | 2 +- src/{tools => sub_windows}/settings.rs | 3 +- src/{tools => sub_windows}/shortcuts.rs | 0 src/tools/file_tree.rs | 100 ------ src/tools/mod.rs | 130 ------- src/tools/terminal.rs | 184 ---------- src/tools/tests.rs | 84 ----- 38 files changed, 992 insertions(+), 1066 deletions(-) delete mode 100644 src/calcifer.rs delete mode 100644 src/calcifer/app_base.rs create mode 100644 src/core/app.rs create mode 100644 src/core/mod.rs create mode 100644 src/core/state.rs create mode 100644 src/core/ui.rs rename src/{calcifer/code_editor => editor}/highlighting.rs (100%) rename src/{calcifer/code_editor => editor}/mod.rs (100%) rename src/{calcifer/code_editor => editor}/syntax/asm.rs (100%) rename src/{calcifer/code_editor => editor}/syntax/lua.rs (100%) rename src/{calcifer/code_editor => editor}/syntax/mod.rs (100%) rename src/{calcifer/code_editor => editor}/syntax/python.rs (100%) rename src/{calcifer/code_editor => editor}/syntax/rust.rs (100%) rename src/{calcifer/code_editor => editor}/syntax/shell.rs (100%) rename src/{calcifer/code_editor => editor}/syntax/sql.rs (100%) rename src/{calcifer/code_editor => editor}/tests.rs (100%) rename src/{calcifer/code_editor => editor}/themes/ayu.rs (100%) rename src/{calcifer/code_editor => editor}/themes/fantasy.rs (100%) rename src/{calcifer/code_editor => editor}/themes/github.rs (100%) rename src/{calcifer/code_editor => editor}/themes/gruvbox.rs (100%) rename src/{calcifer/code_editor => editor}/themes/mod.rs (100%) rename src/{calcifer/code_editor => editor}/themes/sonokai.rs (100%) create mode 100644 src/panels/file_tree.rs create mode 100644 src/panels/mod.rs rename src/{tools => panels}/tabs.rs (100%) create mode 100644 src/panels/terminal/linux_terminal.rs create mode 100644 src/panels/terminal/mod.rs rename src/{tools => panels/terminal}/windows_terminal.rs (100%) rename src/{tools => sub_windows}/confirm.rs (100%) create mode 100644 src/sub_windows/mod.rs rename src/{tools => sub_windows}/search.rs (99%) rename src/{tools => sub_windows}/settings.rs (96%) rename src/{tools => sub_windows}/shortcuts.rs (100%) delete mode 100644 src/tools/file_tree.rs delete mode 100644 src/tools/mod.rs delete mode 100644 src/tools/terminal.rs delete mode 100644 src/tools/tests.rs diff --git a/src/calcifer.rs b/src/calcifer.rs deleted file mode 100644 index 7ef4c5f..0000000 --- a/src/calcifer.rs +++ /dev/null @@ -1,314 +0,0 @@ -use eframe::egui; -use egui::{text::CCursor, text_edit::CCursorRange, Rangef}; -use std::{cmp::max, env, path::PathBuf}; // path::Path, - -use crate::tools; -use crate::Calcifer; -use crate::MAX_TABS; -use crate::PROJECT_EXTENSION; -use tools::hex_str_to_color; - -pub mod code_editor; -use code_editor::CodeEditor; - -mod app_base; - -impl Calcifer { - pub fn draw_settings(&mut self, ctx: &egui::Context) { - egui::SidePanel::left("settings") - .resizable(false) - .exact_width(self.font_size * 1.8) - .show(ctx, |ui| { - ui.vertical(|ui| { - if ui.add(egui::Button::new("📁")).clicked() { - if let Some(path) = rfd::FileDialog::new() - .set_directory(self.home.as_path()) - .pick_file() - { - self.open_file(Some(&path)); - } - } - ui.separator(); - self.tree_visible = self.toggle(ui, self.tree_visible, "📦"); - ui.separator(); - self.terminal_visible = self.toggle(ui, self.terminal_visible, "🖵"); - ui.separator(); - self.search_menu.visible = self.toggle(ui, self.search_menu.visible, "🔍"); - ui.separator(); - self.settings_menu.visible = self.toggle(ui, self.settings_menu.visible, "⚙"); - ui.separator(); - self.shortcuts_menu.visible = self.toggle(ui, self.shortcuts_menu.visible, "⌨"); - ui.separator(); - self.profiler_visible = self.toggle(ui, self.profiler_visible, "⚡"); - - if self.tabs[self.selected_tab.to_index()].language == PROJECT_EXTENSION { - ui.separator(); - self.project_mode = self.toggle(ui, self.project_mode, "✒"); - } - }); - }); - } - - pub fn draw_tree_panel(&mut self, ctx: &egui::Context) { - if !self.tree_visible { - return; - } - egui::SidePanel::left("file_tree_panel").show(ctx, |ui| { - ui.horizontal(|ui| { - ui.label("Bookshelf "); - if ui.add(egui::Button::new("📖")).clicked() { - self.file_tree = tools::file_tree::generate_file_tree(self.home.as_path(), 7); - } - }); - ui.separator(); - let mut n_files: usize = 0; - if let Some(file_tree) = self.file_tree.clone() { - self.list_files(ui, &file_tree, 1, &mut n_files); - } else { - ui.label("No book on the Bookshelf"); - } - ui.separator(); - ui.label(format!("{} files displayed", n_files)); - }); - } - - pub fn draw_bottom_tray(&mut self, ctx: &egui::Context) { - egui::TopBottomPanel::bottom("tray") - .default_height(self.font_size * 1.2) - .resizable(false) - .show(ctx, |ui| { - ui.label(self.profiler()); - }); - } - - pub fn draw_terminal_panel(&mut self, ctx: &egui::Context) { - if !self.terminal_visible { - return; - } - egui::TopBottomPanel::bottom("terminal") - .default_height(super::TERMINAL_HEIGHT) - .height_range(Rangef::new( - super::TERMINAL_RANGE.start, - super::TERMINAL_RANGE.end, - )) - .resizable(true) - .show(ctx, |ui| { - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { - let command_color = hex_str_to_color(self.theme.functions); - let entry_color = hex_str_to_color(self.theme.literals); - let bg_color = hex_str_to_color(self.theme.bg); - - ui.label(""); - - ui.horizontal(|ui| { - if ui.add(egui::Button::new("⟳")).clicked() { - self.command_history.retain(|e| !e.finished); - } - ui.style_mut().visuals.extreme_bg_color = bg_color; - let Self { command, .. } = self; - ui.colored_label( - command_color, - tools::format_path( - &env::current_dir().unwrap_or_else(|_| PathBuf::from("/")), - ), - ); - let response = ui.add( - egui::TextEdit::singleline(command) - .desired_width(f32::INFINITY) - .lock_focus(true), - ); - - if response.lost_focus() && ctx.input(|i| i.key_pressed(egui::Key::Enter)) { - self.command_history - .push(tools::send_command(self.command.clone())); - self.command = "".into(); - response.request_focus(); - } - }); - ui.separator(); - egui::ScrollArea::vertical() - .stick_to_bottom(true) - .show(ui, |ui| { - ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { - ui.separator(); - ui.spacing_mut().item_spacing.y = 0.0; - ui.style_mut().visuals.hyperlink_color = hex_str_to_color(self.theme.keywords); - - for entry in &mut self.command_history { - ui.label(""); - ui.horizontal(|ui| { - if !entry.finished { - entry.update(); - let _ = ui.link("(⌛)"); - } else { - if ui.link("(🗐)").clicked() { - entry.copy_error_code(); - } - } - ui.colored_label( - command_color, - format!("{} {}", entry.env, entry.command), - ); - }); - //ui.end_row(); - for line in &entry.result { - let color = - if line.error { super::RED } else { entry_color }; - ui.colored_label(color, &line.text); - //ui.end_row(); - } - } - }); - }); - }); - }); - } - - pub fn draw_tab_panel(&mut self, ctx: &egui::Context) { - egui::TopBottomPanel::top("tabs") - .resizable(false) - .show(ctx, |ui| { - ui.horizontal(|ui| { - ui.style_mut().visuals.selection.bg_fill = - hex_str_to_color(self.theme.functions); - ui.style_mut().visuals.hyperlink_color = hex_str_to_color(self.theme.functions); - for (index, tab) in self.tabs.clone().iter().enumerate() { - let mut title = tab.get_name(); - if !tab.saved { - title += " ~"; - } - if self.selected_tab == tools::TabNumber::from_index(index) { - ui.style_mut().visuals.override_text_color = - Some(hex_str_to_color(self.theme.bg)); - } - ui.selectable_value( - &mut self.selected_tab, - tools::TabNumber::from_index(index), - title, - ); - - ui.style_mut().visuals.override_text_color = None; - - if ui.link("X").clicked() && !self.close_tab_confirm.visible { - if self.tabs.len() > 1 { - if tab.saved { - self.delete_tab(index); - } else { - self.close_tab_confirm.ask(); - self.tab_to_close = index; - } - } else { - egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::Close); - } - } - ui.separator(); - } - if self.tabs.len() < MAX_TABS { - ui.selectable_value(&mut self.selected_tab, tools::TabNumber::Open, "+"); - } - if self.selected_tab == tools::TabNumber::Open { - self.open_file(None); - } - }); - }); - } - - pub fn draw_content_panel(&mut self, ctx: &egui::Context) { - egui::CentralPanel::default().show(ctx, |ui| { - ui.horizontal(|ui| { - if ui - .add(egui::Button::new("open directory in terminal")) - .clicked() - { - let mut path = self.tabs[self.selected_tab.to_index()].path.clone(); - path.pop(); - tools::send_command(format!("cd {}", path.display())); - } - - ui.label("Picked file:"); - ui.monospace( - self.tabs[self.selected_tab.to_index()] - .path - .to_string_lossy() - .to_string(), - ); - }); - - ui.separator(); - if self.project_mode - && self.tabs[self.selected_tab.to_index()].language == PROJECT_EXTENSION - { - self.draw_project_file(ui); - } else { - 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; - let mut override_cursor: Option = None; - - if !self.search_menu.result_selected { - override_cursor = Some(CCursorRange::two( - CCursor::new(self.search_menu.get_cursor_start()), - CCursor::new(self.search_menu.get_cursor_end()), - )); - self.search_menu.result_selected = true; - } - - CodeEditor::default() - .id_source("code editor") - .with_rows(max(45, lines)) - .with_fontsize(self.font_size) - .with_theme(self.theme) - .with_syntax(tools::to_syntax(¤t_tab.language)) - .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, - ); - } - - fn draw_project_file(&mut self, ui: &mut egui::Ui) { - ui.label("project mode"); - } - - pub fn draw_windows(&mut self, ctx: &egui::Context) { - if self.search_menu.visible { - self.search_menu - .show(ctx, &mut self.tabs, &mut self.selected_tab); - } - if self.close_tab_confirm.visible { - self.close_tab_confirm.show(ctx); - } - if self.refresh_confirm.visible { - self.refresh_confirm.show(ctx); - } - if self.exit_confirm.visible { - self.exit_confirm.show(ctx); - } - if self.exit_confirm.proceed { - for tab in self.tabs.iter_mut() { - tab.saved = true; - } - egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::Close); - } - if self.shortcuts_menu.visible { - self.shortcuts_menu.show(ctx); - } - if self.settings_menu.visible { - self.settings_menu.show(ctx); - } - if self.settings_menu.updated { - self.theme = self.settings_menu.theme; - } - - self.handle_confirm(); - } -} diff --git a/src/calcifer/app_base.rs b/src/calcifer/app_base.rs deleted file mode 100644 index e3bbcfc..0000000 --- a/src/calcifer/app_base.rs +++ /dev/null @@ -1,218 +0,0 @@ -use eframe::egui; -use egui::Color32; -use std::{cmp::min, fs, path::Path, path::PathBuf}; - -use crate::save_path; -use crate::tools; -use crate::Calcifer; -use crate::DEFAULT_THEMES; -use crate::MAX_TABS; -use crate::TIME_LABELS; -use tools::file_tree; -use tools::hex_str_to_color; - -impl Calcifer { - pub fn handle_confirm(&mut self) { - if self.close_tab_confirm.proceed { - self.close_tab_confirm.close(); - self.delete_tab(self.tab_to_close); - } - - if self.refresh_confirm.proceed { - self.refresh_confirm.close(); - self.tabs[self.selected_tab.to_index()].refresh(); - } - } - - pub fn save_tab(&self) -> Option { - if self.tabs[self.selected_tab.to_index()] - .path - .file_name() - .map_or(true, |name| name.to_string_lossy() == "untitled") - { - 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; - } - 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(self.home.as_path()) - .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); - } - 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(), - settings_menu: tools::settings::SettingsWindow::new(DEFAULT_THEMES[app_state.theme]), - ..Default::default() - }; - - for path in app_state.tabs { - if !path - .file_name() - .map_or(true, |name| name.to_string_lossy() == "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().as_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 open_file(&mut self, path_option: Option<&Path>) { - if let Some(path) = path_option { - for (index, tab) in self.tabs.clone().iter().enumerate() { - if tab.path == path { - self.selected_tab = tools::TabNumber::from_index(index); - return; - } - } - } - 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) { - self.tabs.remove(index); - self.selected_tab = tools::TabNumber::from_index(min(index, self.tabs.len() - 1)); - } - - pub fn toggle(&self, ui: &mut egui::Ui, display: bool, title: &str) -> bool { - let bg_color: Color32; - let text_color: Color32; - - if display { - bg_color = hex_str_to_color(self.theme.functions); - text_color = hex_str_to_color(self.theme.bg); - } else { - bg_color = hex_str_to_color(self.theme.bg); - text_color = hex_str_to_color(self.theme.literals); - }; - - ui.style_mut().visuals.override_text_color = Some(text_color); - - if ui.add(egui::Button::new(title).fill(bg_color)).clicked() { - return !display; - } - ui.style_mut().visuals.override_text_color = None; - - display - } - - pub fn profiler(&self) -> String { - if !self.profiler_visible { - return "".to_string(); - } - let combined_string: Vec = TIME_LABELS - .into_iter() - .zip(self.time_watch.clone()) - .map(|(s, v)| format!("{} : {:.1} ms", s, v)) - .collect(); - - let mut result = combined_string.join(" ; "); - result.push_str(&format!( - " total : {:.1} ms", - self.time_watch.clone().iter().sum::() - )); - result - } - - pub fn list_files( - &mut self, - ui: &mut egui::Ui, - file: &file_tree::File, - depth: isize, - n_files: &mut usize, - ) { - *n_files += 1; - - if let Some(folder_content) = &file.folder_content { - let collapsing_response = egui::CollapsingHeader::new(file.name.clone()) - .default_open(depth > 0) - .show(ui, |ui| { - if !self.tree_dir_opened.contains(&file.name) { - return; - } - for deeper_file in folder_content { - self.list_files(ui, deeper_file, depth - 1, n_files); - } - }); - if collapsing_response.fully_closed() { - self.tree_dir_opened.retain(|s| s != &file.name); - } else if !self.tree_dir_opened.contains(&file.name) { - self.tree_dir_opened.push(file.name.clone()); - } - } else if ui.button(&file.name).clicked() { - self.open_file(Some(&file.path)); - } - } -} diff --git a/src/core/app.rs b/src/core/app.rs new file mode 100644 index 0000000..4f706a0 --- /dev/null +++ b/src/core/app.rs @@ -0,0 +1,223 @@ +use eframe::egui; +use egui::Color32; +use std::{cmp::min, fs, path::Path, path::PathBuf}; + +use crate::core; +use crate::editor::themes::DEFAULT_THEMES; +use crate::panels; +use crate::save_path; +use crate::sub_windows; +use crate::Calcifer; +use crate::MAX_TABS; +use crate::TIME_LABELS; + +impl Calcifer { + pub fn handle_confirm(&mut self) { + if self.close_tab_confirm.proceed { + self.close_tab_confirm.close(); + self.delete_tab(self.tab_to_close); + } + + if self.refresh_confirm.proceed { + self.refresh_confirm.close(); + self.tabs[self.selected_tab.to_index()].refresh(); + } + } + + pub fn save_tab(&self) -> Option { + if self.tabs[self.selected_tab.to_index()] + .path + .file_name() + .map_or(true, |name| name.to_string_lossy() == "untitled") + { + 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; + } + 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(self.home.as_path()) + .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); + } + 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: core::AppState) -> Self { + let mut new = Self { + theme: DEFAULT_THEMES[min(app_state.theme, DEFAULT_THEMES.len() - 1)], + tabs: Vec::new(), + settings_menu: sub_windows::SettingsWindow::new(DEFAULT_THEMES[app_state.theme]), + ..Default::default() + }; + + for path in app_state.tabs { + if !path + .file_name() + .map_or(true, |name| name.to_string_lossy() == "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 = core::AppState { + tabs: state_tabs, + theme: state_theme, + }; + + let _ = core::save_state(&app_state, save_path().as_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 = panels::TabNumber::from_index(new_index); + } + + pub fn open_file(&mut self, path_option: Option<&Path>) { + if let Some(path) = path_option { + for (index, tab) in self.tabs.clone().iter().enumerate() { + if tab.path == path { + self.selected_tab = panels::TabNumber::from_index(index); + return; + } + } + } + if self.tabs.len() < MAX_TABS { + if let Some(path) = path_option { + self.tabs.push(panels::Tab::new(path.to_path_buf())); + } else { + self.tabs.push(panels::Tab::default()); + } + self.selected_tab = panels::TabNumber::from_index(self.tabs.len() - 1); + } + } + + pub fn delete_tab(&mut self, index: usize) { + self.tabs.remove(index); + self.selected_tab = panels::TabNumber::from_index(min(index, self.tabs.len() - 1)); + } + + pub fn toggle(&self, ui: &mut egui::Ui, display: bool, title: &str) -> bool { + let bg_color: Color32; + let text_color: Color32; + + if display { + bg_color = hex_str_to_color(self.theme.functions); + text_color = hex_str_to_color(self.theme.bg); + } else { + bg_color = hex_str_to_color(self.theme.bg); + text_color = hex_str_to_color(self.theme.literals); + }; + + ui.style_mut().visuals.override_text_color = Some(text_color); + + if ui.add(egui::Button::new(title).fill(bg_color)).clicked() { + return !display; + } + ui.style_mut().visuals.override_text_color = None; + + display + } + + pub fn profiler(&self) -> String { + if !self.profiler_visible { + return "".to_string(); + } + let combined_string: Vec = TIME_LABELS + .into_iter() + .zip(self.time_watch.clone()) + .map(|(s, v)| format!("{} : {:.1} ms", s, v)) + .collect(); + + let mut result = combined_string.join(" ; "); + result.push_str(&format!( + " total : {:.1} ms", + self.time_watch.clone().iter().sum::() + )); + result + } + + pub fn list_files( + &mut self, + ui: &mut egui::Ui, + file: &panels::FileEntry, + depth: isize, + n_files: &mut usize, + ) { + *n_files += 1; + + if let Some(folder_content) = &file.folder_content { + let collapsing_response = egui::CollapsingHeader::new(file.name.clone()) + .default_open(depth > 0) + .show(ui, |ui| { + if !self.tree_dir_opened.contains(&file.name) { + return; + } + for deeper_file in folder_content { + self.list_files(ui, deeper_file, depth - 1, n_files); + } + }); + if collapsing_response.fully_closed() { + self.tree_dir_opened.retain(|s| s != &file.name); + } else if !self.tree_dir_opened.contains(&file.name) { + self.tree_dir_opened.push(file.name.clone()); + } + } else if ui.button(&file.name).clicked() { + self.open_file(Some(&file.path)); + } + } +} + +#[allow(clippy::unnecessary_lazy_evaluations)] +pub fn hex_str_to_color(hex_str: &str) -> Color32 { + Color32::from_hex(hex_str).unwrap_or_else(|_| Color32::BLACK) +} diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..1c511d4 --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,9 @@ +#[allow(unused_imports)] +mod app; +pub use app::*; + +mod ui; +pub use ui::*; + +mod state; +pub use state::*; diff --git a/src/core/state.rs b/src/core/state.rs new file mode 100644 index 0000000..82e0b3d --- /dev/null +++ b/src/core/state.rs @@ -0,0 +1,58 @@ +use eframe::egui; +use image::GenericImageView; +use serde::{Deserialize, Serialize}; +use std::{ + error::Error, + fs, + fs::{read_to_string, OpenOptions}, + io::Write, + path::{Path, PathBuf}, +}; + +#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] +pub struct AppState { + pub tabs: Vec, + pub theme: usize, +} + +pub fn save_state(state: &AppState, file_path: &Path) -> Result<(), std::io::Error> { + let serialized_state = serde_json::to_string(state)?; + + if let Some(parent_dir) = file_path.parent() { + fs::create_dir_all(parent_dir)?; + } + + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(file_path)?; + + file.write_all(serialized_state.as_bytes())?; + + println!("Saved state at {}", file_path.display()); + + Ok(()) +} + +pub fn load_state(file_path: &Path) -> Result { + let serialized_state = read_to_string(file_path)?; + + Ok(serde_json::from_str(&serialized_state)?) +} + +pub fn load_icon() -> Result> { + let (icon_rgba, icon_width, icon_height) = { + let icon = include_bytes!("../../assets/icon.png"); + let image = image::load_from_memory(icon)?; + let rgba = image.clone().into_rgba8().to_vec(); + let (width, height) = image.dimensions(); + (rgba, width, height) + }; + + Ok(egui::IconData { + rgba: icon_rgba, + width: icon_width, + height: icon_height, + }) +} diff --git a/src/core/ui.rs b/src/core/ui.rs new file mode 100644 index 0000000..1a09bee --- /dev/null +++ b/src/core/ui.rs @@ -0,0 +1,338 @@ +use eframe::egui; +use egui::{text::CCursor, text_edit::CCursorRange, Rangef}; +use std::{cmp::max, env, ffi::OsStr, path::Component, path::Path, path::PathBuf}; + +use crate::core; +use crate::editor; +use crate::panels; +use crate::Calcifer; +use crate::DISPLAY_PATH_DEPTH; +use crate::MAX_TABS; +use crate::PROJECT_EXTENSION; +use crate::RED; +use crate::TERMINAL_HEIGHT; +use crate::TERMINAL_RANGE; +use editor::{CodeEditor, Syntax}; + +impl Calcifer { + pub fn draw_settings(&mut self, ctx: &egui::Context) { + egui::SidePanel::left("settings") + .resizable(false) + .exact_width(self.font_size * 1.8) + .show(ctx, |ui| { + ui.vertical(|ui| { + if ui.add(egui::Button::new("📁")).clicked() { + if let Some(path) = rfd::FileDialog::new() + .set_directory(self.home.as_path()) + .pick_file() + { + self.open_file(Some(&path)); + } + } + ui.separator(); + self.tree_visible = self.toggle(ui, self.tree_visible, "📦"); + ui.separator(); + self.terminal_visible = self.toggle(ui, self.terminal_visible, "🖵"); + ui.separator(); + self.search_menu.visible = self.toggle(ui, self.search_menu.visible, "🔍"); + ui.separator(); + self.settings_menu.visible = self.toggle(ui, self.settings_menu.visible, "⚙"); + ui.separator(); + self.shortcuts_menu.visible = self.toggle(ui, self.shortcuts_menu.visible, "⌨"); + ui.separator(); + self.profiler_visible = self.toggle(ui, self.profiler_visible, "⚡"); + + if self.tabs[self.selected_tab.to_index()].language == PROJECT_EXTENSION { + ui.separator(); + self.project_mode = self.toggle(ui, self.project_mode, "✒"); + } + }); + }); + } + + pub fn draw_tree_panel(&mut self, ctx: &egui::Context) { + if !self.tree_visible { + return; + } + egui::SidePanel::left("file_tree_panel").show(ctx, |ui| { + ui.horizontal(|ui| { + ui.label("Bookshelf "); + if ui.add(egui::Button::new("📖")).clicked() { + self.file_tree = panels::generate_file_tree(self.home.as_path(), 7); + } + }); + ui.separator(); + let mut n_files: usize = 0; + if let Some(file_tree) = self.file_tree.clone() { + self.list_files(ui, &file_tree, 1, &mut n_files); + } else { + ui.label("No book on the Bookshelf"); + } + ui.separator(); + ui.label(format!("{} files displayed", n_files)); + }); + } + + pub fn draw_bottom_tray(&mut self, ctx: &egui::Context) { + egui::TopBottomPanel::bottom("tray") + .default_height(self.font_size * 1.2) + .resizable(false) + .show(ctx, |ui| { + ui.label(self.profiler()); + }); + } + + pub fn draw_terminal_panel(&mut self, ctx: &egui::Context) { + if !self.terminal_visible { + return; + } + egui::TopBottomPanel::bottom("terminal") + .default_height(TERMINAL_HEIGHT) + .height_range(Rangef::new(TERMINAL_RANGE.start, TERMINAL_RANGE.end)) + .resizable(true) + .show(ctx, |ui| { + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { + let command_color = core::hex_str_to_color(self.theme.functions); + let entry_color = core::hex_str_to_color(self.theme.literals); + let bg_color = core::hex_str_to_color(self.theme.bg); + + ui.label(""); + + ui.horizontal(|ui| { + if ui.add(egui::Button::new("⟳")).clicked() { + self.command_history.retain(|e| !e.finished); + } + ui.style_mut().visuals.extreme_bg_color = bg_color; + let Self { command, .. } = self; + ui.colored_label( + command_color, + format_path(&env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))), + ); + let response = ui.add( + egui::TextEdit::singleline(command) + .desired_width(f32::INFINITY) + .lock_focus(true), + ); + + if response.lost_focus() && ctx.input(|i| i.key_pressed(egui::Key::Enter)) { + self.command_history + .push(panels::send_command(self.command.clone())); + self.command = "".into(); + response.request_focus(); + } + }); + ui.separator(); + egui::ScrollArea::vertical() + .stick_to_bottom(true) + .show(ui, |ui| { + ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { + ui.separator(); + ui.spacing_mut().item_spacing.y = 0.0; + ui.style_mut().visuals.hyperlink_color = + core::hex_str_to_color(self.theme.keywords); + + for entry in &mut self.command_history { + ui.label(""); + ui.horizontal(|ui| { + if !entry.finished { + entry.update(); + let _ = ui.link("(⌛)"); + } else if ui.link("(🗐)").clicked() { + entry.copy_error_code(); + } + ui.colored_label( + command_color, + format!("{} {}", entry.env, entry.command), + ); + }); + + for line in &entry.result { + let color = if line.error { RED } else { entry_color }; + ui.colored_label(color, &line.text); + } + } + }); + }); + }); + }); + } + + pub fn draw_tab_panel(&mut self, ctx: &egui::Context) { + egui::TopBottomPanel::top("tabs") + .resizable(false) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.style_mut().visuals.selection.bg_fill = + core::hex_str_to_color(self.theme.functions); + ui.style_mut().visuals.hyperlink_color = + core::hex_str_to_color(self.theme.functions); + for (index, tab) in self.tabs.clone().iter().enumerate() { + let mut title = tab.get_name(); + if !tab.saved { + title += " ~"; + } + if self.selected_tab == panels::TabNumber::from_index(index) { + ui.style_mut().visuals.override_text_color = + Some(core::hex_str_to_color(self.theme.bg)); + } + ui.selectable_value( + &mut self.selected_tab, + panels::TabNumber::from_index(index), + title, + ); + + ui.style_mut().visuals.override_text_color = None; + + if ui.link("X").clicked() && !self.close_tab_confirm.visible { + if self.tabs.len() > 1 { + if tab.saved { + self.delete_tab(index); + } else { + self.close_tab_confirm.ask(); + self.tab_to_close = index; + } + } else { + egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::Close); + } + } + ui.separator(); + } + if self.tabs.len() < MAX_TABS { + ui.selectable_value(&mut self.selected_tab, panels::TabNumber::Open, "+"); + } + if self.selected_tab == panels::TabNumber::Open { + self.open_file(None); + } + }); + }); + } + + pub fn draw_content_panel(&mut self, ctx: &egui::Context) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.horizontal(|ui| { + if ui + .add(egui::Button::new("open directory in terminal")) + .clicked() + { + let mut path = self.tabs[self.selected_tab.to_index()].path.clone(); + path.pop(); + panels::send_command(format!("cd {}", path.display())); + } + + ui.label("Picked file:"); + ui.monospace( + self.tabs[self.selected_tab.to_index()] + .path + .to_string_lossy() + .to_string(), + ); + }); + + ui.separator(); + if self.project_mode + && self.tabs[self.selected_tab.to_index()].language == PROJECT_EXTENSION + { + self.draw_project_file(ui); + } else { + 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; + let mut override_cursor: Option = None; + + if !self.search_menu.result_selected { + override_cursor = Some(CCursorRange::two( + CCursor::new(self.search_menu.get_cursor_start()), + CCursor::new(self.search_menu.get_cursor_end()), + )); + self.search_menu.result_selected = true; + } + + CodeEditor::default() + .id_source("code editor") + .with_rows(max(45, lines)) + .with_fontsize(self.font_size) + .with_theme(self.theme) + .with_syntax(to_syntax(¤t_tab.language)) + .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, + ); + } + + fn draw_project_file(&mut self, ui: &mut egui::Ui) { + ui.label("project mode"); + } + + pub fn draw_windows(&mut self, ctx: &egui::Context) { + if self.search_menu.visible { + self.search_menu + .show(ctx, &mut self.tabs, &mut self.selected_tab); + } + if self.close_tab_confirm.visible { + self.close_tab_confirm.show(ctx); + } + if self.refresh_confirm.visible { + self.refresh_confirm.show(ctx); + } + if self.exit_confirm.visible { + self.exit_confirm.show(ctx); + } + if self.exit_confirm.proceed { + for tab in self.tabs.iter_mut() { + tab.saved = true; + } + egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::Close); + } + if self.shortcuts_menu.visible { + self.shortcuts_menu.show(ctx); + } + if self.settings_menu.visible { + self.settings_menu.show(ctx); + } + if self.settings_menu.updated { + self.theme = self.settings_menu.theme; + } + + self.handle_confirm(); + } +} + +fn to_syntax(language: &str) -> Syntax { + match language { + "py" => Syntax::python(), + "rs" => Syntax::rust(), + _ => Syntax::shell(), + } +} + +pub fn format_path(path: &Path) -> String { + let components: Vec<&OsStr> = path + .components() + .rev() + .take(DISPLAY_PATH_DEPTH) + .filter_map(|component| match component { + Component::RootDir | Component::CurDir => None, + _ => Some(component.as_os_str()), + }) + .collect(); + + format!( + "{}>", + components + .iter() + .rev() + .map(|&c| c.to_string_lossy()) + .collect::>() + .join("/") + ) +} diff --git a/src/calcifer/code_editor/highlighting.rs b/src/editor/highlighting.rs similarity index 100% rename from src/calcifer/code_editor/highlighting.rs rename to src/editor/highlighting.rs diff --git a/src/calcifer/code_editor/mod.rs b/src/editor/mod.rs similarity index 100% rename from src/calcifer/code_editor/mod.rs rename to src/editor/mod.rs diff --git a/src/calcifer/code_editor/syntax/asm.rs b/src/editor/syntax/asm.rs similarity index 100% rename from src/calcifer/code_editor/syntax/asm.rs rename to src/editor/syntax/asm.rs diff --git a/src/calcifer/code_editor/syntax/lua.rs b/src/editor/syntax/lua.rs similarity index 100% rename from src/calcifer/code_editor/syntax/lua.rs rename to src/editor/syntax/lua.rs diff --git a/src/calcifer/code_editor/syntax/mod.rs b/src/editor/syntax/mod.rs similarity index 100% rename from src/calcifer/code_editor/syntax/mod.rs rename to src/editor/syntax/mod.rs diff --git a/src/calcifer/code_editor/syntax/python.rs b/src/editor/syntax/python.rs similarity index 100% rename from src/calcifer/code_editor/syntax/python.rs rename to src/editor/syntax/python.rs diff --git a/src/calcifer/code_editor/syntax/rust.rs b/src/editor/syntax/rust.rs similarity index 100% rename from src/calcifer/code_editor/syntax/rust.rs rename to src/editor/syntax/rust.rs diff --git a/src/calcifer/code_editor/syntax/shell.rs b/src/editor/syntax/shell.rs similarity index 100% rename from src/calcifer/code_editor/syntax/shell.rs rename to src/editor/syntax/shell.rs diff --git a/src/calcifer/code_editor/syntax/sql.rs b/src/editor/syntax/sql.rs similarity index 100% rename from src/calcifer/code_editor/syntax/sql.rs rename to src/editor/syntax/sql.rs diff --git a/src/calcifer/code_editor/tests.rs b/src/editor/tests.rs similarity index 100% rename from src/calcifer/code_editor/tests.rs rename to src/editor/tests.rs diff --git a/src/calcifer/code_editor/themes/ayu.rs b/src/editor/themes/ayu.rs similarity index 100% rename from src/calcifer/code_editor/themes/ayu.rs rename to src/editor/themes/ayu.rs diff --git a/src/calcifer/code_editor/themes/fantasy.rs b/src/editor/themes/fantasy.rs similarity index 100% rename from src/calcifer/code_editor/themes/fantasy.rs rename to src/editor/themes/fantasy.rs diff --git a/src/calcifer/code_editor/themes/github.rs b/src/editor/themes/github.rs similarity index 100% rename from src/calcifer/code_editor/themes/github.rs rename to src/editor/themes/github.rs diff --git a/src/calcifer/code_editor/themes/gruvbox.rs b/src/editor/themes/gruvbox.rs similarity index 100% rename from src/calcifer/code_editor/themes/gruvbox.rs rename to src/editor/themes/gruvbox.rs diff --git a/src/calcifer/code_editor/themes/mod.rs b/src/editor/themes/mod.rs similarity index 100% rename from src/calcifer/code_editor/themes/mod.rs rename to src/editor/themes/mod.rs diff --git a/src/calcifer/code_editor/themes/sonokai.rs b/src/editor/themes/sonokai.rs similarity index 100% rename from src/calcifer/code_editor/themes/sonokai.rs rename to src/editor/themes/sonokai.rs diff --git a/src/main.rs b/src/main.rs index 34598cd..6ad1558 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,16 @@ -mod calcifer; -mod tools; - -use calcifer::code_editor::ColorTheme; use eframe::egui; -use egui::FontFamily::Proportional; -use egui::FontId; -use egui::TextStyle::{Body, Button, Heading, Monospace, Small}; +use egui::{ + FontFamily::Proportional, + FontId, + TextStyle::{Body, Button, Heading, Monospace, Small}, +}; use homedir::get_my_home; use std::{ops::Range, path::PathBuf, sync::Arc, thread, time}; -use calcifer::code_editor::themes::DEFAULT_THEMES; +mod core; +mod editor; +mod panels; +mod sub_windows; #[cfg(debug_assertions)] const TITLE: &str = " debug"; @@ -30,7 +31,7 @@ const DISPLAY_PATH_DEPTH: usize = 3; const MAX_TABS: usize = 20; fn main() -> Result<(), eframe::Error> { - let icon_data = tools::load_icon().unwrap_or_default(); + let icon_data = core::load_icon().unwrap_or_default(); let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() @@ -40,13 +41,13 @@ fn main() -> Result<(), eframe::Error> { }; // Attempt to load previous state - let app_state: tools::AppState = if save_path().exists() { - match tools::load_state(save_path().as_path()) { + let app_state: core::AppState = if save_path().exists() { + match core::load_state(save_path().as_path()) { Ok(app_state) => app_state, - Err(_) => tools::AppState::default(), + Err(_) => core::AppState::default(), } } else { - tools::AppState::default() + core::AppState::default() }; eframe::run_native( @@ -57,33 +58,33 @@ fn main() -> Result<(), eframe::Error> { } struct Calcifer { - selected_tab: tools::TabNumber, - tabs: Vec, + selected_tab: panels::TabNumber, + tabs: Vec, command: String, - command_history: Vec, + command_history: Vec, - theme: ColorTheme, + theme: editor::ColorTheme, font_size: f32, project_mode: bool, home: PathBuf, tree_dir_opened: Vec, - file_tree: Option, + file_tree: Option, tree_visible: bool, profiler_visible: bool, terminal_visible: bool, - close_tab_confirm: tools::confirm::ConfirmWindow, + close_tab_confirm: sub_windows::ConfirmWindow, tab_to_close: usize, - refresh_confirm: tools::confirm::ConfirmWindow, - exit_confirm: tools::confirm::ConfirmWindow, + refresh_confirm: sub_windows::ConfirmWindow, + exit_confirm: sub_windows::ConfirmWindow, - search_menu: tools::search::SearchWindow, - settings_menu: tools::settings::SettingsWindow, - shortcuts_menu: tools::shortcuts::ShortcutsWindow, + search_menu: sub_windows::SearchWindow, + settings_menu: sub_windows::SettingsWindow, + shortcuts_menu: sub_windows::ShortcutsWindow, time_watch: Vec, next_frame: time::Instant, @@ -92,13 +93,13 @@ struct Calcifer { impl Default for Calcifer { fn default() -> Self { Self { - selected_tab: tools::TabNumber::from_index(0), - tabs: vec![tools::Tab::default()], + selected_tab: panels::TabNumber::from_index(0), + tabs: vec![panels::Tab::default()], command: String::new(), command_history: Vec::new(), - theme: DEFAULT_THEMES[0], + theme: editor::themes::DEFAULT_THEMES[0], font_size: 14.0, project_mode: true, @@ -111,20 +112,20 @@ impl Default for Calcifer { profiler_visible: false, terminal_visible: false, - close_tab_confirm: tools::confirm::ConfirmWindow::new( + close_tab_confirm: sub_windows::ConfirmWindow::new( "You have some unsaved changes, Do you still want to close this document ?", "Confirm Close", ), tab_to_close: 0, - refresh_confirm: tools::confirm::ConfirmWindow::new( + refresh_confirm: sub_windows::ConfirmWindow::new( "You have some unsaved changes, Do you still want to refresh this document ?", "Confirm Refresh", ), - exit_confirm: tools::confirm::ConfirmWindow::new("", "Confirm Exit"), + exit_confirm: sub_windows::ConfirmWindow::new("", "Confirm Exit"), - search_menu: tools::search::SearchWindow::default(), - settings_menu: tools::settings::SettingsWindow::new(DEFAULT_THEMES[0]), - shortcuts_menu: tools::shortcuts::ShortcutsWindow::new(), + search_menu: sub_windows::SearchWindow::default(), + settings_menu: sub_windows::SettingsWindow::new(editor::themes::DEFAULT_THEMES[0]), + shortcuts_menu: sub_windows::ShortcutsWindow::new(), time_watch: vec![0.0; TIME_LABELS.len()], next_frame: time::Instant::now(), diff --git a/src/panels/file_tree.rs b/src/panels/file_tree.rs new file mode 100644 index 0000000..ed549de --- /dev/null +++ b/src/panels/file_tree.rs @@ -0,0 +1,114 @@ +use std::{ + cmp::Ordering, + ffi::OsStr, + fs, io, + path::{Path, PathBuf}, +}; + +use crate::ALLOWED_FILE_EXTENSIONS; + +#[derive(Clone)] +pub struct FileEntry { + pub name: String, + pub path: PathBuf, + pub folder_content: Option>, + pub folder_open: bool, +} + +impl FileEntry { + pub fn new_entry(name: String, path: PathBuf) -> Self { + Self { + name, + path, + folder_content: None, + folder_open: false, + } + } +} + +pub fn generate_file_tree(path: &Path, depth: isize) -> Option { + if let Some(file_name) = path.file_name() { + if file_name.to_string_lossy().starts_with('.') { + return None; + } + let extension = path.extension().and_then(|ext| ext.to_str()); + if !ALLOWED_FILE_EXTENSIONS.contains(&extension.unwrap_or_default()) { + return None; + } + } else { + return None; + } + + let name = path + .file_name() + .unwrap_or_else(|| OsStr::new("")) + .to_string_lossy() + .into_owned(); + + if !path.is_dir() || depth < 0 { + return Some(FileEntry::new_entry(name, path.to_path_buf())); + } + + match fs::read_dir(path) { + Err(err) => Some(FileEntry::new_entry( + format!("Error reading directory: {}", err), + path.to_path_buf(), + )), + Ok(entries) => { + let mut paths: Vec> = entries + .map(|r| r.map_err(|e| io::Error::new(io::ErrorKind::Other, e))) + .collect(); + + paths.sort_by(|a, b| match (a, b) { + (Ok(entry_a), Ok(entry_b)) => sort_directories_first(entry_a, entry_b), + (Err(_), Ok(_)) => std::cmp::Ordering::Greater, + (Ok(_), Err(_)) => std::cmp::Ordering::Less, + (Err(_), Err(_)) => std::cmp::Ordering::Equal, + }); + + let mut folder_content = Vec::new(); + + for result in paths { + match result { + Ok(entry) => { + if let Some(file) = generate_file_tree(&entry.path(), depth - 1) { + folder_content.push(file); + } + } + Err(err) => { + folder_content.push(FileEntry::new_entry( + format!("Error reading entry: {}", err), + path.to_path_buf(), + )); + } + } + } + + if folder_content.is_empty() { + return None; + } + + Some(FileEntry { + name, + path: path.to_path_buf(), + folder_content: Some(folder_content), + folder_open: false, + }) + } + } +} + +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(); + + // Directories come first, then files + if a_is_dir && !b_is_dir { + Ordering::Less + } else if !a_is_dir && b_is_dir { + Ordering::Greater + } else { + // Both are either directories or files, sort alphabetically + a.path().cmp(&b.path()) + } +} diff --git a/src/panels/mod.rs b/src/panels/mod.rs new file mode 100644 index 0000000..af45dbe --- /dev/null +++ b/src/panels/mod.rs @@ -0,0 +1,9 @@ +#[allow(unused_imports)] +mod tabs; +pub use tabs::*; + +mod file_tree; +pub use file_tree::*; + +mod terminal; +pub use terminal::*; diff --git a/src/tools/tabs.rs b/src/panels/tabs.rs similarity index 100% rename from src/tools/tabs.rs rename to src/panels/tabs.rs diff --git a/src/panels/terminal/linux_terminal.rs b/src/panels/terminal/linux_terminal.rs new file mode 100644 index 0000000..025146f --- /dev/null +++ b/src/panels/terminal/linux_terminal.rs @@ -0,0 +1,183 @@ +use crate::core::format_path; + +use arboard::Clipboard; +use nix::fcntl::{fcntl, FcntlArg, OFlag}; +use std::{ + env, + io::{BufRead, BufReader}, + os::fd::AsRawFd, + path::{Path, PathBuf}, + process::{Child, Command, Stdio}, +}; + +pub struct Buffer { + pub output_buffer: BufReader, + pub error_buffer: BufReader, + pub child: Child, +} + +#[derive(Clone)] +pub struct Line { + pub text: String, + pub error: bool, +} + +impl Line { + fn output(text: String) -> Self { + Self { + text: remove_line_break(text), + error: false, + } + } + fn error(text: String) -> Self { + Self { + text: remove_line_break(text), + error: true, + } + } +} + +pub struct CommandEntry { + pub env: String, + pub command: String, + pub result: Vec, + pub buffer: Option, + pub finished: bool, +} + +impl CommandEntry { + pub fn new(env: String, command: String) -> Self { + let (buffer, result) = match execute(command.clone()) { + Ok(command_buffer) => (Some(command_buffer), vec![]), + Err(err) => ( + None, + vec![Line::error(format!("failed to get results: {}", err))], + ), + }; + + CommandEntry { + env, + command, + result, + buffer, + finished: false, + } + } + + pub fn update(&mut self) { + if let Some(buffer) = &mut self.buffer { + let mut output = String::new(); + let _ = buffer.output_buffer.read_line(&mut output); + if !remove_line_break(output.to_string()).is_empty() { + self.result.push(Line::output(format!("{}\n", output))); + } + let mut error = String::new(); + let _ = buffer.error_buffer.read_line(&mut error); + if !remove_line_break(error.to_string()).is_empty() { + self.result.push(Line::error(format!("{}\n", error))); + } + } + if let Some(buffer) = &mut self.buffer { + if let Ok(Some(_exit_status)) = buffer.child.try_wait() { + //self.result.push(Line::output(format!("Command finished with status: {:?}\n", exit_status))); + self.finished = true; + } + } + } + + pub fn copy_error_code(&self) { + let mut txt: String = "".to_string(); + for line in self.result.iter() { + if line.error { + txt.push_str(&format!("{}\n", line.text)); + } + } + let mut _clipboard = Clipboard::new().expect("Failed to initialize clipboard"); + _clipboard.set_text(txt).unwrap(); + } +} + +pub fn send_command(command: String) -> CommandEntry { + let env = format_path(&env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))); + + if command.len() < 2 { + return CommandEntry::new(env, command); + } + + if &command[..2] != "cd" { + return CommandEntry::new(env, command); + } + + if command.len() < 4 { + let mut entry = + CommandEntry::new(env, "echo Invalid cd, should provide path >&2".to_string()); + entry.command = command; + return entry; + } + + let path_append = command[3..].replace('~', "/home/penwing"); + let path = Path::new(&path_append); + + if format!("{}", path.display()) == "/" { + let mut entry = CommandEntry::new(env, "echo Root access denied >&2".to_string()); + entry.command = command; + return entry; + } + + if env::set_current_dir(path).is_ok() { + let mut entry = CommandEntry::new(env, format!("echo Moved to : {}", path.display())); + entry.command = command; + entry + } else { + let mut entry = CommandEntry::new( + env, + format!("echo Could not find path : {} >&2", path.display()), + ); + entry.command = command; + entry + } +} + +pub fn execute(command: String) -> Result { + let mut child = Command::new("sh") + .arg("-c") + .arg(command.clone()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn()?; + + let stdout = child + .stdout + .take() + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "Failed to open stdout"))?; + let stderr = child + .stderr + .take() + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "Failed to open stderr"))?; + + let stdout_fd = stdout.as_raw_fd(); + let stderr_fd = stderr.as_raw_fd(); + + fcntl(stdout_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))?; + fcntl(stderr_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))?; + + let output_buffer = BufReader::new(stdout); + let error_buffer = BufReader::new(stderr); + + Ok(Buffer { + output_buffer, + error_buffer, + child, + }) +} + +fn remove_line_break(input: String) -> String { + let mut text = input.clone(); + while text.ends_with('\n') { + text.pop(); + if text.ends_with('\r') { + text.pop(); + } + } + text +} diff --git a/src/panels/terminal/mod.rs b/src/panels/terminal/mod.rs new file mode 100644 index 0000000..9863f85 --- /dev/null +++ b/src/panels/terminal/mod.rs @@ -0,0 +1,10 @@ +#[allow(unused_imports)] +#[cfg(target_os = "linux")] +mod linux_terminal; +#[cfg(target_os = "linux")] +pub use linux_terminal::*; + +#[cfg(target_os = "windows")] +mod windows_terminal; +#[cfg(target_os = "windows")] +pub use windows_terminal::*; diff --git a/src/tools/windows_terminal.rs b/src/panels/terminal/windows_terminal.rs similarity index 100% rename from src/tools/windows_terminal.rs rename to src/panels/terminal/windows_terminal.rs diff --git a/src/tools/confirm.rs b/src/sub_windows/confirm.rs similarity index 100% rename from src/tools/confirm.rs rename to src/sub_windows/confirm.rs diff --git a/src/sub_windows/mod.rs b/src/sub_windows/mod.rs new file mode 100644 index 0000000..4cd0309 --- /dev/null +++ b/src/sub_windows/mod.rs @@ -0,0 +1,12 @@ +#[allow(unused_imports)] +mod confirm; +pub use confirm::*; + +mod search; +pub use search::*; + +mod settings; +pub use settings::*; + +mod shortcuts; +pub use shortcuts::*; diff --git a/src/tools/search.rs b/src/sub_windows/search.rs similarity index 99% rename from src/tools/search.rs rename to src/sub_windows/search.rs index 12bfd19..8f4034e 100644 --- a/src/tools/search.rs +++ b/src/sub_windows/search.rs @@ -1,7 +1,7 @@ use eframe::egui; use std::cmp::min; -use crate::tools::{tabs::Tab, tabs::TabNumber}; +use crate::panels::{Tab, TabNumber}; use crate::RED; enum Action { diff --git a/src/tools/settings.rs b/src/sub_windows/settings.rs similarity index 96% rename from src/tools/settings.rs rename to src/sub_windows/settings.rs index df384d6..6decc35 100644 --- a/src/tools/settings.rs +++ b/src/sub_windows/settings.rs @@ -1,5 +1,4 @@ -use crate::ColorTheme; -use crate::DEFAULT_THEMES; +use crate::editor::{themes::DEFAULT_THEMES, ColorTheme}; use eframe::egui; pub struct SettingsWindow { diff --git a/src/tools/shortcuts.rs b/src/sub_windows/shortcuts.rs similarity index 100% rename from src/tools/shortcuts.rs rename to src/sub_windows/shortcuts.rs diff --git a/src/tools/file_tree.rs b/src/tools/file_tree.rs deleted file mode 100644 index 46e1069..0000000 --- a/src/tools/file_tree.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::ffi::OsStr; -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; - -use crate::tools; -use crate::ALLOWED_FILE_EXTENSIONS; - -#[derive(Clone)] -pub struct File { - pub name: String, - pub path: PathBuf, - pub folder_content: Option>, - pub folder_open: bool, -} - -impl File { - pub fn new_file(name: String, path: PathBuf) -> Self { - Self { - name, - path, - folder_content: None, - folder_open: false, - } - } -} - -pub fn generate_file_tree(path: &Path, depth: isize) -> Option { - if let Some(file_name) = path.file_name() { - if file_name.to_string_lossy().starts_with('.') { - return None; - } - let extension = path.extension().and_then(|ext| ext.to_str()); - if !ALLOWED_FILE_EXTENSIONS.contains(&extension.unwrap_or_default()) { - return None; - } - } else { - return None; - } - - let name = path - .file_name() - .unwrap_or_else(|| OsStr::new("")) - .to_string_lossy() - .into_owned(); - - if !path.is_dir() || depth < 0 { - return Some(File::new_file(name, path.to_path_buf())); - } - - match fs::read_dir(path) { - Err(err) => { - Some(File::new_file( - format!("Error reading directory: {}", err), - path.to_path_buf(), - )) - } - Ok(entries) => { - let mut paths: Vec> = entries - .map(|r| r.map_err(|e| io::Error::new(io::ErrorKind::Other, e))) - .collect(); - - paths.sort_by(|a, b| match (a, b) { - (Ok(entry_a), Ok(entry_b)) => tools::sort_directories_first(entry_a, entry_b), - (Err(_), Ok(_)) => std::cmp::Ordering::Greater, - (Ok(_), Err(_)) => std::cmp::Ordering::Less, - (Err(_), Err(_)) => std::cmp::Ordering::Equal, - }); - - let mut folder_content = Vec::new(); - - for result in paths { - match result { - Ok(entry) => { - if let Some(file) = generate_file_tree(&entry.path(), depth - 1) { - folder_content.push(file); - } - } - Err(err) => { - folder_content.push(File::new_file( - format!("Error reading entry: {}", err), - path.to_path_buf(), - )); - } - } - } - - if folder_content.is_empty() { - return None; - } - - Some(File { - name, - path: path.to_path_buf(), - folder_content: Some(folder_content), - folder_open: false, - }) - } - } -} diff --git a/src/tools/mod.rs b/src/tools/mod.rs deleted file mode 100644 index 2c65c5c..0000000 --- a/src/tools/mod.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::calcifer::code_editor::Syntax; -use crate::DISPLAY_PATH_DEPTH; -use eframe::egui; -use egui::Color32; -use image::GenericImageView; -use serde::{Deserialize, Serialize}; -use std::{ - cmp::Ordering, error::Error, ffi::OsStr, fs, fs::read_to_string, fs::OpenOptions, io::Write, - path::Component, path::Path, path::PathBuf, -}; - -//my tools; -pub mod confirm; -pub mod file_tree; -pub mod search; -pub mod settings; -pub mod shortcuts; - -#[cfg(target_os = "linux")] -pub mod terminal; -#[cfg(target_os = "linux")] -pub use terminal::*; - -#[cfg(target_os = "windows")] -pub mod windows_terminal; -#[cfg(target_os = "windows")] -pub use windows_terminal::*; - -pub mod tabs; -pub use tabs::*; - -#[derive(Serialize, Deserialize, Debug, PartialEq, Default)] -pub struct AppState { - pub tabs: Vec, - pub theme: usize, -} - -pub fn save_state(state: &AppState, file_path: &Path) -> Result<(), std::io::Error> { - let serialized_state = serde_json::to_string(state)?; - - if let Some(parent_dir) = file_path.parent() { - fs::create_dir_all(parent_dir)?; - } - - let mut file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(file_path)?; - - file.write_all(serialized_state.as_bytes())?; - - println!("Saved state at {}", file_path.display()); - - Ok(()) -} - -pub fn load_state(file_path: &Path) -> Result { - let serialized_state = read_to_string(file_path)?; - - Ok(serde_json::from_str(&serialized_state)?) -} - -pub fn load_icon() -> Result> { - let (icon_rgba, icon_width, icon_height) = { - let icon = include_bytes!("../../assets/icon.png"); - let image = image::load_from_memory(icon)?; - let rgba = image.clone().into_rgba8().to_vec(); - let (width, height) = image.dimensions(); - (rgba, width, height) - }; - - Ok(egui::IconData { - rgba: icon_rgba, - width: icon_width, - height: icon_height, - }) -} - -pub fn to_syntax(language: &str) -> Syntax { - match language { - "py" => Syntax::python(), - "rs" => Syntax::rust(), - _ => Syntax::shell(), - } -} - -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(); - - // Directories come first, then files - if a_is_dir && !b_is_dir { - Ordering::Less - } else if !a_is_dir && b_is_dir { - Ordering::Greater - } else { - // Both are either directories or files, sort alphabetically - a.path().cmp(&b.path()) - } -} - -pub fn format_path(path: &Path) -> String { - let components: Vec<&OsStr> = path - .components() - .rev() - .take(DISPLAY_PATH_DEPTH) - .filter_map(|component| match component { - Component::RootDir | Component::CurDir => None, - _ => Some(component.as_os_str()), - }) - .collect(); - - format!( - "{}>", - components - .iter() - .rev() - .map(|&c| c.to_string_lossy()) - .collect::>() - .join("/") - ) -} - -pub fn hex_str_to_color(hex_str: &str) -> Color32 { - Color32::from_hex(hex_str).unwrap_or_else(|_| Color32::BLACK) -} - -#[cfg(test)] -mod tests; diff --git a/src/tools/terminal.rs b/src/tools/terminal.rs deleted file mode 100644 index 8bb2f17..0000000 --- a/src/tools/terminal.rs +++ /dev/null @@ -1,184 +0,0 @@ -use crate::tools::format_path; -use arboard::Clipboard; -use nix::fcntl::{fcntl, FcntlArg, OFlag}; -use std::{ - env, io::{BufRead, BufReader}, - os::fd::AsRawFd, - path::{Path, PathBuf}, - process::{Child, Command, Stdio}, -}; - -pub struct Buffer { - pub output_buffer: BufReader, - pub error_buffer: BufReader, - pub child: Child, -} - -#[derive(Clone)] -pub struct Line { - pub text: String, - pub error: bool, -} - -impl Line { - fn output(text: String) -> Self { - Self { - text: remove_line_break(text), - error: false, - } - } - fn error(text: String) -> Self { - Self { - text: remove_line_break(text), - error: true, - } - } -} - -pub struct CommandEntry { - pub env: String, - pub command: String, - pub result: Vec, - pub buffer: Option, - pub finished: bool, -} - -impl CommandEntry { - pub fn new(env: String, command: String) -> Self { - let (buffer, result) = match execute(command.clone()) { - Ok(command_buffer) => (Some(command_buffer), vec![]), - Err(err) => ( - None, - vec![Line::error(format!("failed to get results: {}", err))], - ), - }; - - CommandEntry { - env, - command, - result, - buffer, - finished: false, - } - } - - pub fn update(&mut self) { - if let Some(buffer) = &mut self.buffer { - let mut output = String::new(); - let _ = buffer.output_buffer.read_line(&mut output); - if !remove_line_break(output.to_string()).is_empty() { - self.result.push(Line::output(format!("{}\n", output))); - } - let mut error = String::new(); - let _ = buffer.error_buffer.read_line(&mut error); - if !remove_line_break(error.to_string()).is_empty() { - self.result.push(Line::error(format!("{}\n", error))); - } - } - if let Some(buffer) = &mut self.buffer { - if let Ok(status) = buffer.child.try_wait() { - if let Some(_exit_status) = status { - //self.result.push(Line::output(format!("Command finished with status: {:?}\n", exit_status))); - self.finished = true; - } - } - } - } - - - pub fn copy_error_code(&self) { - let mut txt : String = "".to_string(); - for line in self.result.iter() { - if line.error { - txt.push_str(&format!("{}\n", line.text)); - } - } - let mut _clipboard = Clipboard::new().expect("Failed to initialize clipboard"); - _clipboard.set_text(txt).unwrap(); - } -} - -pub fn send_command(command: String) -> CommandEntry { - let env = format_path(&env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))); - - if command.len() < 2 { - return CommandEntry::new(env, command); - } - - if &command[..2] != "cd" { - return CommandEntry::new(env, command); - } - - if command.len() < 4 { - let mut entry = - CommandEntry::new(env, "echo Invalid cd, should provide path >&2".to_string()); - entry.command = command; - return entry; - } - - let path_append = command[3..].replace('~', "/home/penwing"); - let path = Path::new(&path_append); - - if format!("{}", path.display()) == "/" { - let mut entry = CommandEntry::new(env, "echo Root access denied >&2".to_string()); - entry.command = command; - return entry; - } - - if env::set_current_dir(path).is_ok() { - let mut entry = CommandEntry::new(env, format!("echo Moved to : {}", path.display())); - entry.command = command; - entry - } else { - let mut entry = CommandEntry::new( - env, - format!("echo Could not find path : {} >&2", path.display()), - ); - entry.command = command; - entry - } -} - -pub fn execute(command: String) -> Result { - let mut child = Command::new("sh") - .arg("-c") - .arg(command.clone()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - - let stdout = child - .stdout - .take() - .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "Failed to open stdout"))?; - let stderr = child - .stderr - .take() - .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "Failed to open stderr"))?; - - let stdout_fd = stdout.as_raw_fd(); - let stderr_fd = stderr.as_raw_fd(); - - fcntl(stdout_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))?; - fcntl(stderr_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))?; - - let output_buffer = BufReader::new(stdout); - let error_buffer = BufReader::new(stderr); - - Ok(Buffer { - output_buffer, - error_buffer, - child, - }) -} - -fn remove_line_break(input: String) -> String { - let mut text = input.clone(); - while text.ends_with('\n') { - text.pop(); - if text.ends_with('\r') { - text.pop(); - } - } - text -} diff --git a/src/tools/tests.rs b/src/tools/tests.rs deleted file mode 100644 index 181dc22..0000000 --- a/src/tools/tests.rs +++ /dev/null @@ -1,84 +0,0 @@ -#[cfg(test)] - -mod tests { - - use crate::tools::*; - - #[test] - fn test_tab_number_conversions() { - let tab_num = TabNumber::from_index(3); - assert_eq!(tab_num, TabNumber::Three); - assert_eq!(tab_num.to_index(), 3); - } - - #[test] - fn test_default_tab() { - let default_tab = Tab::default(); - assert_eq!(default_tab.path, PathBuf::from("untitled")); - assert_eq!(default_tab.code, "// Hello there, Master"); - assert_eq!(default_tab.language, "rs"); - assert!(!default_tab.saved); - assert_eq!(default_tab.scroll_offset, 0.0); - assert_eq!(default_tab.last_cursor, None); - } - - #[test] - fn test_get_tab_name() { - let tab = Tab { - path: PathBuf::from("/path/to/file.rs"), - code: String::from(""), - language: String::from("rs"), - saved: true, - scroll_offset: 0.0, - last_cursor: None, - }; - assert_eq!(tab.get_name(), "file.rs"); - } - - #[test] - fn test_default_command_entry() { - let default_entry = CommandEntry::default(); - assert_eq!( - default_entry.env, - env::current_dir() - .expect("Could not find Shell Environnment") - .file_name() - .expect("Could not get Shell Environnment Name") - .to_string_lossy() - .to_string() - ); - assert_eq!(default_entry.command, ""); - assert_eq!(default_entry.output, ""); - assert_eq!(default_entry.error, ""); - } - - #[test] - fn test_save_and_load_state() { - let tabs = vec![ - PathBuf::from("/path/to/file1.rs"), - PathBuf::from("/path/to/file2.py"), - ]; - let theme = 42; - let original_state = AppState { tabs, theme }; - - // Save state to a temporary file - let temp_file_path = "/tmp/test_state.json"; - save_state(&original_state, temp_file_path).expect("Failed to save state"); - - // Load state from the temporary file - let loaded_state = load_state(temp_file_path).expect("Failed to load state"); - - assert_eq!(original_state, loaded_state); - } - - #[test] - fn test_run_command() { - let cmd = "echo hello".to_string(); - let entry = run_command(cmd); - assert_eq!(entry.command, "echo hello"); - assert_eq!(entry.output.trim(), "hello"); - assert_eq!(entry.error, ""); - } - - // Add more tests as needed for other functions -}