better file structure and clean up

This commit is contained in:
Penwing 2024-01-30 11:38:17 +01:00
parent 654cb2d9cc
commit df18044c64
38 changed files with 992 additions and 1066 deletions

View file

@ -1,314 +0,0 @@
use eframe::egui;
use egui::{text::CCursor, text_edit::CCursorRange, Rangef};
use std::{cmp::max, env, path::PathBuf}; // path::Path,
use crate::tools;
use crate::Calcifer;
use crate::MAX_TABS;
use crate::PROJECT_EXTENSION;
use tools::hex_str_to_color;
pub mod code_editor;
use code_editor::CodeEditor;
mod app_base;
impl Calcifer {
pub fn draw_settings(&mut self, ctx: &egui::Context) {
egui::SidePanel::left("settings")
.resizable(false)
.exact_width(self.font_size * 1.8)
.show(ctx, |ui| {
ui.vertical(|ui| {
if ui.add(egui::Button::new("📁")).clicked() {
if let Some(path) = rfd::FileDialog::new()
.set_directory(self.home.as_path())
.pick_file()
{
self.open_file(Some(&path));
}
}
ui.separator();
self.tree_visible = self.toggle(ui, self.tree_visible, "📦");
ui.separator();
self.terminal_visible = self.toggle(ui, self.terminal_visible, "🖵");
ui.separator();
self.search_menu.visible = self.toggle(ui, self.search_menu.visible, "🔍");
ui.separator();
self.settings_menu.visible = self.toggle(ui, self.settings_menu.visible, "");
ui.separator();
self.shortcuts_menu.visible = self.toggle(ui, self.shortcuts_menu.visible, "");
ui.separator();
self.profiler_visible = self.toggle(ui, self.profiler_visible, "");
if self.tabs[self.selected_tab.to_index()].language == PROJECT_EXTENSION {
ui.separator();
self.project_mode = self.toggle(ui, self.project_mode, "");
}
});
});
}
pub fn draw_tree_panel(&mut self, ctx: &egui::Context) {
if !self.tree_visible {
return;
}
egui::SidePanel::left("file_tree_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Bookshelf ");
if ui.add(egui::Button::new("📖")).clicked() {
self.file_tree = tools::file_tree::generate_file_tree(self.home.as_path(), 7);
}
});
ui.separator();
let mut n_files: usize = 0;
if let Some(file_tree) = self.file_tree.clone() {
self.list_files(ui, &file_tree, 1, &mut n_files);
} else {
ui.label("No book on the Bookshelf");
}
ui.separator();
ui.label(format!("{} files displayed", n_files));
});
}
pub fn draw_bottom_tray(&mut self, ctx: &egui::Context) {
egui::TopBottomPanel::bottom("tray")
.default_height(self.font_size * 1.2)
.resizable(false)
.show(ctx, |ui| {
ui.label(self.profiler());
});
}
pub fn draw_terminal_panel(&mut self, ctx: &egui::Context) {
if !self.terminal_visible {
return;
}
egui::TopBottomPanel::bottom("terminal")
.default_height(super::TERMINAL_HEIGHT)
.height_range(Rangef::new(
super::TERMINAL_RANGE.start,
super::TERMINAL_RANGE.end,
))
.resizable(true)
.show(ctx, |ui| {
ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
let command_color = hex_str_to_color(self.theme.functions);
let entry_color = hex_str_to_color(self.theme.literals);
let bg_color = hex_str_to_color(self.theme.bg);
ui.label("");
ui.horizontal(|ui| {
if ui.add(egui::Button::new("")).clicked() {
self.command_history.retain(|e| !e.finished);
}
ui.style_mut().visuals.extreme_bg_color = bg_color;
let Self { command, .. } = self;
ui.colored_label(
command_color,
tools::format_path(
&env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
),
);
let response = ui.add(
egui::TextEdit::singleline(command)
.desired_width(f32::INFINITY)
.lock_focus(true),
);
if response.lost_focus() && ctx.input(|i| i.key_pressed(egui::Key::Enter)) {
self.command_history
.push(tools::send_command(self.command.clone()));
self.command = "".into();
response.request_focus();
}
});
ui.separator();
egui::ScrollArea::vertical()
.stick_to_bottom(true)
.show(ui, |ui| {
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
ui.separator();
ui.spacing_mut().item_spacing.y = 0.0;
ui.style_mut().visuals.hyperlink_color = hex_str_to_color(self.theme.keywords);
for entry in &mut self.command_history {
ui.label("");
ui.horizontal(|ui| {
if !entry.finished {
entry.update();
let _ = ui.link("(⌛)");
} else {
if ui.link("(🗐)").clicked() {
entry.copy_error_code();
}
}
ui.colored_label(
command_color,
format!("{} {}", entry.env, entry.command),
);
});
//ui.end_row();
for line in &entry.result {
let color =
if line.error { super::RED } else { entry_color };
ui.colored_label(color, &line.text);
//ui.end_row();
}
}
});
});
});
});
}
pub fn draw_tab_panel(&mut self, ctx: &egui::Context) {
egui::TopBottomPanel::top("tabs")
.resizable(false)
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.style_mut().visuals.selection.bg_fill =
hex_str_to_color(self.theme.functions);
ui.style_mut().visuals.hyperlink_color = hex_str_to_color(self.theme.functions);
for (index, tab) in self.tabs.clone().iter().enumerate() {
let mut title = tab.get_name();
if !tab.saved {
title += " ~";
}
if self.selected_tab == tools::TabNumber::from_index(index) {
ui.style_mut().visuals.override_text_color =
Some(hex_str_to_color(self.theme.bg));
}
ui.selectable_value(
&mut self.selected_tab,
tools::TabNumber::from_index(index),
title,
);
ui.style_mut().visuals.override_text_color = None;
if ui.link("X").clicked() && !self.close_tab_confirm.visible {
if self.tabs.len() > 1 {
if tab.saved {
self.delete_tab(index);
} else {
self.close_tab_confirm.ask();
self.tab_to_close = index;
}
} else {
egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::Close);
}
}
ui.separator();
}
if self.tabs.len() < MAX_TABS {
ui.selectable_value(&mut self.selected_tab, tools::TabNumber::Open, "+");
}
if self.selected_tab == tools::TabNumber::Open {
self.open_file(None);
}
});
});
}
pub fn draw_content_panel(&mut self, ctx: &egui::Context) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
if ui
.add(egui::Button::new("open directory in terminal"))
.clicked()
{
let mut path = self.tabs[self.selected_tab.to_index()].path.clone();
path.pop();
tools::send_command(format!("cd {}", path.display()));
}
ui.label("Picked file:");
ui.monospace(
self.tabs[self.selected_tab.to_index()]
.path
.to_string_lossy()
.to_string(),
);
});
ui.separator();
if self.project_mode
&& self.tabs[self.selected_tab.to_index()].language == PROJECT_EXTENSION
{
self.draw_project_file(ui);
} else {
self.draw_code_file(ui);
}
});
}
fn draw_code_file(&mut self, ui: &mut egui::Ui) {
let current_tab = &mut self.tabs[self.selected_tab.to_index()];
let lines = current_tab.code.chars().filter(|&c| c == '\n').count() + 1;
let mut override_cursor: Option<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,
);
}
fn draw_project_file(&mut self, ui: &mut egui::Ui) {
ui.label("project mode");
}
pub fn draw_windows(&mut self, ctx: &egui::Context) {
if self.search_menu.visible {
self.search_menu
.show(ctx, &mut self.tabs, &mut self.selected_tab);
}
if self.close_tab_confirm.visible {
self.close_tab_confirm.show(ctx);
}
if self.refresh_confirm.visible {
self.refresh_confirm.show(ctx);
}
if self.exit_confirm.visible {
self.exit_confirm.show(ctx);
}
if self.exit_confirm.proceed {
for tab in self.tabs.iter_mut() {
tab.saved = true;
}
egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::Close);
}
if self.shortcuts_menu.visible {
self.shortcuts_menu.show(ctx);
}
if self.settings_menu.visible {
self.settings_menu.show(ctx);
}
if self.settings_menu.updated {
self.theme = self.settings_menu.theme;
}
self.handle_confirm();
}
}

View file

@ -1,218 +0,0 @@
use eframe::egui;
use egui::Color32;
use std::{cmp::min, fs, path::Path, path::PathBuf};
use crate::save_path;
use crate::tools;
use crate::Calcifer;
use crate::DEFAULT_THEMES;
use crate::MAX_TABS;
use crate::TIME_LABELS;
use tools::file_tree;
use tools::hex_str_to_color;
impl Calcifer {
pub fn handle_confirm(&mut self) {
if self.close_tab_confirm.proceed {
self.close_tab_confirm.close();
self.delete_tab(self.tab_to_close);
}
if self.refresh_confirm.proceed {
self.refresh_confirm.close();
self.tabs[self.selected_tab.to_index()].refresh();
}
}
pub fn save_tab(&self) -> Option<PathBuf> {
if self.tabs[self.selected_tab.to_index()]
.path
.file_name()
.map_or(true, |name| name.to_string_lossy() == "untitled")
{
self.save_tab_as()
} else {
if let Err(err) = fs::write(
&self.tabs[self.selected_tab.to_index()].path,
&self.tabs[self.selected_tab.to_index()].code,
) {
eprintln!("Error writing file: {}", err);
return None;
}
Some(self.tabs[self.selected_tab.to_index()].path.clone())
}
}
pub fn save_tab_as(&self) -> Option<PathBuf> {
if let Some(path) = rfd::FileDialog::new()
.set_directory(self.home.as_path())
.save_file()
{
if let Err(err) = fs::write(&path, &self.tabs[self.selected_tab.to_index()].code) {
eprintln!("Error writing file: {}", err);
return None;
}
return Some(path);
}
None
}
pub fn handle_save_file(&mut self, path_option: Option<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()
.map_or(true, |name| name.to_string_lossy() == "untitled")
{
new.open_file(Some(&path));
}
}
if new.tabs == vec![] {
new.open_file(None);
}
new
}
pub fn save_state(&self) {
let mut state_theme: usize = 0;
if let Some(theme) = DEFAULT_THEMES.iter().position(|&r| r == self.theme) {
state_theme = theme;
}
let mut state_tabs = vec![];
for tab in &self.tabs {
state_tabs.push(tab.path.clone());
}
let app_state = tools::AppState {
tabs: state_tabs,
theme: state_theme,
};
let _ = tools::save_state(&app_state, save_path().as_path());
}
pub fn move_through_tabs(&mut self, forward: bool) {
let new_index = if forward {
(self.selected_tab.to_index() + 1) % self.tabs.len()
} else {
self.selected_tab
.to_index()
.checked_sub(1)
.unwrap_or(self.tabs.len() - 1)
};
self.selected_tab = tools::TabNumber::from_index(new_index);
}
pub fn open_file(&mut self, path_option: Option<&Path>) {
if let Some(path) = path_option {
for (index, tab) in self.tabs.clone().iter().enumerate() {
if tab.path == path {
self.selected_tab = tools::TabNumber::from_index(index);
return;
}
}
}
if self.tabs.len() < MAX_TABS {
if let Some(path) = path_option {
self.tabs.push(tools::Tab::new(path.to_path_buf()));
} else {
self.tabs.push(tools::Tab::default());
}
self.selected_tab = tools::TabNumber::from_index(self.tabs.len() - 1);
}
}
pub fn delete_tab(&mut self, index: usize) {
self.tabs.remove(index);
self.selected_tab = tools::TabNumber::from_index(min(index, self.tabs.len() - 1));
}
pub fn toggle(&self, ui: &mut egui::Ui, display: bool, title: &str) -> bool {
let bg_color: Color32;
let text_color: Color32;
if display {
bg_color = hex_str_to_color(self.theme.functions);
text_color = hex_str_to_color(self.theme.bg);
} else {
bg_color = hex_str_to_color(self.theme.bg);
text_color = hex_str_to_color(self.theme.literals);
};
ui.style_mut().visuals.override_text_color = Some(text_color);
if ui.add(egui::Button::new(title).fill(bg_color)).clicked() {
return !display;
}
ui.style_mut().visuals.override_text_color = None;
display
}
pub fn profiler(&self) -> String {
if !self.profiler_visible {
return "".to_string();
}
let combined_string: Vec<String> = TIME_LABELS
.into_iter()
.zip(self.time_watch.clone())
.map(|(s, v)| format!("{} : {:.1} ms", s, v))
.collect();
let mut result = combined_string.join(" ; ");
result.push_str(&format!(
" total : {:.1} ms",
self.time_watch.clone().iter().sum::<f32>()
));
result
}
pub fn list_files(
&mut self,
ui: &mut egui::Ui,
file: &file_tree::File,
depth: isize,
n_files: &mut usize,
) {
*n_files += 1;
if let Some(folder_content) = &file.folder_content {
let collapsing_response = egui::CollapsingHeader::new(file.name.clone())
.default_open(depth > 0)
.show(ui, |ui| {
if !self.tree_dir_opened.contains(&file.name) {
return;
}
for deeper_file in folder_content {
self.list_files(ui, deeper_file, depth - 1, n_files);
}
});
if collapsing_response.fully_closed() {
self.tree_dir_opened.retain(|s| s != &file.name);
} else if !self.tree_dir_opened.contains(&file.name) {
self.tree_dir_opened.push(file.name.clone());
}
} else if ui.button(&file.name).clicked() {
self.open_file(Some(&file.path));
}
}
}

223
src/core/app.rs Normal file
View file

@ -0,0 +1,223 @@
use eframe::egui;
use egui::Color32;
use std::{cmp::min, fs, path::Path, path::PathBuf};
use crate::core;
use crate::editor::themes::DEFAULT_THEMES;
use crate::panels;
use crate::save_path;
use crate::sub_windows;
use crate::Calcifer;
use crate::MAX_TABS;
use crate::TIME_LABELS;
impl Calcifer {
pub fn handle_confirm(&mut self) {
if self.close_tab_confirm.proceed {
self.close_tab_confirm.close();
self.delete_tab(self.tab_to_close);
}
if self.refresh_confirm.proceed {
self.refresh_confirm.close();
self.tabs[self.selected_tab.to_index()].refresh();
}
}
pub fn save_tab(&self) -> Option<PathBuf> {
if self.tabs[self.selected_tab.to_index()]
.path
.file_name()
.map_or(true, |name| name.to_string_lossy() == "untitled")
{
self.save_tab_as()
} else {
if let Err(err) = fs::write(
&self.tabs[self.selected_tab.to_index()].path,
&self.tabs[self.selected_tab.to_index()].code,
) {
eprintln!("Error writing file: {}", err);
return None;
}
Some(self.tabs[self.selected_tab.to_index()].path.clone())
}
}
pub fn save_tab_as(&self) -> Option<PathBuf> {
if let Some(path) = rfd::FileDialog::new()
.set_directory(self.home.as_path())
.save_file()
{
if let Err(err) = fs::write(&path, &self.tabs[self.selected_tab.to_index()].code) {
eprintln!("Error writing file: {}", err);
return None;
}
return Some(path);
}
None
}
pub fn handle_save_file(&mut self, path_option: Option<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: core::AppState) -> Self {
let mut new = Self {
theme: DEFAULT_THEMES[min(app_state.theme, DEFAULT_THEMES.len() - 1)],
tabs: Vec::new(),
settings_menu: sub_windows::SettingsWindow::new(DEFAULT_THEMES[app_state.theme]),
..Default::default()
};
for path in app_state.tabs {
if !path
.file_name()
.map_or(true, |name| name.to_string_lossy() == "untitled")
{
new.open_file(Some(&path));
}
}
if new.tabs == vec![] {
new.open_file(None);
}
new
}
pub fn save_state(&self) {
let mut state_theme: usize = 0;
if let Some(theme) = DEFAULT_THEMES.iter().position(|&r| r == self.theme) {
state_theme = theme;
}
let mut state_tabs = vec![];
for tab in &self.tabs {
state_tabs.push(tab.path.clone());
}
let app_state = core::AppState {
tabs: state_tabs,
theme: state_theme,
};
let _ = core::save_state(&app_state, save_path().as_path());
}
pub fn move_through_tabs(&mut self, forward: bool) {
let new_index = if forward {
(self.selected_tab.to_index() + 1) % self.tabs.len()
} else {
self.selected_tab
.to_index()
.checked_sub(1)
.unwrap_or(self.tabs.len() - 1)
};
self.selected_tab = panels::TabNumber::from_index(new_index);
}
pub fn open_file(&mut self, path_option: Option<&Path>) {
if let Some(path) = path_option {
for (index, tab) in self.tabs.clone().iter().enumerate() {
if tab.path == path {
self.selected_tab = panels::TabNumber::from_index(index);
return;
}
}
}
if self.tabs.len() < MAX_TABS {
if let Some(path) = path_option {
self.tabs.push(panels::Tab::new(path.to_path_buf()));
} else {
self.tabs.push(panels::Tab::default());
}
self.selected_tab = panels::TabNumber::from_index(self.tabs.len() - 1);
}
}
pub fn delete_tab(&mut self, index: usize) {
self.tabs.remove(index);
self.selected_tab = panels::TabNumber::from_index(min(index, self.tabs.len() - 1));
}
pub fn toggle(&self, ui: &mut egui::Ui, display: bool, title: &str) -> bool {
let bg_color: Color32;
let text_color: Color32;
if display {
bg_color = hex_str_to_color(self.theme.functions);
text_color = hex_str_to_color(self.theme.bg);
} else {
bg_color = hex_str_to_color(self.theme.bg);
text_color = hex_str_to_color(self.theme.literals);
};
ui.style_mut().visuals.override_text_color = Some(text_color);
if ui.add(egui::Button::new(title).fill(bg_color)).clicked() {
return !display;
}
ui.style_mut().visuals.override_text_color = None;
display
}
pub fn profiler(&self) -> String {
if !self.profiler_visible {
return "".to_string();
}
let combined_string: Vec<String> = TIME_LABELS
.into_iter()
.zip(self.time_watch.clone())
.map(|(s, v)| format!("{} : {:.1} ms", s, v))
.collect();
let mut result = combined_string.join(" ; ");
result.push_str(&format!(
" total : {:.1} ms",
self.time_watch.clone().iter().sum::<f32>()
));
result
}
pub fn list_files(
&mut self,
ui: &mut egui::Ui,
file: &panels::FileEntry,
depth: isize,
n_files: &mut usize,
) {
*n_files += 1;
if let Some(folder_content) = &file.folder_content {
let collapsing_response = egui::CollapsingHeader::new(file.name.clone())
.default_open(depth > 0)
.show(ui, |ui| {
if !self.tree_dir_opened.contains(&file.name) {
return;
}
for deeper_file in folder_content {
self.list_files(ui, deeper_file, depth - 1, n_files);
}
});
if collapsing_response.fully_closed() {
self.tree_dir_opened.retain(|s| s != &file.name);
} else if !self.tree_dir_opened.contains(&file.name) {
self.tree_dir_opened.push(file.name.clone());
}
} else if ui.button(&file.name).clicked() {
self.open_file(Some(&file.path));
}
}
}
#[allow(clippy::unnecessary_lazy_evaluations)]
pub fn hex_str_to_color(hex_str: &str) -> Color32 {
Color32::from_hex(hex_str).unwrap_or_else(|_| Color32::BLACK)
}

9
src/core/mod.rs Normal file
View file

@ -0,0 +1,9 @@
#[allow(unused_imports)]
mod app;
pub use app::*;
mod ui;
pub use ui::*;
mod state;
pub use state::*;

58
src/core/state.rs Normal file
View file

@ -0,0 +1,58 @@
use eframe::egui;
use image::GenericImageView;
use serde::{Deserialize, Serialize};
use std::{
error::Error,
fs,
fs::{read_to_string, OpenOptions},
io::Write,
path::{Path, PathBuf},
};
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
pub struct AppState {
pub tabs: Vec<PathBuf>,
pub theme: usize,
}
pub fn save_state(state: &AppState, file_path: &Path) -> Result<(), std::io::Error> {
let serialized_state = serde_json::to_string(state)?;
if let Some(parent_dir) = file_path.parent() {
fs::create_dir_all(parent_dir)?;
}
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(file_path)?;
file.write_all(serialized_state.as_bytes())?;
println!("Saved state at {}", file_path.display());
Ok(())
}
pub fn load_state(file_path: &Path) -> Result<AppState, std::io::Error> {
let serialized_state = read_to_string(file_path)?;
Ok(serde_json::from_str(&serialized_state)?)
}
pub fn load_icon() -> Result<egui::IconData, Box<dyn Error>> {
let (icon_rgba, icon_width, icon_height) = {
let icon = include_bytes!("../../assets/icon.png");
let image = image::load_from_memory(icon)?;
let rgba = image.clone().into_rgba8().to_vec();
let (width, height) = image.dimensions();
(rgba, width, height)
};
Ok(egui::IconData {
rgba: icon_rgba,
width: icon_width,
height: icon_height,
})
}

338
src/core/ui.rs Normal file
View file

@ -0,0 +1,338 @@
use eframe::egui;
use egui::{text::CCursor, text_edit::CCursorRange, Rangef};
use std::{cmp::max, env, ffi::OsStr, path::Component, path::Path, path::PathBuf};
use crate::core;
use crate::editor;
use crate::panels;
use crate::Calcifer;
use crate::DISPLAY_PATH_DEPTH;
use crate::MAX_TABS;
use crate::PROJECT_EXTENSION;
use crate::RED;
use crate::TERMINAL_HEIGHT;
use crate::TERMINAL_RANGE;
use editor::{CodeEditor, Syntax};
impl Calcifer {
pub fn draw_settings(&mut self, ctx: &egui::Context) {
egui::SidePanel::left("settings")
.resizable(false)
.exact_width(self.font_size * 1.8)
.show(ctx, |ui| {
ui.vertical(|ui| {
if ui.add(egui::Button::new("📁")).clicked() {
if let Some(path) = rfd::FileDialog::new()
.set_directory(self.home.as_path())
.pick_file()
{
self.open_file(Some(&path));
}
}
ui.separator();
self.tree_visible = self.toggle(ui, self.tree_visible, "📦");
ui.separator();
self.terminal_visible = self.toggle(ui, self.terminal_visible, "🖵");
ui.separator();
self.search_menu.visible = self.toggle(ui, self.search_menu.visible, "🔍");
ui.separator();
self.settings_menu.visible = self.toggle(ui, self.settings_menu.visible, "");
ui.separator();
self.shortcuts_menu.visible = self.toggle(ui, self.shortcuts_menu.visible, "");
ui.separator();
self.profiler_visible = self.toggle(ui, self.profiler_visible, "");
if self.tabs[self.selected_tab.to_index()].language == PROJECT_EXTENSION {
ui.separator();
self.project_mode = self.toggle(ui, self.project_mode, "");
}
});
});
}
pub fn draw_tree_panel(&mut self, ctx: &egui::Context) {
if !self.tree_visible {
return;
}
egui::SidePanel::left("file_tree_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("Bookshelf ");
if ui.add(egui::Button::new("📖")).clicked() {
self.file_tree = panels::generate_file_tree(self.home.as_path(), 7);
}
});
ui.separator();
let mut n_files: usize = 0;
if let Some(file_tree) = self.file_tree.clone() {
self.list_files(ui, &file_tree, 1, &mut n_files);
} else {
ui.label("No book on the Bookshelf");
}
ui.separator();
ui.label(format!("{} files displayed", n_files));
});
}
pub fn draw_bottom_tray(&mut self, ctx: &egui::Context) {
egui::TopBottomPanel::bottom("tray")
.default_height(self.font_size * 1.2)
.resizable(false)
.show(ctx, |ui| {
ui.label(self.profiler());
});
}
pub fn draw_terminal_panel(&mut self, ctx: &egui::Context) {
if !self.terminal_visible {
return;
}
egui::TopBottomPanel::bottom("terminal")
.default_height(TERMINAL_HEIGHT)
.height_range(Rangef::new(TERMINAL_RANGE.start, TERMINAL_RANGE.end))
.resizable(true)
.show(ctx, |ui| {
ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
let command_color = core::hex_str_to_color(self.theme.functions);
let entry_color = core::hex_str_to_color(self.theme.literals);
let bg_color = core::hex_str_to_color(self.theme.bg);
ui.label("");
ui.horizontal(|ui| {
if ui.add(egui::Button::new("")).clicked() {
self.command_history.retain(|e| !e.finished);
}
ui.style_mut().visuals.extreme_bg_color = bg_color;
let Self { command, .. } = self;
ui.colored_label(
command_color,
format_path(&env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))),
);
let response = ui.add(
egui::TextEdit::singleline(command)
.desired_width(f32::INFINITY)
.lock_focus(true),
);
if response.lost_focus() && ctx.input(|i| i.key_pressed(egui::Key::Enter)) {
self.command_history
.push(panels::send_command(self.command.clone()));
self.command = "".into();
response.request_focus();
}
});
ui.separator();
egui::ScrollArea::vertical()
.stick_to_bottom(true)
.show(ui, |ui| {
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
ui.separator();
ui.spacing_mut().item_spacing.y = 0.0;
ui.style_mut().visuals.hyperlink_color =
core::hex_str_to_color(self.theme.keywords);
for entry in &mut self.command_history {
ui.label("");
ui.horizontal(|ui| {
if !entry.finished {
entry.update();
let _ = ui.link("(⌛)");
} else if ui.link("(🗐)").clicked() {
entry.copy_error_code();
}
ui.colored_label(
command_color,
format!("{} {}", entry.env, entry.command),
);
});
for line in &entry.result {
let color = if line.error { RED } else { entry_color };
ui.colored_label(color, &line.text);
}
}
});
});
});
});
}
pub fn draw_tab_panel(&mut self, ctx: &egui::Context) {
egui::TopBottomPanel::top("tabs")
.resizable(false)
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.style_mut().visuals.selection.bg_fill =
core::hex_str_to_color(self.theme.functions);
ui.style_mut().visuals.hyperlink_color =
core::hex_str_to_color(self.theme.functions);
for (index, tab) in self.tabs.clone().iter().enumerate() {
let mut title = tab.get_name();
if !tab.saved {
title += " ~";
}
if self.selected_tab == panels::TabNumber::from_index(index) {
ui.style_mut().visuals.override_text_color =
Some(core::hex_str_to_color(self.theme.bg));
}
ui.selectable_value(
&mut self.selected_tab,
panels::TabNumber::from_index(index),
title,
);
ui.style_mut().visuals.override_text_color = None;
if ui.link("X").clicked() && !self.close_tab_confirm.visible {
if self.tabs.len() > 1 {
if tab.saved {
self.delete_tab(index);
} else {
self.close_tab_confirm.ask();
self.tab_to_close = index;
}
} else {
egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::Close);
}
}
ui.separator();
}
if self.tabs.len() < MAX_TABS {
ui.selectable_value(&mut self.selected_tab, panels::TabNumber::Open, "+");
}
if self.selected_tab == panels::TabNumber::Open {
self.open_file(None);
}
});
});
}
pub fn draw_content_panel(&mut self, ctx: &egui::Context) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| {
if ui
.add(egui::Button::new("open directory in terminal"))
.clicked()
{
let mut path = self.tabs[self.selected_tab.to_index()].path.clone();
path.pop();
panels::send_command(format!("cd {}", path.display()));
}
ui.label("Picked file:");
ui.monospace(
self.tabs[self.selected_tab.to_index()]
.path
.to_string_lossy()
.to_string(),
);
});
ui.separator();
if self.project_mode
&& self.tabs[self.selected_tab.to_index()].language == PROJECT_EXTENSION
{
self.draw_project_file(ui);
} else {
self.draw_code_file(ui);
}
});
}
fn draw_code_file(&mut self, ui: &mut egui::Ui) {
let current_tab = &mut self.tabs[self.selected_tab.to_index()];
let lines = current_tab.code.chars().filter(|&c| c == '\n').count() + 1;
let mut override_cursor: Option<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(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,
);
}
fn draw_project_file(&mut self, ui: &mut egui::Ui) {
ui.label("project mode");
}
pub fn draw_windows(&mut self, ctx: &egui::Context) {
if self.search_menu.visible {
self.search_menu
.show(ctx, &mut self.tabs, &mut self.selected_tab);
}
if self.close_tab_confirm.visible {
self.close_tab_confirm.show(ctx);
}
if self.refresh_confirm.visible {
self.refresh_confirm.show(ctx);
}
if self.exit_confirm.visible {
self.exit_confirm.show(ctx);
}
if self.exit_confirm.proceed {
for tab in self.tabs.iter_mut() {
tab.saved = true;
}
egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::Close);
}
if self.shortcuts_menu.visible {
self.shortcuts_menu.show(ctx);
}
if self.settings_menu.visible {
self.settings_menu.show(ctx);
}
if self.settings_menu.updated {
self.theme = self.settings_menu.theme;
}
self.handle_confirm();
}
}
fn to_syntax(language: &str) -> Syntax {
match language {
"py" => Syntax::python(),
"rs" => Syntax::rust(),
_ => Syntax::shell(),
}
}
pub fn format_path(path: &Path) -> String {
let components: Vec<&OsStr> = path
.components()
.rev()
.take(DISPLAY_PATH_DEPTH)
.filter_map(|component| match component {
Component::RootDir | Component::CurDir => None,
_ => Some(component.as_os_str()),
})
.collect();
format!(
"{}>",
components
.iter()
.rev()
.map(|&c| c.to_string_lossy())
.collect::<Vec<_>>()
.join("/")
)
}

View file

@ -1,15 +1,16 @@
mod calcifer;
mod tools;
use calcifer::code_editor::ColorTheme;
use eframe::egui; use eframe::egui;
use egui::FontFamily::Proportional; use egui::{
use egui::FontId; FontFamily::Proportional,
use egui::TextStyle::{Body, Button, Heading, Monospace, Small}; FontId,
TextStyle::{Body, Button, Heading, Monospace, Small},
};
use homedir::get_my_home; use homedir::get_my_home;
use std::{ops::Range, path::PathBuf, sync::Arc, thread, time}; use std::{ops::Range, path::PathBuf, sync::Arc, thread, time};
use calcifer::code_editor::themes::DEFAULT_THEMES; mod core;
mod editor;
mod panels;
mod sub_windows;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
const TITLE: &str = " debug"; const TITLE: &str = " debug";
@ -30,7 +31,7 @@ const DISPLAY_PATH_DEPTH: usize = 3;
const MAX_TABS: usize = 20; const MAX_TABS: usize = 20;
fn main() -> Result<(), eframe::Error> { fn main() -> Result<(), eframe::Error> {
let icon_data = tools::load_icon().unwrap_or_default(); let icon_data = core::load_icon().unwrap_or_default();
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()
@ -40,13 +41,13 @@ fn main() -> Result<(), eframe::Error> {
}; };
// Attempt to load previous state // Attempt to load previous state
let app_state: tools::AppState = if save_path().exists() { let app_state: core::AppState = if save_path().exists() {
match tools::load_state(save_path().as_path()) { match core::load_state(save_path().as_path()) {
Ok(app_state) => app_state, Ok(app_state) => app_state,
Err(_) => tools::AppState::default(), Err(_) => core::AppState::default(),
} }
} else { } else {
tools::AppState::default() core::AppState::default()
}; };
eframe::run_native( eframe::run_native(
@ -57,33 +58,33 @@ fn main() -> Result<(), eframe::Error> {
} }
struct Calcifer { struct Calcifer {
selected_tab: tools::TabNumber, selected_tab: panels::TabNumber,
tabs: Vec<tools::Tab>, tabs: Vec<panels::Tab>,
command: String, command: String,
command_history: Vec<tools::CommandEntry>, command_history: Vec<panels::CommandEntry>,
theme: ColorTheme, theme: editor::ColorTheme,
font_size: f32, font_size: f32,
project_mode: bool, project_mode: bool,
home: PathBuf, home: PathBuf,
tree_dir_opened: Vec<String>, tree_dir_opened: Vec<String>,
file_tree: Option<tools::file_tree::File>, file_tree: Option<panels::FileEntry>,
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: sub_windows::ConfirmWindow,
tab_to_close: usize, tab_to_close: usize,
refresh_confirm: tools::confirm::ConfirmWindow, refresh_confirm: sub_windows::ConfirmWindow,
exit_confirm: tools::confirm::ConfirmWindow, exit_confirm: sub_windows::ConfirmWindow,
search_menu: tools::search::SearchWindow, search_menu: sub_windows::SearchWindow,
settings_menu: tools::settings::SettingsWindow, settings_menu: sub_windows::SettingsWindow,
shortcuts_menu: tools::shortcuts::ShortcutsWindow, shortcuts_menu: sub_windows::ShortcutsWindow,
time_watch: Vec<f32>, time_watch: Vec<f32>,
next_frame: time::Instant, next_frame: time::Instant,
@ -92,13 +93,13 @@ struct Calcifer {
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: panels::TabNumber::from_index(0),
tabs: vec![tools::Tab::default()], tabs: vec![panels::Tab::default()],
command: String::new(), command: String::new(),
command_history: Vec::new(), command_history: Vec::new(),
theme: DEFAULT_THEMES[0], theme: editor::themes::DEFAULT_THEMES[0],
font_size: 14.0, font_size: 14.0,
project_mode: true, project_mode: true,
@ -111,20 +112,20 @@ impl Default for Calcifer {
profiler_visible: false, profiler_visible: false,
terminal_visible: false, terminal_visible: false,
close_tab_confirm: tools::confirm::ConfirmWindow::new( close_tab_confirm: sub_windows::ConfirmWindow::new(
"You have some unsaved changes, Do you still want to close this document ?", "You have some unsaved changes, Do you still want to close this document ?",
"Confirm Close", "Confirm Close",
), ),
tab_to_close: 0, tab_to_close: 0,
refresh_confirm: tools::confirm::ConfirmWindow::new( refresh_confirm: sub_windows::ConfirmWindow::new(
"You have some unsaved changes, Do you still want to refresh this document ?", "You have some unsaved changes, Do you still want to refresh this document ?",
"Confirm Refresh", "Confirm Refresh",
), ),
exit_confirm: tools::confirm::ConfirmWindow::new("", "Confirm Exit"), exit_confirm: sub_windows::ConfirmWindow::new("", "Confirm Exit"),
search_menu: tools::search::SearchWindow::default(), search_menu: sub_windows::SearchWindow::default(),
settings_menu: tools::settings::SettingsWindow::new(DEFAULT_THEMES[0]), settings_menu: sub_windows::SettingsWindow::new(editor::themes::DEFAULT_THEMES[0]),
shortcuts_menu: tools::shortcuts::ShortcutsWindow::new(), shortcuts_menu: sub_windows::ShortcutsWindow::new(),
time_watch: vec![0.0; TIME_LABELS.len()], time_watch: vec![0.0; TIME_LABELS.len()],
next_frame: time::Instant::now(), next_frame: time::Instant::now(),

114
src/panels/file_tree.rs Normal file
View file

@ -0,0 +1,114 @@
use std::{
cmp::Ordering,
ffi::OsStr,
fs, io,
path::{Path, PathBuf},
};
use crate::ALLOWED_FILE_EXTENSIONS;
#[derive(Clone)]
pub struct FileEntry {
pub name: String,
pub path: PathBuf,
pub folder_content: Option<Vec<FileEntry>>,
pub folder_open: bool,
}
impl FileEntry {
pub fn new_entry(name: String, path: PathBuf) -> Self {
Self {
name,
path,
folder_content: None,
folder_open: false,
}
}
}
pub fn generate_file_tree(path: &Path, depth: isize) -> Option<FileEntry> {
if let Some(file_name) = path.file_name() {
if file_name.to_string_lossy().starts_with('.') {
return None;
}
let extension = path.extension().and_then(|ext| ext.to_str());
if !ALLOWED_FILE_EXTENSIONS.contains(&extension.unwrap_or_default()) {
return None;
}
} else {
return None;
}
let name = path
.file_name()
.unwrap_or_else(|| OsStr::new(""))
.to_string_lossy()
.into_owned();
if !path.is_dir() || depth < 0 {
return Some(FileEntry::new_entry(name, path.to_path_buf()));
}
match fs::read_dir(path) {
Err(err) => Some(FileEntry::new_entry(
format!("Error reading directory: {}", err),
path.to_path_buf(),
)),
Ok(entries) => {
let mut paths: Vec<Result<fs::DirEntry, io::Error>> = entries
.map(|r| r.map_err(|e| io::Error::new(io::ErrorKind::Other, e)))
.collect();
paths.sort_by(|a, b| match (a, b) {
(Ok(entry_a), Ok(entry_b)) => sort_directories_first(entry_a, entry_b),
(Err(_), Ok(_)) => std::cmp::Ordering::Greater,
(Ok(_), Err(_)) => std::cmp::Ordering::Less,
(Err(_), Err(_)) => std::cmp::Ordering::Equal,
});
let mut folder_content = Vec::new();
for result in paths {
match result {
Ok(entry) => {
if let Some(file) = generate_file_tree(&entry.path(), depth - 1) {
folder_content.push(file);
}
}
Err(err) => {
folder_content.push(FileEntry::new_entry(
format!("Error reading entry: {}", err),
path.to_path_buf(),
));
}
}
}
if folder_content.is_empty() {
return None;
}
Some(FileEntry {
name,
path: path.to_path_buf(),
folder_content: Some(folder_content),
folder_open: false,
})
}
}
}
fn sort_directories_first(a: &std::fs::DirEntry, b: &std::fs::DirEntry) -> Ordering {
let a_is_dir = a.path().is_dir();
let b_is_dir = b.path().is_dir();
// Directories come first, then files
if a_is_dir && !b_is_dir {
Ordering::Less
} else if !a_is_dir && b_is_dir {
Ordering::Greater
} else {
// Both are either directories or files, sort alphabetically
a.path().cmp(&b.path())
}
}

9
src/panels/mod.rs Normal file
View file

@ -0,0 +1,9 @@
#[allow(unused_imports)]
mod tabs;
pub use tabs::*;
mod file_tree;
pub use file_tree::*;
mod terminal;
pub use terminal::*;

View file

@ -0,0 +1,183 @@
use crate::core::format_path;
use arboard::Clipboard;
use nix::fcntl::{fcntl, FcntlArg, OFlag};
use std::{
env,
io::{BufRead, BufReader},
os::fd::AsRawFd,
path::{Path, PathBuf},
process::{Child, Command, Stdio},
};
pub struct Buffer {
pub output_buffer: BufReader<std::process::ChildStdout>,
pub error_buffer: BufReader<std::process::ChildStderr>,
pub child: Child,
}
#[derive(Clone)]
pub struct Line {
pub text: String,
pub error: bool,
}
impl Line {
fn output(text: String) -> Self {
Self {
text: remove_line_break(text),
error: false,
}
}
fn error(text: String) -> Self {
Self {
text: remove_line_break(text),
error: true,
}
}
}
pub struct CommandEntry {
pub env: String,
pub command: String,
pub result: Vec<Line>,
pub buffer: Option<Buffer>,
pub finished: bool,
}
impl CommandEntry {
pub fn new(env: String, command: String) -> Self {
let (buffer, result) = match execute(command.clone()) {
Ok(command_buffer) => (Some(command_buffer), vec![]),
Err(err) => (
None,
vec![Line::error(format!("failed to get results: {}", err))],
),
};
CommandEntry {
env,
command,
result,
buffer,
finished: false,
}
}
pub fn update(&mut self) {
if let Some(buffer) = &mut self.buffer {
let mut output = String::new();
let _ = buffer.output_buffer.read_line(&mut output);
if !remove_line_break(output.to_string()).is_empty() {
self.result.push(Line::output(format!("{}\n", output)));
}
let mut error = String::new();
let _ = buffer.error_buffer.read_line(&mut error);
if !remove_line_break(error.to_string()).is_empty() {
self.result.push(Line::error(format!("{}\n", error)));
}
}
if let Some(buffer) = &mut self.buffer {
if let Ok(Some(_exit_status)) = buffer.child.try_wait() {
//self.result.push(Line::output(format!("Command finished with status: {:?}\n", exit_status)));
self.finished = true;
}
}
}
pub fn copy_error_code(&self) {
let mut txt: String = "".to_string();
for line in self.result.iter() {
if line.error {
txt.push_str(&format!("{}\n", line.text));
}
}
let mut _clipboard = Clipboard::new().expect("Failed to initialize clipboard");
_clipboard.set_text(txt).unwrap();
}
}
pub fn send_command(command: String) -> CommandEntry {
let env = format_path(&env::current_dir().unwrap_or_else(|_| PathBuf::from("/")));
if command.len() < 2 {
return CommandEntry::new(env, command);
}
if &command[..2] != "cd" {
return CommandEntry::new(env, command);
}
if command.len() < 4 {
let mut entry =
CommandEntry::new(env, "echo Invalid cd, should provide path >&2".to_string());
entry.command = command;
return entry;
}
let path_append = command[3..].replace('~', "/home/penwing");
let path = Path::new(&path_append);
if format!("{}", path.display()) == "/" {
let mut entry = CommandEntry::new(env, "echo Root access denied >&2".to_string());
entry.command = command;
return entry;
}
if env::set_current_dir(path).is_ok() {
let mut entry = CommandEntry::new(env, format!("echo Moved to : {}", path.display()));
entry.command = command;
entry
} else {
let mut entry = CommandEntry::new(
env,
format!("echo Could not find path : {} >&2", path.display()),
);
entry.command = command;
entry
}
}
pub fn execute(command: String) -> Result<Buffer, std::io::Error> {
let mut child = Command::new("sh")
.arg("-c")
.arg(command.clone())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdout = child
.stdout
.take()
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "Failed to open stdout"))?;
let stderr = child
.stderr
.take()
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "Failed to open stderr"))?;
let stdout_fd = stdout.as_raw_fd();
let stderr_fd = stderr.as_raw_fd();
fcntl(stdout_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))?;
fcntl(stderr_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))?;
let output_buffer = BufReader::new(stdout);
let error_buffer = BufReader::new(stderr);
Ok(Buffer {
output_buffer,
error_buffer,
child,
})
}
fn remove_line_break(input: String) -> String {
let mut text = input.clone();
while text.ends_with('\n') {
text.pop();
if text.ends_with('\r') {
text.pop();
}
}
text
}

View file

@ -0,0 +1,10 @@
#[allow(unused_imports)]
#[cfg(target_os = "linux")]
mod linux_terminal;
#[cfg(target_os = "linux")]
pub use linux_terminal::*;
#[cfg(target_os = "windows")]
mod windows_terminal;
#[cfg(target_os = "windows")]
pub use windows_terminal::*;

12
src/sub_windows/mod.rs Normal file
View file

@ -0,0 +1,12 @@
#[allow(unused_imports)]
mod confirm;
pub use confirm::*;
mod search;
pub use search::*;
mod settings;
pub use settings::*;
mod shortcuts;
pub use shortcuts::*;

View file

@ -1,7 +1,7 @@
use eframe::egui; use eframe::egui;
use std::cmp::min; use std::cmp::min;
use crate::tools::{tabs::Tab, tabs::TabNumber}; use crate::panels::{Tab, TabNumber};
use crate::RED; use crate::RED;
enum Action { enum Action {

View file

@ -1,5 +1,4 @@
use crate::ColorTheme; use crate::editor::{themes::DEFAULT_THEMES, ColorTheme};
use crate::DEFAULT_THEMES;
use eframe::egui; use eframe::egui;
pub struct SettingsWindow { pub struct SettingsWindow {

View file

@ -1,100 +0,0 @@
use std::ffi::OsStr;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use crate::tools;
use crate::ALLOWED_FILE_EXTENSIONS;
#[derive(Clone)]
pub struct File {
pub name: String,
pub path: PathBuf,
pub folder_content: Option<Vec<File>>,
pub folder_open: bool,
}
impl File {
pub fn new_file(name: String, path: PathBuf) -> Self {
Self {
name,
path,
folder_content: None,
folder_open: false,
}
}
}
pub fn generate_file_tree(path: &Path, depth: isize) -> Option<File> {
if let Some(file_name) = path.file_name() {
if file_name.to_string_lossy().starts_with('.') {
return None;
}
let extension = path.extension().and_then(|ext| ext.to_str());
if !ALLOWED_FILE_EXTENSIONS.contains(&extension.unwrap_or_default()) {
return None;
}
} else {
return None;
}
let name = path
.file_name()
.unwrap_or_else(|| OsStr::new(""))
.to_string_lossy()
.into_owned();
if !path.is_dir() || depth < 0 {
return Some(File::new_file(name, path.to_path_buf()));
}
match fs::read_dir(path) {
Err(err) => {
Some(File::new_file(
format!("Error reading directory: {}", err),
path.to_path_buf(),
))
}
Ok(entries) => {
let mut paths: Vec<Result<fs::DirEntry, io::Error>> = entries
.map(|r| r.map_err(|e| io::Error::new(io::ErrorKind::Other, e)))
.collect();
paths.sort_by(|a, b| match (a, b) {
(Ok(entry_a), Ok(entry_b)) => tools::sort_directories_first(entry_a, entry_b),
(Err(_), Ok(_)) => std::cmp::Ordering::Greater,
(Ok(_), Err(_)) => std::cmp::Ordering::Less,
(Err(_), Err(_)) => std::cmp::Ordering::Equal,
});
let mut folder_content = Vec::new();
for result in paths {
match result {
Ok(entry) => {
if let Some(file) = generate_file_tree(&entry.path(), depth - 1) {
folder_content.push(file);
}
}
Err(err) => {
folder_content.push(File::new_file(
format!("Error reading entry: {}", err),
path.to_path_buf(),
));
}
}
}
if folder_content.is_empty() {
return None;
}
Some(File {
name,
path: path.to_path_buf(),
folder_content: Some(folder_content),
folder_open: false,
})
}
}
}

View file

@ -1,130 +0,0 @@
use crate::calcifer::code_editor::Syntax;
use crate::DISPLAY_PATH_DEPTH;
use eframe::egui;
use egui::Color32;
use image::GenericImageView;
use serde::{Deserialize, Serialize};
use std::{
cmp::Ordering, error::Error, ffi::OsStr, fs, fs::read_to_string, fs::OpenOptions, io::Write,
path::Component, path::Path, path::PathBuf,
};
//my tools;
pub mod confirm;
pub mod file_tree;
pub mod search;
pub mod settings;
pub mod shortcuts;
#[cfg(target_os = "linux")]
pub mod terminal;
#[cfg(target_os = "linux")]
pub use terminal::*;
#[cfg(target_os = "windows")]
pub mod windows_terminal;
#[cfg(target_os = "windows")]
pub use windows_terminal::*;
pub mod tabs;
pub use tabs::*;
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
pub struct AppState {
pub tabs: Vec<PathBuf>,
pub theme: usize,
}
pub fn save_state(state: &AppState, file_path: &Path) -> Result<(), std::io::Error> {
let serialized_state = serde_json::to_string(state)?;
if let Some(parent_dir) = file_path.parent() {
fs::create_dir_all(parent_dir)?;
}
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(file_path)?;
file.write_all(serialized_state.as_bytes())?;
println!("Saved state at {}", file_path.display());
Ok(())
}
pub fn load_state(file_path: &Path) -> Result<AppState, std::io::Error> {
let serialized_state = read_to_string(file_path)?;
Ok(serde_json::from_str(&serialized_state)?)
}
pub fn load_icon() -> Result<egui::IconData, Box<dyn Error>> {
let (icon_rgba, icon_width, icon_height) = {
let icon = include_bytes!("../../assets/icon.png");
let image = image::load_from_memory(icon)?;
let rgba = image.clone().into_rgba8().to_vec();
let (width, height) = image.dimensions();
(rgba, width, height)
};
Ok(egui::IconData {
rgba: icon_rgba,
width: icon_width,
height: icon_height,
})
}
pub fn to_syntax(language: &str) -> Syntax {
match language {
"py" => Syntax::python(),
"rs" => Syntax::rust(),
_ => Syntax::shell(),
}
}
pub fn sort_directories_first(a: &std::fs::DirEntry, b: &std::fs::DirEntry) -> Ordering {
let a_is_dir = a.path().is_dir();
let b_is_dir = b.path().is_dir();
// Directories come first, then files
if a_is_dir && !b_is_dir {
Ordering::Less
} else if !a_is_dir && b_is_dir {
Ordering::Greater
} else {
// Both are either directories or files, sort alphabetically
a.path().cmp(&b.path())
}
}
pub fn format_path(path: &Path) -> String {
let components: Vec<&OsStr> = path
.components()
.rev()
.take(DISPLAY_PATH_DEPTH)
.filter_map(|component| match component {
Component::RootDir | Component::CurDir => None,
_ => Some(component.as_os_str()),
})
.collect();
format!(
"{}>",
components
.iter()
.rev()
.map(|&c| c.to_string_lossy())
.collect::<Vec<_>>()
.join("/")
)
}
pub fn hex_str_to_color(hex_str: &str) -> Color32 {
Color32::from_hex(hex_str).unwrap_or_else(|_| Color32::BLACK)
}
#[cfg(test)]
mod tests;

View file

@ -1,184 +0,0 @@
use crate::tools::format_path;
use arboard::Clipboard;
use nix::fcntl::{fcntl, FcntlArg, OFlag};
use std::{
env, io::{BufRead, BufReader},
os::fd::AsRawFd,
path::{Path, PathBuf},
process::{Child, Command, Stdio},
};
pub struct Buffer {
pub output_buffer: BufReader<std::process::ChildStdout>,
pub error_buffer: BufReader<std::process::ChildStderr>,
pub child: Child,
}
#[derive(Clone)]
pub struct Line {
pub text: String,
pub error: bool,
}
impl Line {
fn output(text: String) -> Self {
Self {
text: remove_line_break(text),
error: false,
}
}
fn error(text: String) -> Self {
Self {
text: remove_line_break(text),
error: true,
}
}
}
pub struct CommandEntry {
pub env: String,
pub command: String,
pub result: Vec<Line>,
pub buffer: Option<Buffer>,
pub finished: bool,
}
impl CommandEntry {
pub fn new(env: String, command: String) -> Self {
let (buffer, result) = match execute(command.clone()) {
Ok(command_buffer) => (Some(command_buffer), vec![]),
Err(err) => (
None,
vec![Line::error(format!("failed to get results: {}", err))],
),
};
CommandEntry {
env,
command,
result,
buffer,
finished: false,
}
}
pub fn update(&mut self) {
if let Some(buffer) = &mut self.buffer {
let mut output = String::new();
let _ = buffer.output_buffer.read_line(&mut output);
if !remove_line_break(output.to_string()).is_empty() {
self.result.push(Line::output(format!("{}\n", output)));
}
let mut error = String::new();
let _ = buffer.error_buffer.read_line(&mut error);
if !remove_line_break(error.to_string()).is_empty() {
self.result.push(Line::error(format!("{}\n", error)));
}
}
if let Some(buffer) = &mut self.buffer {
if let Ok(status) = buffer.child.try_wait() {
if let Some(_exit_status) = status {
//self.result.push(Line::output(format!("Command finished with status: {:?}\n", exit_status)));
self.finished = true;
}
}
}
}
pub fn copy_error_code(&self) {
let mut txt : String = "".to_string();
for line in self.result.iter() {
if line.error {
txt.push_str(&format!("{}\n", line.text));
}
}
let mut _clipboard = Clipboard::new().expect("Failed to initialize clipboard");
_clipboard.set_text(txt).unwrap();
}
}
pub fn send_command(command: String) -> CommandEntry {
let env = format_path(&env::current_dir().unwrap_or_else(|_| PathBuf::from("/")));
if command.len() < 2 {
return CommandEntry::new(env, command);
}
if &command[..2] != "cd" {
return CommandEntry::new(env, command);
}
if command.len() < 4 {
let mut entry =
CommandEntry::new(env, "echo Invalid cd, should provide path >&2".to_string());
entry.command = command;
return entry;
}
let path_append = command[3..].replace('~', "/home/penwing");
let path = Path::new(&path_append);
if format!("{}", path.display()) == "/" {
let mut entry = CommandEntry::new(env, "echo Root access denied >&2".to_string());
entry.command = command;
return entry;
}
if env::set_current_dir(path).is_ok() {
let mut entry = CommandEntry::new(env, format!("echo Moved to : {}", path.display()));
entry.command = command;
entry
} else {
let mut entry = CommandEntry::new(
env,
format!("echo Could not find path : {} >&2", path.display()),
);
entry.command = command;
entry
}
}
pub fn execute(command: String) -> Result<Buffer, std::io::Error> {
let mut child = Command::new("sh")
.arg("-c")
.arg(command.clone())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let stdout = child
.stdout
.take()
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "Failed to open stdout"))?;
let stderr = child
.stderr
.take()
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::Other, "Failed to open stderr"))?;
let stdout_fd = stdout.as_raw_fd();
let stderr_fd = stderr.as_raw_fd();
fcntl(stdout_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))?;
fcntl(stderr_fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))?;
let output_buffer = BufReader::new(stdout);
let error_buffer = BufReader::new(stderr);
Ok(Buffer {
output_buffer,
error_buffer,
child,
})
}
fn remove_line_break(input: String) -> String {
let mut text = input.clone();
while text.ends_with('\n') {
text.pop();
if text.ends_with('\r') {
text.pop();
}
}
text
}

View file

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