better format

This commit is contained in:
Penwing 2024-01-25 20:50:56 +01:00
parent fd1e818447
commit c8ce57781e
16 changed files with 1768 additions and 1615 deletions

View file

@ -1,242 +1,285 @@
use eframe::egui; use eframe::egui;
use egui::{text::CCursor, text_edit::CCursorRange, Rangef}; use egui::{text::CCursor, text_edit::CCursorRange, Rangef};
use std::{env, path::Path, cmp::max}; use std::{cmp::max, env, path::Path};
use crate::tools; use crate::tools;
use crate::Calcifer; use crate::Calcifer;
use crate::PATH_ROOT;
use crate::MAX_TABS; use crate::MAX_TABS;
use crate::PATH_ROOT;
pub mod code_editor; pub mod code_editor;
use code_editor::CodeEditor; use code_editor::CodeEditor;
mod app_base; mod app_base;
impl Calcifer { impl Calcifer {
pub fn draw_settings(&mut self, ctx: &egui::Context) { pub fn draw_settings(&mut self, ctx: &egui::Context) {
egui::SidePanel::left("settings") egui::SidePanel::left("settings")
.resizable(false) .resizable(false)
.exact_width(self.font_size * 1.8) .exact_width(self.font_size * 1.8)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.vertical(|ui| { ui.vertical(|ui| {
if ui.add(egui::Button::new("📁")).clicked() { if ui.add(egui::Button::new("📁")).clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(Path::new(&PATH_ROOT)).pick_file() { if let Some(path) = rfd::FileDialog::new()
self.open_file(Some(&path)); .set_directory(Path::new(&PATH_ROOT))
} .pick_file()
} {
ui.separator(); self.open_file(Some(&path));
self.tree_visible = self.toggle(ui, self.tree_visible, "🗐"); }
ui.separator(); }
self.terminal_visible = self.toggle(ui, self.terminal_visible, "🖵"); ui.separator();
ui.separator(); self.tree_visible = self.toggle(ui, self.tree_visible, "🗐");
self.search_menu.visible = self.toggle(ui, self.search_menu.visible, "🔍"); ui.separator();
ui.separator(); self.terminal_visible = self.toggle(ui, self.terminal_visible, "🖵");
self.settings_menu.visible = self.toggle(ui, self.settings_menu.visible, ""); ui.separator();
ui.separator(); self.search_menu.visible = self.toggle(ui, self.search_menu.visible, "🔍");
self.shortcuts_menu.visible = self.toggle(ui, self.shortcuts_menu.visible, ""); ui.separator();
ui.separator(); self.settings_menu.visible = self.toggle(ui, self.settings_menu.visible, "");
self.profiler_visible = self.toggle(ui, self.profiler_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, "");
});
});
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.heading("Bookshelf");
ui.separator();
let _ = self.list_files(ui, Path::new(&PATH_ROOT));
ui.separator();
});
}
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.clone())
.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 = egui::Color32::from_hex(self.theme.functions).expect("Theme color issue (functions)");
let entry_color = egui::Color32::from_hex(self.theme.literals).expect("Theme color issue (literals)");
let bg_color = egui::Color32::from_hex(self.theme.bg).expect("Theme color issue (bg)");
ui.label("");
ui.horizontal(|ui| {
if ui.add(egui::Button::new("")).clicked() {
self.command_history = vec![];
}
ui.style_mut().visuals.extreme_bg_color = bg_color;
let Self { command, .. } = self;
ui.colored_label(command_color.clone(), tools::format_path(&env::current_dir().expect("Could not find Shell Environnment")));
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)) { pub fn draw_tree_panel(&mut self, ctx: &egui::Context) {
self.command_history.push(tools::send_command(self.command.clone())); if !self.tree_visible {
self.command = "".into(); return;
response.request_focus(); }
} egui::SidePanel::left("file_tree_panel").show(ctx, |ui| {
}); ui.heading("Bookshelf");
ui.separator(); ui.separator();
egui::ScrollArea::vertical().stick_to_bottom(true).show(ui, |ui| { let _ = self.list_files(ui, Path::new(&PATH_ROOT));
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { ui.separator();
ui.separator(); });
ui.horizontal_wrapped(|ui| { }
ui.spacing_mut().item_spacing.y = 0.0;
for entry in &mut self.command_history {
entry.update();
ui.colored_label(command_color, format!("\n{} {}", entry.env, entry.command));
ui.end_row();
if entry.output != "" {
ui.colored_label(entry_color, &entry.output);
ui.end_row();
}
if entry.error != "" {
ui.colored_label(super::RED, &entry.error);
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 = egui::Color32::from_hex(self.theme.functions).expect("Could not convert color");
ui.style_mut().visuals.hyperlink_color = egui::Color32::from_hex(self.theme.functions).expect("Could not convert color");
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(egui::Color32::from_hex(self.theme.bg).expect("Could not convert color"));
}
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 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();
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<CCursorRange> = None;
if !self.search_menu.result_selected { pub fn draw_bottom_tray(&mut self, ctx: &egui::Context) {
override_cursor = Some(CCursorRange::two( egui::TopBottomPanel::bottom("tray")
CCursor::new(self.search_menu.get_cursor_start()), .default_height(self.font_size * 1.2)
CCursor::new(self.search_menu.get_cursor_end()), .resizable(false)
)); .show(ctx, |ui| {
self.search_menu.result_selected = true; ui.label(self.profiler());
} });
}
CodeEditor::default().id_source("code editor")
.with_rows(max(45,lines)) pub fn draw_terminal_panel(&mut self, ctx: &egui::Context) {
.with_fontsize(self.font_size) if !self.terminal_visible {
.with_theme(self.theme) return;
.with_syntax(tools::to_syntax(&current_tab.language)) }
.with_numlines(true) egui::TopBottomPanel::bottom("terminal")
.show(ui, &mut current_tab.code, &mut current_tab.saved, &mut current_tab.last_cursor, &mut current_tab.scroll_offset, override_cursor); .default_height(super::TERMINAL_HEIGHT.clone())
} .height_range(Rangef::new(
super::TERMINAL_RANGE.start,
pub fn draw_windows(&mut self, ctx: &egui::Context) { super::TERMINAL_RANGE.end,
if self.search_menu.visible { ))
self.search_menu.show(ctx, &mut self.tabs, &mut self.selected_tab); .resizable(true)
} .show(ctx, |ui| {
if self.close_tab_confirm.visible { ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
self.close_tab_confirm.show(ctx); let command_color = egui::Color32::from_hex(self.theme.functions)
} .expect("Theme color issue (functions)");
if self.refresh_confirm.visible { let entry_color = egui::Color32::from_hex(self.theme.literals)
self.refresh_confirm.show(ctx); .expect("Theme color issue (literals)");
} let bg_color =
if self.exit_confirm.visible { egui::Color32::from_hex(self.theme.bg).expect("Theme color issue (bg)");
self.exit_confirm.show(ctx);
} ui.label("");
if self.exit_confirm.proceed {
for tab in self.tabs.iter_mut() { ui.horizontal(|ui| {
tab.saved = true; if ui.add(egui::Button::new("")).clicked() {
} self.command_history = vec![];
egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::Close); }
} ui.style_mut().visuals.extreme_bg_color = bg_color;
if self.shortcuts_menu.visible { let Self { command, .. } = self;
self.shortcuts_menu.show(ctx); ui.colored_label(
} command_color.clone(),
if self.settings_menu.visible { tools::format_path(
self.settings_menu.show(ctx); &env::current_dir().expect("Could not find Shell Environnment"),
} ),
if self.settings_menu.updated { );
self.theme = self.settings_menu.theme.clone(); let response = ui.add(
} egui::TextEdit::singleline(command)
.desired_width(f32::INFINITY)
self.handle_confirm(); .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.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.y = 0.0;
for entry in &mut self.command_history {
entry.update();
ui.colored_label(
command_color,
format!("\n{} {}", entry.env, entry.command),
);
ui.end_row();
if entry.output != "" {
ui.colored_label(entry_color, &entry.output);
ui.end_row();
}
if entry.error != "" {
ui.colored_label(super::RED, &entry.error);
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 =
egui::Color32::from_hex(self.theme.functions)
.expect("Could not convert color");
ui.style_mut().visuals.hyperlink_color =
egui::Color32::from_hex(self.theme.functions)
.expect("Could not convert color");
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(
egui::Color32::from_hex(self.theme.bg)
.expect("Could not convert color"),
);
}
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 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();
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<CCursorRange> = 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(&current_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,
);
}
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.clone();
}
self.handle_confirm();
}
} }

View file

@ -1,188 +1,213 @@
use std::{path::PathBuf, fs, path::Path, cmp::min, io};
use eframe::egui; use eframe::egui;
use egui::Color32; use egui::Color32;
use std::{cmp::min, fs, io, path::Path, path::PathBuf};
use crate::Calcifer;
use crate::tools; use crate::tools;
use crate::PATH_ROOT; use crate::Calcifer;
use crate::DEFAULT_THEMES; use crate::DEFAULT_THEMES;
use crate::MAX_TABS; use crate::MAX_TABS;
use crate::PATH_ROOT;
use crate::SAVE_PATH; use crate::SAVE_PATH;
use crate::TIME_LABELS; use crate::TIME_LABELS;
impl Calcifer { impl Calcifer {
pub fn handle_confirm(&mut self) { pub fn handle_confirm(&mut self) {
if self.close_tab_confirm.proceed { if self.close_tab_confirm.proceed {
self.close_tab_confirm.close(); self.close_tab_confirm.close();
self.delete_tab(self.tab_to_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<PathBuf> {
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<PathBuf> {
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<PathBuf>) {
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().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 { if self.refresh_confirm.proceed {
let _ = self.list_files(ui, &result.path()); self.refresh_confirm.close();
} self.tabs[self.selected_tab.to_index()].refresh();
}); }
} 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) {
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.clone() {
bg_color = Color32::from_hex(self.theme.functions).expect("Could not convert color to hex (functions)");
text_color = Color32::from_hex(self.theme.bg).expect("Could not convert color to hex (bg)");
} else {
bg_color = Color32::from_hex(self.theme.bg).expect("Could not convert color to hex (bg)");
text_color = Color32::from_hex(self.theme.literals).expect("Could not convert color to hex (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;
return display
}
pub fn profiler(&self) -> String {
if !self.profiler_visible {
return "".to_string()
}
let combined_string: Vec<String> = TIME_LABELS.into_iter().zip(self.time_watch.clone().into_iter())
.map(|(s, v)| format!("{} : {:.1} ms", s, v)).collect();
let mut result = combined_string.join(" ; "); pub fn save_tab(&self) -> Option<PathBuf> {
result.push_str(&format!(" total : {:.1} ms", self.time_watch.clone().iter().sum::<f32>())); if self.tabs[self.selected_tab.to_index()]
return result .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<PathBuf> {
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<PathBuf>) {
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()
.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) {
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.clone() {
bg_color = Color32::from_hex(self.theme.functions)
.expect("Could not convert color to hex (functions)");
text_color =
Color32::from_hex(self.theme.bg).expect("Could not convert color to hex (bg)");
} else {
bg_color =
Color32::from_hex(self.theme.bg).expect("Could not convert color to hex (bg)");
text_color = Color32::from_hex(self.theme.literals)
.expect("Could not convert color to hex (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;
return display;
}
pub fn profiler(&self) -> String {
if !self.profiler_visible {
return "".to_string();
}
let combined_string: Vec<String> = TIME_LABELS
.into_iter()
.zip(self.time_watch.clone().into_iter())
.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::<f32>()
));
return result;
}
}

View file

@ -1,8 +1,8 @@
use super::syntax::{Syntax, TokenType, QUOTES, SEPARATORS}; use super::syntax::{Syntax, TokenType, QUOTES, SEPARATORS};
use std::mem;
use super::CodeEditor; use super::CodeEditor;
use eframe::egui::text::LayoutJob;
use eframe::egui; use eframe::egui;
use eframe::egui::text::LayoutJob;
use std::mem;
#[derive(Default, Debug, PartialEq, PartialOrd, Eq, Ord)] #[derive(Default, Debug, PartialEq, PartialOrd, Eq, Ord)]
/// Lexer and Token /// Lexer and Token
@ -222,8 +222,6 @@ impl Token {
} }
} }
impl eframe::egui::util::cache::ComputerMut<(&CodeEditor, &str), LayoutJob> for Token { impl eframe::egui::util::cache::ComputerMut<(&CodeEditor, &str), LayoutJob> for Token {
fn compute(&mut self, (cache, text): (&CodeEditor, &str)) -> LayoutJob { fn compute(&mut self, (cache, text): (&CodeEditor, &str)) -> LayoutJob {
self.highlight(cache, text) self.highlight(cache, text)
@ -236,7 +234,6 @@ pub fn highlight(ctx: &egui::Context, cache: &CodeEditor, text: &str) -> LayoutJ
ctx.memory_mut(|mem| mem.caches.cache::<HighlightCache>().get((cache, text))) ctx.memory_mut(|mem| mem.caches.cache::<HighlightCache>().get((cache, text)))
} }
impl CodeEditor { impl CodeEditor {
fn append(&self, job: &mut LayoutJob, token: &Token) { fn append(&self, job: &mut LayoutJob, token: &Token) {
job.append(token.buffer(), 0.0, self.format(token.ty())); job.append(token.buffer(), 0.0, self.format(token.ty()));

View file

@ -5,436 +5,502 @@ mod syntax;
pub mod themes; pub mod themes;
use eframe::egui; use eframe::egui;
use egui::{text_edit::CCursorRange, text::CCursor}; use egui::{text::CCursor, text_edit::CCursorRange};
use highlighting::highlight; use highlighting::highlight;
use std::cmp::{max, min};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::ops::{Bound, RangeBounds};
pub use syntax::{Syntax, TokenType}; pub use syntax::{Syntax, TokenType};
pub use themes::ColorTheme; pub use themes::ColorTheme;
use std::cmp::{min, max};
use std::ops::{Bound, RangeBounds};
trait StringUtils { trait StringUtils {
fn substring(&self, start: usize, len: usize) -> &str; fn substring(&self, start: usize, len: usize) -> &str;
fn slice(&self, range: impl RangeBounds<usize>) -> &str; fn slice(&self, range: impl RangeBounds<usize>) -> &str;
fn char_at(&self, index: usize) -> char; fn char_at(&self, index: usize) -> char;
} }
impl StringUtils for str { impl StringUtils for str {
fn substring(&self, start: usize, len: usize) -> &str { fn substring(&self, start: usize, len: usize) -> &str {
let mut char_pos = 0; let mut char_pos = 0;
let mut byte_start = 0; let mut byte_start = 0;
let mut it = self.chars(); let mut it = self.chars();
loop { loop {
if char_pos == start { break; } if char_pos == start {
if let Some(c) = it.next() { break;
char_pos += 1; }
byte_start += c.len_utf8(); if let Some(c) = it.next() {
} char_pos += 1;
else { break; } byte_start += c.len_utf8();
} } else {
char_pos = 0; break;
let mut byte_end = byte_start; }
loop { }
if char_pos == len { break; } char_pos = 0;
if let Some(c) = it.next() { let mut byte_end = byte_start;
char_pos += 1; loop {
byte_end += c.len_utf8(); if char_pos == len {
} break;
else { break; } }
} if let Some(c) = it.next() {
&self[byte_start..byte_end] char_pos += 1;
} byte_end += c.len_utf8();
} else {
break;
}
}
&self[byte_start..byte_end]
}
fn slice(&self, range: impl RangeBounds<usize>) -> &str { fn slice(&self, range: impl RangeBounds<usize>) -> &str {
let start = match range.start_bound() { let start = match range.start_bound() {
Bound::Included(bound) | Bound::Excluded(bound) => *bound, Bound::Included(bound) | Bound::Excluded(bound) => *bound,
Bound::Unbounded => 0, Bound::Unbounded => 0,
}; };
let len = match range.end_bound() { let len = match range.end_bound() {
Bound::Included(bound) => *bound + 1, Bound::Included(bound) => *bound + 1,
Bound::Excluded(bound) => *bound, Bound::Excluded(bound) => *bound,
Bound::Unbounded => self.len(), Bound::Unbounded => self.len(),
} - start; } - start;
self.substring(start, len) self.substring(start, len)
} }
fn char_at(&self, index: usize) -> char { 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)] #[derive(Clone, Debug, PartialEq)]
/// CodeEditor struct which stores settings for highlighting. /// CodeEditor struct which stores settings for highlighting.
pub struct CodeEditor { pub struct CodeEditor {
id: String, id: String,
theme: ColorTheme, theme: ColorTheme,
syntax: Syntax, syntax: Syntax,
numlines: bool, numlines: bool,
fontsize: f32, fontsize: f32,
rows: usize, rows: usize,
vscroll: bool, vscroll: bool,
stick_to_bottom: bool, stick_to_bottom: bool,
shrink: bool, shrink: bool,
} }
impl Hash for CodeEditor { impl Hash for CodeEditor {
fn hash<H: Hasher>(&self, state: &mut H) { fn hash<H: Hasher>(&self, state: &mut H) {
self.theme.hash(state); self.theme.hash(state);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
(self.fontsize as u32).hash(state); (self.fontsize as u32).hash(state);
self.syntax.hash(state); self.syntax.hash(state);
} }
} }
impl Default for CodeEditor { impl Default for CodeEditor {
fn default() -> CodeEditor { fn default() -> CodeEditor {
CodeEditor { CodeEditor {
id: String::from("Code Editor"), id: String::from("Code Editor"),
theme: ColorTheme::GRUVBOX, theme: ColorTheme::GRUVBOX,
syntax: Syntax::rust(), syntax: Syntax::rust(),
numlines: true, numlines: true,
fontsize: 10.0, fontsize: 10.0,
rows: 10, rows: 10,
vscroll: true, vscroll: true,
stick_to_bottom: false, stick_to_bottom: false,
shrink: false, shrink: false,
} }
} }
} }
impl CodeEditor { impl CodeEditor {
pub fn id_source(self, id_source: impl Into<String>) -> Self { pub fn id_source(self, id_source: impl Into<String>) -> Self {
CodeEditor { CodeEditor {
id: id_source.into(), id: id_source.into(),
..self ..self
} }
} }
/// Minimum number of rows to show. /// Minimum number of rows to show.
/// ///
/// **Default: 10** /// **Default: 10**
pub fn with_rows(self, rows: usize) -> Self { pub fn with_rows(self, rows: usize) -> Self {
CodeEditor { rows, ..self } CodeEditor { rows, ..self }
} }
/// Use custom Color Theme /// Use custom Color Theme
/// ///
/// **Default: Gruvbox** /// **Default: Gruvbox**
pub fn with_theme(self, theme: ColorTheme) -> Self { pub fn with_theme(self, theme: ColorTheme) -> Self {
CodeEditor { theme, ..self } CodeEditor { theme, ..self }
} }
/// Use custom font size /// Use custom font size
/// ///
/// **Default: 10.0** /// **Default: 10.0**
pub fn with_fontsize(self, fontsize: f32) -> Self { pub fn with_fontsize(self, fontsize: f32) -> Self {
CodeEditor { fontsize, ..self } CodeEditor { fontsize, ..self }
} }
#[cfg(feature = "egui")] #[cfg(feature = "egui")]
/// Use UI font size /// Use UI font size
pub fn with_ui_fontsize(self, ui: &mut egui::Ui) -> Self { pub fn with_ui_fontsize(self, ui: &mut egui::Ui) -> Self {
CodeEditor { CodeEditor {
fontsize: egui::TextStyle::Monospace.resolve(ui.style()).size, fontsize: egui::TextStyle::Monospace.resolve(ui.style()).size,
..self ..self
} }
} }
/// Show or hide lines numbering /// Show or hide lines numbering
/// ///
/// **Default: true** /// **Default: true**
pub fn with_numlines(self, numlines: bool) -> Self { pub fn with_numlines(self, numlines: bool) -> Self {
CodeEditor { numlines, ..self } CodeEditor { numlines, ..self }
} }
/// Use custom syntax for highlighting /// Use custom syntax for highlighting
/// ///
/// **Default: Rust** /// **Default: Rust**
pub fn with_syntax(self, syntax: Syntax) -> Self { pub fn with_syntax(self, syntax: Syntax) -> Self {
CodeEditor { syntax, ..self } CodeEditor { syntax, ..self }
} }
/// Turn on/off scrolling on the vertical axis. /// Turn on/off scrolling on the vertical axis.
/// ///
/// **Default: true** /// **Default: true**
pub fn vscroll(self, vscroll: bool) -> Self { pub fn vscroll(self, vscroll: bool) -> Self {
CodeEditor { vscroll, ..self } CodeEditor { vscroll, ..self }
} }
/// Should the containing area shrink if the content is small? /// Should the containing area shrink if the content is small?
/// ///
/// **Default: false** /// **Default: false**
pub fn auto_shrink(self, shrink: bool) -> Self { pub fn auto_shrink(self, shrink: bool) -> Self {
CodeEditor { shrink, ..self } CodeEditor { shrink, ..self }
} }
/// Stick to bottom /// Stick to bottom
/// The scroll handle will stick to the bottom position even while the content size /// 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. /// 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" /// 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 /// 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 /// handle is dragged to the bottom it will again become stuck and remain there until manually
/// pulled from the end position. /// pulled from the end position.
/// ///
/// **Default: false** /// **Default: false**
pub fn stick_to_bottom(self, stick_to_bottom: bool) -> Self { pub fn stick_to_bottom(self, stick_to_bottom: bool) -> Self {
CodeEditor { CodeEditor {
stick_to_bottom, stick_to_bottom,
..self ..self
} }
} }
pub fn format(&self, ty: TokenType) -> egui::text::TextFormat { pub fn format(&self, ty: TokenType) -> egui::text::TextFormat {
let font_id = egui::FontId::monospace(self.fontsize); let font_id = egui::FontId::monospace(self.fontsize);
let color = self.theme.type_color(ty); let color = self.theme.type_color(ty);
egui::text::TextFormat::simple(font_id, color) egui::text::TextFormat::simple(font_id, color)
} }
fn numlines_show(&self, ui: &mut egui::Ui, text: &str) { fn numlines_show(&self, ui: &mut egui::Ui, text: &str) {
let total = if text.ends_with('\n') || text.is_empty() { let total = if text.ends_with('\n') || text.is_empty() {
text.lines().count() + 1 text.lines().count() + 1
} else { } else {
text.lines().count() text.lines().count()
} }
.max(self.rows); .max(self.rows);
let max_indent = total.to_string().len(); let max_indent = total.to_string().len();
let mut counter = (1..=total) let mut counter = (1..=total)
.map(|i| { .map(|i| {
let label = i.to_string(); let label = i.to_string();
format!( format!(
"{}{label}", "{}{label}",
" ".repeat(max_indent.saturating_sub(label.len())) " ".repeat(max_indent.saturating_sub(label.len()))
) )
}) })
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n"); .join("\n");
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
let width = max_indent as f32 * self.fontsize * 0.5; let width = max_indent as f32 * self.fontsize * 0.5;
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| { let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| {
let layout_job = egui::text::LayoutJob::single_section( let layout_job = egui::text::LayoutJob::single_section(
string.to_string(), string.to_string(),
egui::TextFormat::simple( egui::TextFormat::simple(
egui::FontId::monospace(self.fontsize), egui::FontId::monospace(self.fontsize),
self.theme.type_color(TokenType::Comment(true)), self.theme.type_color(TokenType::Comment(true)),
), ),
); );
ui.fonts(|f| f.layout_job(layout_job)) ui.fonts(|f| f.layout_job(layout_job))
}; };
ui.add( ui.add(
egui::TextEdit::multiline(&mut counter) egui::TextEdit::multiline(&mut counter)
.id_source(format!("{}_numlines", self.id)) .id_source(format!("{}_numlines", self.id))
.font(egui::TextStyle::Monospace) .font(egui::TextStyle::Monospace)
.interactive(false) .interactive(false)
.frame(false) .frame(false)
.desired_rows(self.rows) .desired_rows(self.rows)
.desired_width(width) .desired_width(width)
.layouter(&mut layouter), .layouter(&mut layouter),
); );
} }
/// Show Code Editor /// Show Code Editor
pub fn show(&mut self, ui: &mut egui::Ui, text: &mut String, saved: &mut bool, last_cursor: &mut Option<CCursorRange>, vertical_offset: &mut f32, override_cursor: Option<CCursorRange>) { pub fn show(
//let mut text_edit_output: Option<TextEditOutput> = None; &mut self,
let mut code_editor = |ui: &mut egui::Ui| { ui: &mut egui::Ui,
ui.horizontal_top(|h| { text: &mut String,
self.theme.modify_style(h, self.fontsize); saved: &mut bool,
if self.numlines { last_cursor: &mut Option<CCursorRange>,
self.numlines_show(h, text); vertical_offset: &mut f32,
} override_cursor: Option<CCursorRange>,
egui::ScrollArea::horizontal() ) {
.id_source(format!("{}_inner_scroll", self.id)) //let mut text_edit_output: Option<TextEditOutput> = None;
.show(h, |ui| { let mut code_editor = |ui: &mut egui::Ui| {
let mut layouter = |ui: &egui::Ui, string: &str, _wrap_width: f32| { ui.horizontal_top(|h| {
let layout_job = highlight(ui.ctx(), self, string); self.theme.modify_style(h, self.fontsize);
ui.fonts(|f| f.layout_job(layout_job)) 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 previous_text = text.clone();
let mut output = egui::TextEdit::multiline(text) let mut output = egui::TextEdit::multiline(text)
.id_source(&self.id) .id_source(&self.id)
.lock_focus(true) .lock_focus(true)
.desired_rows(self.rows) .desired_rows(self.rows)
.frame(true) .frame(true)
.desired_width(if self.shrink { 0.0 } else { f32::MAX }) .desired_width(if self.shrink { 0.0 } else { f32::MAX })
.layouter(&mut layouter) .layouter(&mut layouter)
.show(ui); .show(ui);
let mut get_new_cursor : bool = true; let mut get_new_cursor: bool = true;
let mut extend : isize = 0; let mut extend: isize = 0;
if output.response.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { if output.response.has_focus()
if let Some(range) = last_cursor { && ui.input(|i| i.key_pressed(egui::Key::Enter))
(*text, extend) = self.new_line(range.clone(), text.clone()); {
get_new_cursor = false; if let Some(range) = last_cursor {
} (*text, extend) = self.new_line(range.clone(), text.clone());
} get_new_cursor = false;
}
}
if output.response.has_focus() && ui.input(|i| i.key_pressed(egui::Key::E) && i.modifiers.ctrl) { if output.response.has_focus()
if let Some(range) = last_cursor { && ui.input(|i| i.key_pressed(egui::Key::E) && i.modifiers.ctrl)
(*text, extend) = self.toggle_start_of_line(range.clone(), text.clone(), "//"); {
get_new_cursor = false; if let Some(range) = last_cursor {
} (*text, extend) =
} self.toggle_start_of_line(range.clone(), text.clone(), "//");
get_new_cursor = false;
}
}
if output.response.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Tab)) { if output.response.has_focus()
if let Some(range) = last_cursor { && ui.input(|i| i.key_pressed(egui::Key::Tab))
if range.primary.index != range.secondary.index { {
(*text, extend) = self.add_start_of_line(range.clone(), previous_text.clone(), "\t"); if let Some(range) = last_cursor {
get_new_cursor = false; if range.primary.index != range.secondary.index {
} (*text, extend) = self.add_start_of_line(
} range.clone(),
} previous_text.clone(),
"\t",
);
get_new_cursor = false;
}
}
}
if output.response.has_focus() && ui.input(|i| i.key_pressed(egui::Key::Tab) && i.modifiers.shift) { if output.response.has_focus()
if let Some(range) = last_cursor { && ui.input(|i| i.key_pressed(egui::Key::Tab) && i.modifiers.shift)
if range.primary.index != range.secondary.index { {
(*text, extend) = self.remove_start_of_line(range.clone(), previous_text.clone(), "\t"); if let Some(range) = last_cursor {
get_new_cursor = false; if range.primary.index != range.secondary.index {
} (*text, extend) = self.remove_start_of_line(
} range.clone(),
} previous_text.clone(),
"\t",
);
get_new_cursor = false;
}
}
}
if override_cursor != None { if override_cursor != None {
output.response.request_focus(); output.response.request_focus();
output.state.set_ccursor_range(override_cursor); output.state.set_ccursor_range(override_cursor);
output.state.store(ui.ctx(), output.response.id); output.state.store(ui.ctx(), output.response.id);
} else if get_new_cursor { } else if get_new_cursor {
*last_cursor = output.state.clone().ccursor_range(); *last_cursor = output.state.clone().ccursor_range();
} else { } else {
if let Some(cursor_range) = last_cursor.clone() { if let Some(cursor_range) = last_cursor.clone() {
let mut start = min(cursor_range.primary.index, cursor_range.secondary.index); let mut start =
let end = max(cursor_range.primary.index, cursor_range.secondary.index); min(cursor_range.primary.index, cursor_range.secondary.index);
let extended = match end as isize + extend { let end =
// Check for overflow or negative result max(cursor_range.primary.index, cursor_range.secondary.index);
value if value < 0 => 0, let extended = match end as isize + extend {
value => value as usize, // Check for overflow or negative result
}; value if value < 0 => 0,
if start == end { value => value as usize,
start = extended; };
} if start == end {
let cursor = Some(CCursorRange { start = extended;
primary : CCursor::new(start.clone()), }
secondary : CCursor::new(max(start.clone(), extended)), let cursor = Some(CCursorRange {
}); primary: CCursor::new(start.clone()),
output.state.set_ccursor_range(cursor.clone()); secondary: CCursor::new(max(start.clone(), extended)),
output.state.store(ui.ctx(), output.response.id); });
*last_cursor = cursor.clone(); output.state.set_ccursor_range(cursor.clone());
} output.state.store(ui.ctx(), output.response.id);
} *last_cursor = cursor.clone();
}
if previous_text != text.clone() { }
*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)
.vertical_scroll_offset(vertical_offset.clone())
.show(ui, code_editor);
*vertical_offset = scroll_area.state.offset.y.clone();
} else {
code_editor(ui);
}
//text_edit_output.expect("TextEditOutput should exist at this point") if previous_text != text.clone() {
} *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)
.vertical_scroll_offset(vertical_offset.clone())
.show(ui, code_editor);
*vertical_offset = scroll_area.state.offset.y.clone();
} else {
code_editor(ui);
}
fn toggle_start_of_line(&self, cursor_range : CCursorRange, text : String, head : &str) -> (String, isize) { //text_edit_output.expect("TextEditOutput should exist at this point")
let mut substring = self.get_selection_substring(text.clone(), cursor_range.clone()).clone(); }
let mut new_text : String = "".into();
let extend : isize;
if substring[1].contains(head) {
extend = - self.delta_char(substring[1].clone(), head);
substring[1] = substring[1].replace(&format!("\n{}", head), &"\n".to_string());
} else {
extend = self.delta_char(substring[1].clone(), head);
substring[1] = substring[1].replace(&"\n".to_string(), &format!("\n{}", head));
}
new_text.push_str(&substring[0].clone());
new_text.push_str(&substring[1].clone());
new_text.push_str(&substring[2].clone());
return (new_text, extend) 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();
let mut new_text: String = "".into();
let extend: isize;
if substring[1].contains(head) {
extend = -self.delta_char(substring[1].clone(), head);
substring[1] = substring[1].replace(&format!("\n{}", head), &"\n".to_string());
} else {
extend = self.delta_char(substring[1].clone(), head);
substring[1] = substring[1].replace(&"\n".to_string(), &format!("\n{}", head));
}
new_text.push_str(&substring[0].clone());
new_text.push_str(&substring[1].clone());
new_text.push_str(&substring[2].clone());
fn add_start_of_line(&self, cursor_range : CCursorRange, text : String, head : &str) -> (String, isize) { return (new_text, extend);
let mut substring = self.get_selection_substring(text.clone(), cursor_range.clone()).clone(); }
let mut new_text : String = "".into();
let extend : isize = self.delta_char(substring[1].clone(), head); fn add_start_of_line(
substring[1] = substring[1].replace(&"\n".to_string(), &format!("\n{}", head)); &self,
cursor_range: CCursorRange,
new_text.push_str(&substring[0].clone()); text: String,
new_text.push_str(&substring[1].clone()); head: &str,
new_text.push_str(&substring[2].clone()); ) -> (String, isize) {
let mut substring = self
.get_selection_substring(text.clone(), cursor_range.clone())
.clone();
let mut new_text: String = "".into();
return (new_text, extend) let extend: isize = self.delta_char(substring[1].clone(), head);
} substring[1] = substring[1].replace(&"\n".to_string(), &format!("\n{}", head));
fn remove_start_of_line(&self, cursor_range : CCursorRange, text : String, head : &str) -> (String, isize) { new_text.push_str(&substring[0].clone());
let mut substring = self.get_selection_substring(text.clone(), cursor_range.clone()).clone(); new_text.push_str(&substring[1].clone());
let mut new_text : String = "".into(); new_text.push_str(&substring[2].clone());
let extend : isize = - self.delta_char(substring[1].clone(), head);
substring[1] = substring[1].replace(&format!("\n{}", head), &"\n".to_string());
new_text.push_str(&substring[0].clone());
new_text.push_str(&substring[1].clone());
new_text.push_str(&substring[2].clone());
return (new_text, extend) return (new_text, extend);
} }
fn get_selection_substring(&self, text : String, cursor_range : CCursorRange) -> Vec<String> { fn remove_start_of_line(
let start = min(cursor_range.primary.index, cursor_range.secondary.index); &self,
let end = max(cursor_range.primary.index, cursor_range.secondary.index); cursor_range: CCursorRange,
text: String,
head: &str,
) -> (String, isize) {
let mut substring = self
.get_selection_substring(text.clone(), cursor_range.clone())
.clone();
let mut new_text: String = "".into();
let mut first_char = max(0, start - 1); let extend: isize = -self.delta_char(substring[1].clone(), head);
substring[1] = substring[1].replace(&format!("\n{}", head), &"\n".to_string());
while first_char > 0 && text.char_at(first_char) != '\n' { new_text.push_str(&substring[0].clone());
first_char -= 1; new_text.push_str(&substring[1].clone());
} new_text.push_str(&substring[2].clone());
let last_char = end; return (new_text, extend);
}
return vec![text.slice(..first_char).to_string(), text.slice(first_char..last_char).to_string(), text.slice(last_char..).to_string()];
}
fn delta_char(&self, text : String, modifier: &str) -> isize { fn get_selection_substring(&self, text: String, cursor_range: CCursorRange) -> Vec<String> {
(modifier.len() * text.match_indices(&"\n".to_string()).collect::<Vec<_>>().len()) as isize let start = min(cursor_range.primary.index, cursor_range.secondary.index);
} let end = max(cursor_range.primary.index, cursor_range.secondary.index);
fn new_line(&self, cursor_range : CCursorRange, text : String) -> (String, isize) { let mut first_char = max(0, start - 1);
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 = cursor - 1; while first_char > 0 && text.char_at(first_char) != '\n' {
first_char -= 1;
}
while last_line_break > 0 && text.char_at(last_line_break) != '\n' { let last_char = end;
last_line_break -= 1;
}
let indent_depth = text.slice(last_line_break..cursor).match_indices(&"\t".to_string()).collect::<Vec<_>>().len();
let new_indent_depth = indent_depth.clone(); return vec![
text.slice(..first_char).to_string(),
let mut new_text : String = text.clone().slice(..(cursor + 1)).to_string(); text.slice(first_char..last_char).to_string(),
new_text.push_str(&"\t".repeat(new_indent_depth.clone())); text.slice(last_char..).to_string(),
new_text.push_str(text.clone().slice((cursor + 1)..)); ];
}
return (new_text.clone().to_string(), (new_indent_depth + 1) as isize); fn delta_char(&self, text: String, modifier: &str) -> isize {
} (modifier.len()
* text
.match_indices(&"\n".to_string())
.collect::<Vec<_>>()
.len()) as isize
}
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 = cursor - 1;
while last_line_break > 0 && text.char_at(last_line_break) != '\n' {
last_line_break -= 1;
}
let indent_depth = text
.slice(last_line_break..cursor)
.match_indices(&"\t".to_string())
.collect::<Vec<_>>()
.len();
let new_indent_depth = indent_depth.clone();
let mut new_text: String = text.clone().slice(..(cursor + 1)).to_string();
new_text.push_str(&"\t".repeat(new_indent_depth.clone()));
new_text.push_str(text.clone().slice((cursor + 1)..));
return (
new_text.clone().to_string(),
(new_indent_depth + 1) as isize,
);
}
} }

View file

@ -12,7 +12,7 @@ impl ColorTheme {
comments: "#656565", // dark_gray comments: "#656565", // dark_gray
functions: "#ffad69", // light orange functions: "#ffad69", // light orange
keywords: "#48b1a7", // mid green keywords: "#48b1a7", // mid green
literals: "#d2d2d3", // literals: "#d2d2d3", //
numerics: "#ff7b4f", // orange numerics: "#ff7b4f", // orange
punctuation: "#989898", // gray punctuation: "#989898", // gray
strs: "#cbd5a1", // light_green strs: "#cbd5a1", // light_green
@ -20,7 +20,7 @@ impl ColorTheme {
special: "#48b1a7", // mid green special: "#48b1a7", // mid green
}; };
pub const ASH: ColorTheme = ColorTheme { pub const ASH: ColorTheme = ColorTheme {
name: "Ash", name: "Ash",
dark: true, dark: true,
bg: "#101010", bg: "#101010",

View file

@ -1,9 +1,9 @@
#![allow(dead_code)] #![allow(dead_code)]
pub mod ayu; pub mod ayu;
pub mod fantasy;
pub mod github; pub mod github;
pub mod gruvbox; pub mod gruvbox;
pub mod sonokai; pub mod sonokai;
pub mod fantasy;
use super::syntax::TokenType; use super::syntax::TokenType;
@ -19,8 +19,8 @@ pub const DEFAULT_THEMES: [ColorTheme; 7] = [
ColorTheme::GITHUB_DARK, ColorTheme::GITHUB_DARK,
ColorTheme::GRUVBOX, ColorTheme::GRUVBOX,
ColorTheme::SONOKAI, ColorTheme::SONOKAI,
ColorTheme::FIRE, ColorTheme::FIRE,
ColorTheme::ASH, ColorTheme::ASH,
]; ];
fn color_from_hex(hex: &str) -> Option<Color32> { fn color_from_hex(hex: &str) -> Option<Color32> {

View file

@ -1,22 +1,22 @@
use super::ColorTheme; use super::ColorTheme;
impl ColorTheme { impl ColorTheme {
/// Original Author: sainnhe <https://github.com/sainnhe/sonokai> /// Original Author: sainnhe <https://github.com/sainnhe/sonokai>
/// Modified by p4ymak <https://github.com/p4ymak> /// Modified by p4ymak <https://github.com/p4ymak>
pub const SONOKAI: ColorTheme = ColorTheme { pub const SONOKAI: ColorTheme = ColorTheme {
name: "Sonokai", name: "Sonokai",
dark: true, dark: true,
bg: "#2c2e34", // bg0 bg: "#2c2e34", // bg0
cursor: "#76cce0", // blue cursor: "#76cce0", // blue
selection: "#444852", // bg5 selection: "#444852", // bg5
comments: "#7f8490", // gray comments: "#7f8490", // gray
functions: "#9ed072", // green functions: "#9ed072", // green
keywords: "#fc5d7c", // red keywords: "#fc5d7c", // red
literals: "#e2e2e3", // foreground literals: "#e2e2e3", // foreground
numerics: "#b39df3", // purple numerics: "#b39df3", // purple
punctuation: "#7f8490", // gray punctuation: "#7f8490", // gray
strs: "#e7c664", // yellow strs: "#e7c664", // yellow
types: "#399ee6", // blue types: "#399ee6", // blue
special: "#f39660", // orange special: "#f39660", // orange
}; };
} }

View file

@ -1,236 +1,247 @@
mod tools;
mod calcifer; mod calcifer;
mod tools;
use eframe::egui;
use calcifer::code_editor::ColorTheme; use calcifer::code_editor::ColorTheme;
use std::{path::Path, sync::Arc, time, thread, ops::Range}; use eframe::egui;
use egui::FontFamily::Proportional; use egui::FontFamily::Proportional;
use egui::FontId; use egui::FontId;
use egui::TextStyle::{Small, Button, Body, Heading, Monospace}; use egui::TextStyle::{Body, Button, Heading, Monospace, Small};
use std::{ops::Range, path::Path, sync::Arc, thread, time};
use calcifer::code_editor::themes::DEFAULT_THEMES; use calcifer::code_editor::themes::DEFAULT_THEMES;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
mod build { mod build {
pub const SAVE_PATH : &str = "/home/penwing/Documents/.save/debug/calcifer_save.json"; pub const SAVE_PATH: &str = "/home/penwing/Documents/.save/debug/calcifer_save.json";
pub const TITLE: &str = " debug"; pub const TITLE: &str = " debug";
} }
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
mod build { mod build {
pub const SAVE_PATH : &str = "/home/penwing/Documents/.save/calcifer_save.json"; pub const SAVE_PATH: &str = "/home/penwing/Documents/.save/calcifer_save.json";
pub const TITLE: &str = ""; pub const TITLE: &str = "";
} }
use build::SAVE_PATH; use build::SAVE_PATH;
use build::TITLE; use build::TITLE;
const TERMINAL_HEIGHT : f32 = 200.0; const TERMINAL_HEIGHT: f32 = 200.0;
const TERMINAL_RANGE : Range<f32> = 100.0..500.0; const TERMINAL_RANGE: Range<f32> = 100.0..500.0;
const RED : egui::Color32 = egui::Color32::from_rgb(235, 108, 99); const RED: egui::Color32 = egui::Color32::from_rgb(235, 108, 99);
const TIME_LABELS : [&str; 7] = ["input", "settings", "tree", "terminal", "tabs", "content", "windows"]; const TIME_LABELS: [&str; 7] = [
const MAX_FPS : f32 = 30.0; "input", "settings", "tree", "terminal", "tabs", "content", "windows",
const PATH_ROOT : &str = "/home/penwing/Documents/"; ];
const DISPLAY_PATH_DEPTH : usize = 3; const MAX_FPS: f32 = 30.0;
const MAX_TABS : usize = 20; const PATH_ROOT: &str = "/home/penwing/Documents/";
const DISPLAY_PATH_DEPTH: usize = 3;
const MAX_TABS: usize = 20;
fn main() -> Result<(), eframe::Error> { fn main() -> Result<(), eframe::Error> {
let icon_data = tools::load_icon(); let icon_data = tools::load_icon();
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()
.with_inner_size([1200.0, 800.0]) .with_inner_size([1200.0, 800.0])
.with_icon(Arc::new(icon_data)), .with_icon(Arc::new(icon_data)),
..Default::default() ..Default::default()
}; };
let app_state: tools::AppState;
// Attempt to load previous state
if Path::new(SAVE_PATH).exists() {
app_state = tools::load_state(SAVE_PATH).expect("Failed to load the save");
} else {
app_state = tools::AppState {
tabs: vec![],
theme: 0,
};
}
eframe::run_native( let app_state: tools::AppState;
&format!("Calcifer{}{}", tools::version(), TITLE), // Attempt to load previous state
options, if Path::new(SAVE_PATH).exists() {
Box::new(move |_cc| Box::from(Calcifer::from_app_state(app_state))), app_state = tools::load_state(SAVE_PATH).expect("Failed to load the save");
) } else {
app_state = tools::AppState {
tabs: vec![],
theme: 0,
};
}
eframe::run_native(
&format!("Calcifer{}{}", tools::version(), TITLE),
options,
Box::new(move |_cc| Box::from(Calcifer::from_app_state(app_state))),
)
} }
struct Calcifer { struct Calcifer {
selected_tab : tools::TabNumber, selected_tab: tools::TabNumber,
tabs: Vec<tools::Tab>, tabs: Vec<tools::Tab>,
command: String, command: String,
command_history: Vec<tools::CommandEntry>, command_history: Vec<tools::CommandEntry>,
theme: ColorTheme, theme: ColorTheme,
font_size: f32, font_size: f32,
tree_visible: bool, tree_visible: bool,
profiler_visible: bool, profiler_visible: bool,
terminal_visible: bool, terminal_visible: bool,
close_tab_confirm: tools::confirm::ConfirmWindow, close_tab_confirm: tools::confirm::ConfirmWindow,
tab_to_close: usize, tab_to_close: usize,
refresh_confirm: tools::confirm::ConfirmWindow, refresh_confirm: tools::confirm::ConfirmWindow,
exit_confirm: tools::confirm::ConfirmWindow, exit_confirm: tools::confirm::ConfirmWindow,
search_menu: tools::search::SearchWindow, search_menu: tools::search::SearchWindow,
settings_menu: tools::settings::SettingsWindow, settings_menu: tools::settings::SettingsWindow,
shortcuts_menu: tools::shortcuts::ShortcutsWindow, shortcuts_menu: tools::shortcuts::ShortcutsWindow,
time_watch: Vec<f32>, time_watch: Vec<f32>,
next_frame: time::Instant, next_frame: time::Instant,
} }
impl Default for Calcifer { impl Default for Calcifer {
fn default() -> Self { fn default() -> Self {
Self { Self {
selected_tab: tools::TabNumber::from_index(0), selected_tab: tools::TabNumber::from_index(0),
tabs: vec![tools::Tab::default()], tabs: vec![tools::Tab::default()],
command: String::new(), command: String::new(),
command_history: Vec::new(), command_history: Vec::new(),
theme: DEFAULT_THEMES[0], theme: DEFAULT_THEMES[0],
font_size: 14.0, font_size: 14.0,
tree_visible: false, tree_visible: false,
profiler_visible: false, profiler_visible: false,
terminal_visible: false, terminal_visible: false,
close_tab_confirm: tools::confirm::ConfirmWindow::new("You have some unsaved changes, Do you still want to close this document ?", "Confirm Close"), close_tab_confirm: tools::confirm::ConfirmWindow::new(
tab_to_close: 0, "You have some unsaved changes, Do you still want to close this document ?",
refresh_confirm: tools::confirm::ConfirmWindow::new("You have some unsaved changes, Do you still want to refresh this document ?", "Confirm Refresh"), "Confirm Close",
exit_confirm: tools::confirm::ConfirmWindow::new("", "Confirm Exit"), ),
tab_to_close: 0,
search_menu: tools::search::SearchWindow::default(), refresh_confirm: tools::confirm::ConfirmWindow::new(
settings_menu: tools::settings::SettingsWindow::new(DEFAULT_THEMES[0]), "You have some unsaved changes, Do you still want to refresh this document ?",
shortcuts_menu: tools::shortcuts::ShortcutsWindow::new(), "Confirm Refresh",
),
time_watch: vec![0.0; TIME_LABELS.len()], exit_confirm: tools::confirm::ConfirmWindow::new("", "Confirm Exit"),
next_frame: time::Instant::now(),
} search_menu: tools::search::SearchWindow::default(),
} settings_menu: tools::settings::SettingsWindow::new(DEFAULT_THEMES[0]),
shortcuts_menu: tools::shortcuts::ShortcutsWindow::new(),
time_watch: vec![0.0; TIME_LABELS.len()],
next_frame: time::Instant::now(),
}
}
} }
impl eframe::App for Calcifer { impl eframe::App for Calcifer {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
thread::sleep(time::Duration::from_secs_f32(((1.0/MAX_FPS) - self.next_frame.elapsed().as_secs_f32()).max(0.0))); thread::sleep(time::Duration::from_secs_f32(
self.next_frame = time::Instant::now(); ((1.0 / MAX_FPS) - self.next_frame.elapsed().as_secs_f32()).max(0.0),
));
let mut watch = time::Instant::now(); self.next_frame = time::Instant::now();
let mut style = (*ctx.style()).clone();
style.text_styles = [
(Heading, FontId::new(self.font_size * 1.6, Proportional)),
(Body, FontId::new(self.font_size, Proportional)),
(Monospace, FontId::new(self.font_size, Proportional)),
(Button, FontId::new(self.font_size, Proportional)),
(Small, FontId::new(self.font_size, Proportional)),
]
.into();
ctx.set_style(style);
if ctx.input( |i| i.key_pressed(egui::Key::R) && i.modifiers.ctrl) && !self.refresh_confirm.visible {
if self.tabs[self.selected_tab.to_index()].saved {
self.tabs[self.selected_tab.to_index()].refresh();
} else {
self.refresh_confirm.ask();
}
}
if ctx.input( |i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
self.handle_save_file(self.save_tab());
}
if ctx.input( |i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl && i.modifiers.shift) {
self.handle_save_file(self.save_tab_as());
}
if ctx.input( |i| i.key_pressed(egui::Key::ArrowLeft) && i.modifiers.alt) {
self.move_through_tabs(false);
}
if ctx.input( |i| i.key_pressed(egui::Key::ArrowRight) && i.modifiers.alt) {
self.move_through_tabs(true);
}
if ctx.input( |i| i.zoom_delta() > 1.0) {
self.font_size = (self.font_size * 1.1).min(30.0);
}
if ctx.input( |i| i.zoom_delta() < 1.0) {
self.font_size = (self.font_size / 1.1).max(10.0);
}
if ctx.input( |i| i.key_pressed(egui::Key::F) && i.modifiers.ctrl) { let mut watch = time::Instant::now();
self.search_menu.visible = !self.search_menu.visible.clone();
self.search_menu.initialized = !self.search_menu.visible.clone(); let mut style = (*ctx.style()).clone();
} style.text_styles = [
(Heading, FontId::new(self.font_size * 1.6, Proportional)),
if ctx.input(|i| i.viewport().close_requested()) { (Body, FontId::new(self.font_size, Proportional)),
let mut unsaved_tabs : Vec<usize> = vec![]; (Monospace, FontId::new(self.font_size, Proportional)),
for (index, tab) in self.tabs.iter().enumerate() { (Button, FontId::new(self.font_size, Proportional)),
if !tab.saved { (Small, FontId::new(self.font_size, Proportional)),
unsaved_tabs.push(index); ]
} .into();
} ctx.set_style(style);
if unsaved_tabs.len() > 0 {
let mut unsaved_tabs_names : String = "".to_string(); if ctx.input(|i| i.key_pressed(egui::Key::R) && i.modifiers.ctrl)
for index in unsaved_tabs.iter() { && !self.refresh_confirm.visible
unsaved_tabs_names.push_str(&self.tabs[*index].get_name()); {
} if self.tabs[self.selected_tab.to_index()].saved {
egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::CancelClose); self.tabs[self.selected_tab.to_index()].refresh();
self.exit_confirm.prompt = format!("You have some unsaved changes :\n{}\nDo you still want to exit ?", unsaved_tabs_names); } else {
self.exit_confirm.ask(); self.refresh_confirm.ask();
} }
} }
self.time_watch[0] = watch.elapsed().as_micros() as f32 / 1000.0; if ctx.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
watch = time::Instant::now(); self.handle_save_file(self.save_tab());
}
self.draw_settings(ctx);
if ctx.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl && i.modifiers.shift) {
self.time_watch[1] = watch.elapsed().as_micros() as f32 / 1000.0; self.handle_save_file(self.save_tab_as());
watch = time::Instant::now(); }
self.draw_tree_panel(ctx); if ctx.input(|i| i.key_pressed(egui::Key::ArrowLeft) && i.modifiers.alt) {
self.move_through_tabs(false);
self.time_watch[2] = watch.elapsed().as_micros() as f32 / 1000.0; }
watch = time::Instant::now();
if ctx.input(|i| i.key_pressed(egui::Key::ArrowRight) && i.modifiers.alt) {
self.draw_bottom_tray(ctx); self.move_through_tabs(true);
self.draw_terminal_panel(ctx); }
self.time_watch[3] = watch.elapsed().as_micros() as f32 / 1000.0; if ctx.input(|i| i.zoom_delta() > 1.0) {
watch = time::Instant::now(); self.font_size = (self.font_size * 1.1).min(30.0);
}
self.draw_tab_panel(ctx);
if ctx.input(|i| i.zoom_delta() < 1.0) {
self.time_watch[4] = watch.elapsed().as_micros() as f32 / 1000.0; self.font_size = (self.font_size / 1.1).max(10.0);
watch = time::Instant::now(); }
self.draw_content_panel(ctx); if ctx.input(|i| i.key_pressed(egui::Key::F) && i.modifiers.ctrl) {
self.search_menu.visible = !self.search_menu.visible.clone();
self.time_watch[5] = watch.elapsed().as_micros() as f32 / 1000.0; self.search_menu.initialized = !self.search_menu.visible.clone();
watch = time::Instant::now(); }
self.draw_windows(ctx); if ctx.input(|i| i.viewport().close_requested()) {
let mut unsaved_tabs: Vec<usize> = vec![];
self.time_watch[6] = watch.elapsed().as_micros() as f32 / 1000.0; for (index, tab) in self.tabs.iter().enumerate() {
} if !tab.saved {
unsaved_tabs.push(index);
fn on_exit(&mut self, _gl : std::option::Option<&eframe::glow::Context>) { }
self.save_state(); }
} if unsaved_tabs.len() > 0 {
let mut unsaved_tabs_names: String = "".to_string();
for index in unsaved_tabs.iter() {
unsaved_tabs_names.push_str(&self.tabs[*index].get_name());
}
egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::CancelClose);
self.exit_confirm.prompt = format!(
"You have some unsaved changes :\n{}\nDo you still want to exit ?",
unsaved_tabs_names
);
self.exit_confirm.ask();
}
}
self.time_watch[0] = watch.elapsed().as_micros() as f32 / 1000.0;
watch = time::Instant::now();
self.draw_settings(ctx);
self.time_watch[1] = watch.elapsed().as_micros() as f32 / 1000.0;
watch = time::Instant::now();
self.draw_tree_panel(ctx);
self.time_watch[2] = watch.elapsed().as_micros() as f32 / 1000.0;
watch = time::Instant::now();
self.draw_bottom_tray(ctx);
self.draw_terminal_panel(ctx);
self.time_watch[3] = watch.elapsed().as_micros() as f32 / 1000.0;
watch = time::Instant::now();
self.draw_tab_panel(ctx);
self.time_watch[4] = watch.elapsed().as_micros() as f32 / 1000.0;
watch = time::Instant::now();
self.draw_content_panel(ctx);
self.time_watch[5] = watch.elapsed().as_micros() as f32 / 1000.0;
watch = time::Instant::now();
self.draw_windows(ctx);
self.time_watch[6] = watch.elapsed().as_micros() as f32 / 1000.0;
}
fn on_exit(&mut self, _gl: std::option::Option<&eframe::glow::Context>) {
self.save_state();
}
} }

View file

@ -1,58 +1,53 @@
use eframe::egui; use eframe::egui;
pub struct ConfirmWindow { pub struct ConfirmWindow {
pub visible: bool, pub visible: bool,
pub proceed: bool, pub proceed: bool,
pub prompt: String, pub prompt: String,
id: String, id: String,
} }
impl ConfirmWindow { impl ConfirmWindow {
pub fn new(prompt: &str, id: &str) -> Self { pub fn new(prompt: &str, id: &str) -> Self {
Self { Self {
visible: false, visible: false,
proceed: false, proceed: false,
prompt: prompt.to_string(), prompt: prompt.to_string(),
id: id.to_string(), id: id.to_string(),
} }
} }
pub fn show(&mut self, ctx: &egui::Context) {
pub fn show(&mut self, ctx: &egui::Context) { let mut visible = self.visible.clone();
let mut visible = self.visible.clone(); egui::Window::new(self.id.clone())
egui::Window::new(self.id.clone()) .open(&mut visible)
.open(&mut visible) .vscroll(true)
.vscroll(true) .hscroll(true)
.hscroll(true) .show(ctx, |ui| self.ui(ui));
.show(ctx, |ui| self.ui(ui)); self.visible = self.visible.clone() && visible;
self.visible = self.visible.clone() && visible; }
}
fn ui(&mut self, ui: &mut egui::Ui) {
ui.set_min_width(250.0);
fn ui(&mut self, ui: &mut egui::Ui) { ui.label(self.prompt.clone());
ui.set_min_width(250.0); ui.vertical_centered(|ui| {
ui.label(self.prompt.clone()); if ui.add(egui::Button::new("Yes")).clicked() {
ui.vertical_centered(|ui| { self.proceed = true;
if ui.add(egui::Button::new("Yes")).clicked() { }
self.proceed = true;
} if ui.add(egui::Button::new("No")).clicked() {
self.visible = false;
if ui.add(egui::Button::new("No")).clicked() { }
self.visible = false; });
} }
});
} pub fn ask(&mut self) {
self.visible = true;
pub fn ask(&mut self) { self.proceed = false;
self.visible = true; }
self.proceed = false;
} pub fn close(&mut self) {
self.visible = false;
self.proceed = false;
pub fn close(&mut self) { }
self.visible = false; }
self.proceed = false;
}
}

View file

@ -1,13 +1,16 @@
use std::{cmp::Ordering, path::PathBuf, path::Path, fs, fs::read_to_string, io::Write, path::Component, ffi::OsStr, fs::OpenOptions};
use crate::calcifer::code_editor::Syntax; use crate::calcifer::code_editor::Syntax;
use eframe::egui;
use serde::{Serialize, Deserialize};
use crate::DISPLAY_PATH_DEPTH; use crate::DISPLAY_PATH_DEPTH;
use eframe::egui;
use serde::{Deserialize, Serialize};
use std::{
cmp::Ordering, ffi::OsStr, fs, fs::read_to_string, fs::OpenOptions, io::Write, path::Component,
path::Path, path::PathBuf,
};
use toml::Value; use toml::Value;
//my tools; //my tools;
pub mod search;
pub mod confirm; pub mod confirm;
pub mod search;
pub mod settings; pub mod settings;
pub mod shortcuts; pub mod shortcuts;
@ -17,122 +20,122 @@ pub use terminal::*;
pub mod tabs; pub mod tabs;
pub use tabs::*; pub use tabs::*;
#[derive(Serialize, Deserialize, Debug, PartialEq)] #[derive(Serialize, Deserialize, Debug, PartialEq)]
pub struct AppState { pub struct AppState {
pub tabs: Vec<PathBuf>, pub tabs: Vec<PathBuf>,
pub theme: usize, pub theme: usize,
} }
pub fn save_state(state: &AppState, file_path: &str) -> Result<(), std::io::Error> { pub fn save_state(state: &AppState, file_path: &str) -> Result<(), std::io::Error> {
let serialized_state = serde_json::to_string(state)?; let serialized_state = serde_json::to_string(state)?;
if let Some(parent_dir) = Path::new(file_path).parent() {
fs::create_dir_all(parent_dir)?;
}
let mut file = OpenOptions::new() if let Some(parent_dir) = Path::new(file_path).parent() {
.write(true) fs::create_dir_all(parent_dir)?;
.create(true) }
.truncate(true)
.open(file_path)?;
file.write_all(serialized_state.as_bytes())?; let mut file = OpenOptions::new()
.write(true)
println!("Saved state at {}", file_path); .create(true)
.truncate(true)
.open(file_path)?;
Ok(()) file.write_all(serialized_state.as_bytes())?;
println!("Saved state at {}", file_path);
Ok(())
} }
pub fn load_state(file_path: &str) -> Result<AppState, std::io::Error> { pub fn load_state(file_path: &str) -> Result<AppState, std::io::Error> {
let serialized_state = read_to_string(file_path)?; let serialized_state = read_to_string(file_path)?;
Ok(serde_json::from_str(&serialized_state)?) Ok(serde_json::from_str(&serialized_state)?)
} }
pub fn load_icon() -> egui::IconData { pub fn load_icon() -> egui::IconData {
let (icon_rgba, icon_width, icon_height) = { let (icon_rgba, icon_width, icon_height) = {
let icon = include_bytes!("../../assets/icon.png"); let icon = include_bytes!("../../assets/icon.png");
let image = image::load_from_memory(icon) let image = image::load_from_memory(icon)
.expect("Failed to open icon path") .expect("Failed to open icon path")
.into_rgba8(); .into_rgba8();
let (width, height) = image.dimensions(); let (width, height) = image.dimensions();
let rgba = image.into_raw(); let rgba = image.into_raw();
(rgba, width, height) (rgba, width, height)
}; };
egui::IconData { egui::IconData {
rgba: icon_rgba, rgba: icon_rgba,
width: icon_width, width: icon_width,
height: icon_height, height: icon_height,
} }
} }
pub fn to_syntax(language: &str) -> Syntax {
pub fn to_syntax(language : &str) -> Syntax { match language {
match language { "py" => Syntax::python(),
"py" => Syntax::python(), "rs" => Syntax::rust(),
"rs" => Syntax::rust(), _ => Syntax::shell(),
_ => Syntax::shell(), }
}
} }
pub fn sort_directories_first(a: &std::fs::DirEntry, b: &std::fs::DirEntry) -> Ordering { pub fn sort_directories_first(a: &std::fs::DirEntry, b: &std::fs::DirEntry) -> Ordering {
let a_is_dir = a.path().is_dir(); let a_is_dir = a.path().is_dir();
let b_is_dir = b.path().is_dir(); let b_is_dir = b.path().is_dir();
// Directories come first, then files // Directories come first, then files
if a_is_dir && !b_is_dir { if a_is_dir && !b_is_dir {
Ordering::Less Ordering::Less
} else if !a_is_dir && b_is_dir { } else if !a_is_dir && b_is_dir {
Ordering::Greater Ordering::Greater
} else { } else {
// Both are either directories or files, sort alphabetically // Both are either directories or files, sort alphabetically
a.path().cmp(&b.path()) a.path().cmp(&b.path())
} }
} }
pub fn format_path(path: &Path) -> String { pub fn format_path(path: &Path) -> String {
let components: Vec<&OsStr> = path let components: Vec<&OsStr> = path
.components() .components()
.rev() .rev()
.take(DISPLAY_PATH_DEPTH) .take(DISPLAY_PATH_DEPTH)
.filter_map(|component| match component { .filter_map(|component| match component {
Component::RootDir | Component::CurDir => None, Component::RootDir | Component::CurDir => None,
_ => Some(component.as_os_str()), _ => Some(component.as_os_str()),
}) })
.collect(); .collect();
format!("{}>", components.iter().rev().map(|&c| c.to_string_lossy()).collect::<Vec<_>>().join("/")) format!(
"{}>",
components
.iter()
.rev()
.map(|&c| c.to_string_lossy())
.collect::<Vec<_>>()
.join("/")
)
} }
pub fn version() -> String { pub fn version() -> String {
// Read the contents of the Cargo.toml file // Read the contents of the Cargo.toml file
if !Path::new("Cargo.toml").exists() { if !Path::new("Cargo.toml").exists() {
return "".to_string() return "".to_string();
} }
let toml_content = fs::read_to_string("Cargo.toml").expect("Failed to read Cargo.toml"); let toml_content = fs::read_to_string("Cargo.toml").expect("Failed to read Cargo.toml");
// Parse the TOML content // Parse the TOML content
let toml: Value = toml::from_str(&toml_content).expect("Failed to parse TOML"); let toml: Value = toml::from_str(&toml_content).expect("Failed to parse TOML");
// Extract version information // Extract version information
if let Some(package) = toml.get("package") { if let Some(package) = toml.get("package") {
if let Some(version) = package.get("version") { if let Some(version) = package.get("version") {
if let Some(version_string) = version.as_str() { if let Some(version_string) = version.as_str() {
println!("Version: {}", version_string); println!("Version: {}", version_string);
return format!(" v{}", version_string) return format!(" v{}", version_string);
} }
} }
} }
return "".to_string() return "".to_string();
} }
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;

View file

@ -1,237 +1,254 @@
use std::{cmp::min};
use eframe::egui; use eframe::egui;
use std::cmp::min;
use crate::RED;
use crate::tools::{tabs::Tab, tabs::TabNumber}; use crate::tools::{tabs::Tab, tabs::TabNumber};
use crate::RED;
enum Action { enum Action {
Next, Next,
Previous, Previous,
Replace, Replace,
Update, Update,
None, None,
} }
#[derive(Clone)] #[derive(Clone)]
pub struct Selection { pub struct Selection {
pub tab: TabNumber, pub tab: TabNumber,
pub start: usize, pub start: usize,
pub end: usize, pub end: usize,
} }
impl Default for Selection { impl Default for Selection {
fn default() -> Self { fn default() -> Self {
Self { Self {
tab: TabNumber::from_index(0), tab: TabNumber::from_index(0),
start: 0, start: 0,
end: 0, end: 0,
} }
} }
} }
pub struct SearchWindow { pub struct SearchWindow {
pub visible: bool, pub visible: bool,
search_text: String,
searched_text: String,
replace_text: String,
pub initialized: bool, search_text: String,
searched_text: String,
replace_text: String,
across_documents: bool, pub initialized: bool,
results: Vec<Selection>, across_documents: bool,
current_result: usize,
pub result_selected: bool, results: Vec<Selection>,
current_result: usize,
row_height: f32,
pub result_selected: bool,
row_height: f32,
} }
impl Default for SearchWindow { impl Default for SearchWindow {
fn default() -> Self { fn default() -> Self {
Self { Self {
visible: false, visible: false,
search_text: "".into(),
searched_text: "".into(),
replace_text: "".into(),
initialized: false,
across_documents: false, search_text: "".into(),
searched_text: "".into(),
replace_text: "".into(),
results: vec![], initialized: false,
current_result: 0,
result_selected: true, across_documents: false,
row_height: 0.0, results: vec![],
} current_result: 0,
}
result_selected: true,
row_height: 0.0,
}
}
} }
impl SearchWindow { impl SearchWindow {
pub fn show(&mut self, ctx: &egui::Context, tabs: &mut Vec<Tab>, selected_tab: &mut TabNumber) { pub fn show(&mut self, ctx: &egui::Context, tabs: &mut Vec<Tab>, selected_tab: &mut TabNumber) {
let mut visible = self.visible.clone(); let mut visible = self.visible.clone();
egui::Window::new("Search") egui::Window::new("Search")
.open(&mut visible) //I want it to be able to change its visibility (if user close manually) .open(&mut visible) //I want it to be able to change its visibility (if user close manually)
.vscroll(true) .vscroll(true)
.hscroll(true) .hscroll(true)
.show(ctx, |ui| self.ui(ui, tabs, selected_tab)); //but I want to edit the rest of the parameters and maybe close automatically .show(ctx, |ui| self.ui(ui, tabs, selected_tab)); //but I want to edit the rest of the parameters and maybe close automatically
self.visible = self.visible.clone() && visible; self.visible = self.visible.clone() && visible;
} }
fn ui(&mut self, ui: &mut egui::Ui, tabs: &mut Vec<Tab>, selected_tab: &mut TabNumber) {
ui.set_min_width(250.0);
let font_id = egui::TextStyle::Body.resolve(ui.style());
self.row_height = ui.fonts(|f| f.row_height(&font_id)); //+ ui.spacing().item_spacing.y;
let mut action : Action = Action::None; fn ui(&mut self, ui: &mut egui::Ui, tabs: &mut Vec<Tab>, selected_tab: &mut TabNumber) {
ui.set_min_width(250.0);
ui.horizontal(|ui| { let font_id = egui::TextStyle::Body.resolve(ui.style());
let Self { search_text, .. } = self; self.row_height = ui.fonts(|f| f.row_height(&font_id)); //+ ui.spacing().item_spacing.y;
let response = ui.add(egui::TextEdit::singleline(search_text).desired_width(120.0).lock_focus(true));
if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
action = Action::Update;
}
if !self.initialized {
response.request_focus();
self.initialized = true;
}
if ui.add(egui::Button::new("Update")).clicked() {
action = Action::Update;
}
if ui.add(egui::Button::new("<")).clicked() { let mut action: Action = Action::None;
action = Action::Previous;
}
if self.search_text == self.searched_text && self.search_text.len() > 0 && self.results.len() == 0 { ui.horizontal(|ui| {
ui.colored_label(RED, " 0/0 "); let Self { search_text, .. } = self;
} else {
ui.label(format!(" {}/{} ", min(self.current_result + 1, self.results.len()), self.results.len()));
}
if ui.add(egui::Button::new(">")).clicked() { let response = ui.add(
action = Action::Next; egui::TextEdit::singleline(search_text)
} .desired_width(120.0)
}); .lock_focus(true),
);
if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
action = Action::Update;
}
if !self.initialized {
response.request_focus();
self.initialized = true;
}
let previous_bool_state = self.across_documents.clone(); if ui.add(egui::Button::new("Update")).clicked() {
ui.checkbox(&mut self.across_documents, "Across documents"); action = Action::Update;
if previous_bool_state != self.across_documents { }
self.searched_text = "".into();
}
egui::CollapsingHeader::new("Replace") if ui.add(egui::Button::new("<")).clicked() {
.default_open(false) action = Action::Previous;
.show(ui, |ui| { }
ui.horizontal(|ui| {
let Self { replace_text, .. } = self;
ui.add(egui::TextEdit::singleline(replace_text).desired_width(120.0).lock_focus(true));
if ui.add(egui::Button::new("Replace")).clicked() {
action = Action::Replace;
}
});
});
match action { if self.search_text == self.searched_text
Action::Update => self.search(tabs, selected_tab), && self.search_text.len() > 0
Action::Next => self.find_result(tabs, selected_tab, 1), && self.results.len() == 0
Action::Previous => self.find_result(tabs, selected_tab, -1), {
Action::Replace => self.replace(tabs, selected_tab), ui.colored_label(RED, " 0/0 ");
Action::None => (), } else {
} ui.label(format!(
} " {}/{} ",
min(self.current_result + 1, self.results.len()),
self.results.len()
pub fn get_cursor_start(&self) -> usize { ));
self.results[self.current_result].start.clone() }
}
if ui.add(egui::Button::new(">")).clicked() {
action = Action::Next;
}
});
pub fn get_cursor_end(&self) -> usize { let previous_bool_state = self.across_documents.clone();
self.results[self.current_result].end.clone() ui.checkbox(&mut self.across_documents, "Across documents");
} if previous_bool_state != self.across_documents {
self.searched_text = "".into();
}
fn search(&mut self, tabs: &mut Vec<Tab>, selected_tab: &mut TabNumber) { egui::CollapsingHeader::new("Replace")
if self.search_text.len() == 0 { .default_open(false)
return .show(ui, |ui| {
} ui.horizontal(|ui| {
let Self { replace_text, .. } = self;
ui.add(
egui::TextEdit::singleline(replace_text)
.desired_width(120.0)
.lock_focus(true),
);
if ui.add(egui::Button::new("Replace")).clicked() {
action = Action::Replace;
}
});
});
let mut search_results: Vec<Selection> = vec![]; match action {
Action::Update => self.search(tabs, selected_tab),
Action::Next => self.find_result(tabs, selected_tab, 1),
Action::Previous => self.find_result(tabs, selected_tab, -1),
Action::Replace => self.replace(tabs, selected_tab),
Action::None => (),
}
}
if self.across_documents { pub fn get_cursor_start(&self) -> usize {
for (index, tab) in tabs.iter().enumerate() { self.results[self.current_result].start.clone()
search_results.extend(self.match_text(tab.code.clone(), TabNumber::from_index(index))); }
}
} else {
search_results.extend(self.match_text(tabs[selected_tab.to_index()].code.clone(), selected_tab.clone()));
}
self.searched_text = self.search_text.clone();
self.results = search_results.clone();
self.current_result = 0;
if self.results.len() > 0 {
self.find_result(tabs, selected_tab, 0);
}
}
pub fn get_cursor_end(&self) -> usize {
self.results[self.current_result].end.clone()
}
fn match_text(&self, tab_text: String, tab_number: TabNumber) -> Vec<Selection> { fn search(&mut self, tabs: &mut Vec<Tab>, selected_tab: &mut TabNumber) {
let matches = tab_text.match_indices(&self.search_text.clone()).map(|(i, _)| Selection { if self.search_text.len() == 0 {
tab : tab_number.clone(), return;
start: i, }
end: i + self.search_text.len(),
}).collect();
matches let mut search_results: Vec<Selection> = vec![];
}
if self.across_documents {
for (index, tab) in tabs.iter().enumerate() {
search_results
.extend(self.match_text(tab.code.clone(), TabNumber::from_index(index)));
}
} else {
search_results.extend(self.match_text(
tabs[selected_tab.to_index()].code.clone(),
selected_tab.clone(),
));
}
fn find_result(&mut self, tabs: &mut Vec<Tab>, selected_tab: &mut TabNumber, direction: i32) { self.searched_text = self.search_text.clone();
if self.searched_text != self.search_text { self.results = search_results.clone();
self.search(tabs, &mut *selected_tab);
} else if self.results.len() > 0 {
self.current_result = (self.current_result as i32 + direction + self.results.len() as i32) as usize % self.results.len();
self.result_selected = false;
*selected_tab = self.results[self.current_result].tab.clone();
let target = self.results[self.current_result].start;
let code = tabs[selected_tab.to_index()].code.clone();
let (upstream, _downstream) = code.split_at(target);
let row = upstream.match_indices(&"\n".to_string()).collect::<Vec<_>>().len();
tabs[selected_tab.to_index()].scroll_offset = self.row_height * row.saturating_sub(5) as f32;
}
}
self.current_result = 0;
if self.results.len() > 0 {
self.find_result(tabs, selected_tab, 0);
}
}
fn replace(&mut self, tabs: &mut Vec<Tab>, selected_tab: &mut TabNumber) { fn match_text(&self, tab_text: String, tab_number: TabNumber) -> Vec<Selection> {
if self.searched_text != self.search_text { let matches = tab_text
self.search(tabs, &mut *selected_tab); .match_indices(&self.search_text.clone())
} .map(|(i, _)| Selection {
tab: tab_number.clone(),
let mut done : Vec<TabNumber> = vec![]; start: i,
for element in &self.results { end: i + self.search_text.len(),
if done.contains(&element.tab) { })
continue; .collect();
}
tabs[element.tab.to_index()].code = tabs[element.tab.to_index()].code.replace(&self.search_text, &self.replace_text); matches
tabs[element.tab.to_index()].saved = false; }
done.push(element.tab.clone())
} fn find_result(&mut self, tabs: &mut Vec<Tab>, selected_tab: &mut TabNumber, direction: i32) {
} if self.searched_text != self.search_text {
} self.search(tabs, &mut *selected_tab);
} else if self.results.len() > 0 {
self.current_result =
(self.current_result as i32 + direction + self.results.len() as i32) as usize
% self.results.len();
self.result_selected = false;
*selected_tab = self.results[self.current_result].tab.clone();
let target = self.results[self.current_result].start;
let code = tabs[selected_tab.to_index()].code.clone();
let (upstream, _downstream) = code.split_at(target);
let row = upstream
.match_indices(&"\n".to_string())
.collect::<Vec<_>>()
.len();
tabs[selected_tab.to_index()].scroll_offset =
self.row_height * row.saturating_sub(5) as f32;
}
}
fn replace(&mut self, tabs: &mut Vec<Tab>, selected_tab: &mut TabNumber) {
if self.searched_text != self.search_text {
self.search(tabs, &mut *selected_tab);
}
let mut done: Vec<TabNumber> = vec![];
for element in &self.results {
if done.contains(&element.tab) {
continue;
}
tabs[element.tab.to_index()].code = tabs[element.tab.to_index()]
.code
.replace(&self.search_text, &self.replace_text);
tabs[element.tab.to_index()].saved = false;
done.push(element.tab.clone())
}
}
}

View file

@ -1,54 +1,50 @@
use eframe::egui;
use crate::ColorTheme; use crate::ColorTheme;
use crate::DEFAULT_THEMES; use crate::DEFAULT_THEMES;
use eframe::egui;
pub struct SettingsWindow { pub struct SettingsWindow {
pub visible: bool, pub visible: bool,
pub updated: bool, pub updated: bool,
pub theme: ColorTheme, pub theme: ColorTheme,
} }
impl SettingsWindow { impl SettingsWindow {
pub fn new(theme : ColorTheme) -> Self { pub fn new(theme: ColorTheme) -> Self {
Self { Self {
visible: false, visible: false,
updated: false, updated: false,
theme, theme,
} }
} }
pub fn show(&mut self, ctx: &egui::Context) {
pub fn show(&mut self, ctx: &egui::Context) { let mut visible = self.visible.clone();
let mut visible = self.visible.clone(); egui::Window::new("Settings")
egui::Window::new("Settings") .open(&mut visible) //I want it to be able to change its visibility (if user close manually)
.open(&mut visible) //I want it to be able to change its visibility (if user close manually) .vscroll(true)
.vscroll(true) .hscroll(true)
.hscroll(true) .show(ctx, |ui| self.ui(ui)); //but I want to edit the rest of the parameters and maybe close automatically
.show(ctx, |ui| self.ui(ui)); //but I want to edit the rest of the parameters and maybe close automatically self.visible = self.visible.clone() && visible;
self.visible = self.visible.clone() && visible; }
}
fn ui(&mut self, ui: &mut egui::Ui) {
ui.set_min_width(250.0);
fn ui(&mut self, ui: &mut egui::Ui) { ui.horizontal(|ui| {
ui.set_min_width(250.0); ui.label("Theme ");
ui.horizontal(|ui| {
ui.label("Theme "); let previous_theme = self.theme.clone();
egui::ComboBox::from_label("")
let previous_theme = self.theme.clone(); .selected_text(format!("{}", self.theme.name))
egui::ComboBox::from_label("") .show_ui(ui, |ui| {
.selected_text(format!("{}", self.theme.name)) ui.style_mut().wrap = Some(false);
.show_ui(ui, |ui| { ui.set_min_width(60.0);
ui.style_mut().wrap = Some(false); for theme in DEFAULT_THEMES {
ui.set_min_width(60.0); ui.selectable_value(&mut self.theme, theme, theme.name);
for theme in DEFAULT_THEMES { }
ui.selectable_value(&mut self.theme, theme, theme.name); });
} if self.theme != previous_theme {
}); self.updated = true;
if self.theme != previous_theme { }
self.updated = true; });
} }
}); }
}
}

View file

@ -1,42 +1,36 @@
use eframe::egui; use eframe::egui;
pub struct ShortcutsWindow { pub struct ShortcutsWindow {
pub visible: bool, pub visible: bool,
} }
impl ShortcutsWindow { impl ShortcutsWindow {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self { visible: false }
visible: false, }
}
} pub fn show(&mut self, ctx: &egui::Context) {
let mut visible = self.visible.clone();
egui::Window::new("Shortcuts")
pub fn show(&mut self, ctx: &egui::Context) { .open(&mut visible)
let mut visible = self.visible.clone(); .vscroll(true)
egui::Window::new("Shortcuts") .hscroll(true)
.open(&mut visible) .show(ctx, |ui| self.ui(ui));
.vscroll(true) self.visible = self.visible.clone() && visible;
.hscroll(true) }
.show(ctx, |ui| self.ui(ui,));
self.visible = self.visible.clone() && visible; fn ui(&mut self, ui: &mut egui::Ui) {
} ui.set_min_width(250.0);
ui.label("Ctrl+S : save file");
ui.label("Ctrl+Shift+S : save file as");
fn ui(&mut self, ui: &mut egui::Ui) { ui.label("Ctrl+R : reload file");
ui.set_min_width(250.0); ui.separator();
ui.label("Ctrl+S : save file"); ui.label("Ctrl+F : open search window");
ui.label("Ctrl+Shift+S : save file as"); ui.separator();
ui.label("Ctrl+R : reload file"); ui.label("Ctrl+Z : undo");
ui.separator(); ui.label("Ctrl+Y : redo");
ui.label("Ctrl+F : open search window"); ui.label("Tab on selection : add indent of selection");
ui.separator(); ui.label("Shift+Tab on selection : remove indent of selection");
ui.label("Ctrl+Z : undo"); ui.label("Ctrl+E : comment selection");
ui.label("Ctrl+Y : redo"); }
ui.label("Tab on selection : add indent of selection"); }
ui.label("Shift+Tab on selection : remove indent of selection");
ui.label("Ctrl+E : comment selection");
}
}

View file

@ -1,74 +1,79 @@
use std::{fs::read_to_string, path::PathBuf};
use eframe::egui::text_edit::CCursorRange; use eframe::egui::text_edit::CCursorRange;
use std::{fs::read_to_string, path::PathBuf};
use crate::MAX_TABS; use crate::MAX_TABS;
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Clone)]
pub enum TabNumber { pub enum TabNumber {
Open, Open,
Number(u8), // Using a range for numeric values Number(u8), // Using a range for numeric values
} }
impl TabNumber { impl TabNumber {
pub fn from_index(n: usize) -> TabNumber { pub fn from_index(n: usize) -> TabNumber {
match n { match n {
0..=MAX_TABS => TabNumber::Number(n as u8), 0..=MAX_TABS => TabNumber::Number(n as u8),
_ => TabNumber::Number(0), _ => TabNumber::Number(0),
} }
} }
pub fn to_index(&self) -> usize { pub fn to_index(&self) -> usize {
match self { match self {
TabNumber::Number(n) => *n as usize, TabNumber::Number(n) => *n as usize,
_ => 0, _ => 0,
} }
} }
} }
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct Tab { pub struct Tab {
pub path : PathBuf, pub path: PathBuf,
pub code : String, pub code: String,
pub language : String, pub language: String,
pub saved : bool, pub saved: bool,
pub scroll_offset : f32, pub scroll_offset: f32,
pub last_cursor : Option<CCursorRange>, pub last_cursor: Option<CCursorRange>,
} }
impl Default for Tab { impl Default for Tab {
fn default() -> Self { fn default() -> Self {
Self { Self {
path: "untitled".into(), path: "untitled".into(),
code: "// Hello there, Master".into(), code: "// Hello there, Master".into(),
language: "rs".into(), language: "rs".into(),
saved: false, saved: false,
scroll_offset: 0.0, scroll_offset: 0.0,
last_cursor: None, last_cursor: None,
} }
} }
} }
impl Tab { impl Tab {
pub fn new(path : PathBuf) -> Self { pub fn new(path: PathBuf) -> Self {
Self { Self {
path: path.clone().into(), path: path.clone().into(),
code: read_to_string(path.clone()).expect("Not able to read the file").replace(&" ".repeat(4), "\t"), code: read_to_string(path.clone())
language: path.to_str().unwrap().split('.').last().unwrap().into(), .expect("Not able to read the file")
saved: true, .replace(&" ".repeat(4), "\t"),
scroll_offset: 0.0, language: path.to_str().unwrap().split('.').last().unwrap().into(),
last_cursor: None, 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 get_name(&self) -> String {
} self.path
.file_name()
pub fn refresh(&mut self) { .expect("Could not get Tab Name")
self.code = read_to_string(self.path.clone()).expect("Not able to read the file").replace(&" ".repeat(4), "\t"); .to_string_lossy()
self.saved = true; .to_string()
println!("refreshed {}", self.path.display()); }
}
} pub fn refresh(&mut self) {
self.code = read_to_string(self.path.clone())
.expect("Not able to read the file")
.replace(&" ".repeat(4), "\t");
self.saved = true;
println!("refreshed {}", self.path.display());
}
}

View file

@ -1,112 +1,114 @@
use crate::tools::format_path; use crate::tools::format_path;
use nix::fcntl::fcntl;
use nix::fcntl::FcntlArg;
use nix::fcntl::OFlag;
use std::io::BufRead; use std::io::BufRead;
use std::io::BufReader; use std::io::BufReader;
use std::io::Read; use std::io::Read;
use std::os::fd::AsRawFd;
use std::process::Stdio; use std::process::Stdio;
use std::{env, path::Path, process::Command}; use std::{env, path::Path, process::Command};
use std::os::fd::AsRawFd;
use nix::fcntl::OFlag;
use nix::fcntl::FcntlArg;
use nix::fcntl::fcntl;
pub struct CommandEntry { pub struct CommandEntry {
pub env: String, pub env: String,
pub command: String, pub command: String,
pub output: String, pub output: String,
pub error: String, pub error: String,
pub output_buffer: BufReader<std::process::ChildStdout>, pub output_buffer: BufReader<std::process::ChildStdout>,
pub error_buffer: BufReader<std::process::ChildStderr>, pub error_buffer: BufReader<std::process::ChildStderr>,
} }
impl CommandEntry { impl CommandEntry {
pub fn new(command: String) -> Self { pub fn new(command: String) -> Self {
let (stdout_reader, stderr_reader) = execute(command.clone()); let (stdout_reader, stderr_reader) = execute(command.clone());
CommandEntry { CommandEntry {
env: format_path(&env::current_dir().expect("Could not find Shell Environnment")), env: format_path(&env::current_dir().expect("Could not find Shell Environnment")),
command, command,
output: String::new(), output: String::new(),
error: String::new(), error: String::new(),
output_buffer: stdout_reader, output_buffer: stdout_reader,
error_buffer: stderr_reader, error_buffer: stderr_reader,
} }
} }
pub fn update(&mut self) { pub fn update(&mut self) {
for line in self.output_buffer.by_ref().lines() { for line in self.output_buffer.by_ref().lines() {
match line { match line {
Ok(line) => self.output += &format!("{}\n", line), Ok(line) => self.output += &format!("{}\n", line),
Err(_) => return, Err(_) => return,
} }
} }
for line in self.error_buffer.by_ref().lines() { for line in self.error_buffer.by_ref().lines() {
match line { match line {
Ok(line) => self.error += &format!("{}\n", line), Ok(line) => self.error += &format!("{}\n", line),
Err(_) => return, Err(_) => return,
} }
} }
} }
} }
pub fn send_command(command: String) -> CommandEntry { pub fn send_command(command: String) -> CommandEntry {
if command.len() < 2 { if command.len() < 2 {
return CommandEntry::new(command); return CommandEntry::new(command);
} }
if &command[..2] != "cd" { if &command[..2] != "cd" {
return CommandEntry::new(command); return CommandEntry::new(command);
} }
if command.len() < 4 { if command.len() < 4 {
let mut entry = CommandEntry::new("echo Invalid cd, should provide path >&2".to_string()); let mut entry = CommandEntry::new("echo Invalid cd, should provide path >&2".to_string());
entry.command = command; entry.command = command;
return entry; return entry;
} }
let path_append = command[3..].replace("~", "/home/penwing"); let path_append = command[3..].replace("~", "/home/penwing");
let path = Path::new(&path_append); let path = Path::new(&path_append);
if format!("{}", path.display()) == "/" { if format!("{}", path.display()) == "/" {
let mut entry = CommandEntry::new("echo Root access denied >&2".to_string()); let mut entry = CommandEntry::new("echo Root access denied >&2".to_string());
entry.command = command; entry.command = command;
return entry; return entry;
} }
if env::set_current_dir(path).is_ok() { if env::set_current_dir(path).is_ok() {
let mut entry = CommandEntry::new(format!("echo Moved to : {}", path.display())); let mut entry = CommandEntry::new(format!("echo Moved to : {}", path.display()));
entry.command = command; entry.command = command;
return entry; return entry;
} else { } else {
let mut entry = let mut entry =
CommandEntry::new(format!("echo Could not find path : {} >&2", path.display())); CommandEntry::new(format!("echo Could not find path : {} >&2", path.display()));
entry.command = command; entry.command = command;
return entry; return entry;
} }
} }
pub fn execute( pub fn execute(
command: String, command: String,
) -> ( ) -> (
BufReader<std::process::ChildStdout>, BufReader<std::process::ChildStdout>,
BufReader<std::process::ChildStderr>, BufReader<std::process::ChildStderr>,
) { ) {
let mut child = Command::new("sh") let mut child = Command::new("sh")
.arg("-c") .arg("-c")
.arg(command.clone()) .arg(command.clone())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.spawn() .spawn()
.expect("failed to execute process"); .expect("failed to execute process");
let stdout = child.stdout.take().unwrap(); let stdout = child.stdout.take().unwrap();
let stderr = child.stderr.take().unwrap(); let stderr = child.stderr.take().unwrap();
let stdout_fd = stdout.as_raw_fd();
let stderr_fd = stderr.as_raw_fd();
fcntl(stdout_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK)).expect("Failed to set non-blocking mode"); let stdout_fd = stdout.as_raw_fd();
fcntl(stderr_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK)).expect("Failed to set non-blocking mode"); let stderr_fd = stderr.as_raw_fd();
return (BufReader::new(stdout), BufReader::new(stderr)); fcntl(stdout_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))
.expect("Failed to set non-blocking mode");
fcntl(stderr_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))
.expect("Failed to set non-blocking mode");
return (BufReader::new(stdout), BufReader::new(stderr));
} }

View file

@ -1,85 +1,84 @@
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tools::*; use crate::tools::*;
#[test] #[test]
fn test_tab_number_conversions() { fn test_tab_number_conversions() {
let tab_num = TabNumber::from_index(3); let tab_num = TabNumber::from_index(3);
assert_eq!(tab_num, TabNumber::Three); assert_eq!(tab_num, TabNumber::Three);
assert_eq!(tab_num.to_index(), 3); assert_eq!(tab_num.to_index(), 3);
} }
#[test] #[test]
fn test_default_tab() { fn test_default_tab() {
let default_tab = Tab::default(); let default_tab = Tab::default();
assert_eq!(default_tab.path, PathBuf::from("untitled")); assert_eq!(default_tab.path, PathBuf::from("untitled"));
assert_eq!(default_tab.code, "// Hello there, Master"); assert_eq!(default_tab.code, "// Hello there, Master");
assert_eq!(default_tab.language, "rs"); assert_eq!(default_tab.language, "rs");
assert!(!default_tab.saved); assert!(!default_tab.saved);
assert_eq!(default_tab.scroll_offset, 0.0); assert_eq!(default_tab.scroll_offset, 0.0);
assert_eq!(default_tab.last_cursor, None); assert_eq!(default_tab.last_cursor, None);
} }
#[test] #[test]
fn test_get_tab_name() { fn test_get_tab_name() {
let tab = Tab { let tab = Tab {
path: PathBuf::from("/path/to/file.rs"), path: PathBuf::from("/path/to/file.rs"),
code: String::from(""), code: String::from(""),
language: String::from("rs"), language: String::from("rs"),
saved: true, saved: true,
scroll_offset: 0.0, scroll_offset: 0.0,
last_cursor: None, last_cursor: None,
}; };
assert_eq!(tab.get_name(), "file.rs"); assert_eq!(tab.get_name(), "file.rs");
} }
#[test] #[test]
fn test_default_command_entry() { fn test_default_command_entry() {
let default_entry = CommandEntry::default(); let default_entry = CommandEntry::default();
assert_eq!( assert_eq!(
default_entry.env, default_entry.env,
env::current_dir() env::current_dir()
.expect("Could not find Shell Environnment") .expect("Could not find Shell Environnment")
.file_name() .file_name()
.expect("Could not get Shell Environnment Name") .expect("Could not get Shell Environnment Name")
.to_string_lossy() .to_string_lossy()
.to_string() .to_string()
); );
assert_eq!(default_entry.command, ""); assert_eq!(default_entry.command, "");
assert_eq!(default_entry.output, ""); assert_eq!(default_entry.output, "");
assert_eq!(default_entry.error, ""); assert_eq!(default_entry.error, "");
} }
#[test] #[test]
fn test_save_and_load_state() { fn test_save_and_load_state() {
let tabs = vec![ let tabs = vec![
PathBuf::from("/path/to/file1.rs"), PathBuf::from("/path/to/file1.rs"),
PathBuf::from("/path/to/file2.py"), PathBuf::from("/path/to/file2.py"),
]; ];
let theme = 42; let theme = 42;
let original_state = AppState { tabs, theme }; let original_state = AppState { tabs, theme };
// Save state to a temporary file // Save state to a temporary file
let temp_file_path = "/tmp/test_state.json"; let temp_file_path = "/tmp/test_state.json";
save_state(&original_state, temp_file_path).expect("Failed to save state"); save_state(&original_state, temp_file_path).expect("Failed to save state");
// Load state from the temporary file // Load state from the temporary file
let loaded_state = load_state(temp_file_path).expect("Failed to load state"); let loaded_state = load_state(temp_file_path).expect("Failed to load state");
assert_eq!(original_state, loaded_state); assert_eq!(original_state, loaded_state);
} }
#[test] #[test]
fn test_run_command() { fn test_run_command() {
let cmd = "echo hello".to_string(); let cmd = "echo hello".to_string();
let entry = run_command(cmd); let entry = run_command(cmd);
assert_eq!(entry.command, "echo hello"); assert_eq!(entry.command, "echo hello");
assert_eq!(entry.output.trim(), "hello"); assert_eq!(entry.output.trim(), "hello");
assert_eq!(entry.error, ""); assert_eq!(entry.error, "");
} }
// Add more tests as needed for other functions // Add more tests as needed for other functions
} }