chronological terminal result (error and output interlaced)

This commit is contained in:
Penwing 2024-01-27 12:34:55 +01:00
parent fa2104b9de
commit 8436619c0e
6 changed files with 737 additions and 707 deletions

View file

@ -45,3 +45,4 @@ Async terminal !
Real Ui Real Ui
# 1.1.0 : # 1.1.0 :
Better error handling

View file

@ -15,277 +15,277 @@ 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() if let Some(path) = rfd::FileDialog::new()
.set_directory(Path::new(&PATH_ROOT)) .set_directory(Path::new(&PATH_ROOT))
.pick_file() .pick_file()
{ {
self.open_file(Some(&path)); self.open_file(Some(&path));
} }
} }
ui.separator(); ui.separator();
self.tree_visible = self.toggle(ui, self.tree_visible, "🗐"); self.tree_visible = self.toggle(ui, self.tree_visible, "🗐");
ui.separator(); ui.separator();
self.terminal_visible = self.toggle(ui, self.terminal_visible, "🖵"); self.terminal_visible = self.toggle(ui, self.terminal_visible, "🖵");
ui.separator(); ui.separator();
self.search_menu.visible = self.toggle(ui, self.search_menu.visible, "🔍"); self.search_menu.visible = self.toggle(ui, self.search_menu.visible, "🔍");
ui.separator(); ui.separator();
self.settings_menu.visible = self.toggle(ui, self.settings_menu.visible, ""); self.settings_menu.visible = self.toggle(ui, self.settings_menu.visible, "");
ui.separator(); ui.separator();
self.shortcuts_menu.visible = self.toggle(ui, self.shortcuts_menu.visible, ""); self.shortcuts_menu.visible = self.toggle(ui, self.shortcuts_menu.visible, "");
ui.separator(); ui.separator();
self.profiler_visible = self.toggle(ui, self.profiler_visible, ""); self.profiler_visible = self.toggle(ui, self.profiler_visible, "");
if self.tabs[self.selected_tab.to_index()].language == PROJECT_EXTENSION { if self.tabs[self.selected_tab.to_index()].language == PROJECT_EXTENSION {
ui.separator(); ui.separator();
self.project_mode = self.toggle(ui, self.project_mode, "🛠"); self.project_mode = self.toggle(ui, self.project_mode, "🛠");
} }
}); });
}); });
} }
pub fn draw_tree_panel(&mut self, ctx: &egui::Context) { pub fn draw_tree_panel(&mut self, ctx: &egui::Context) {
if !self.tree_visible { if !self.tree_visible {
return; return;
} }
egui::SidePanel::left("file_tree_panel").show(ctx, |ui| { egui::SidePanel::left("file_tree_panel").show(ctx, |ui| {
ui.heading("Bookshelf"); ui.heading("Bookshelf");
ui.separator(); ui.separator();
let _ = self.list_files(ui, Path::new(&PATH_ROOT)); let _ = self.list_files(ui, Path::new(&PATH_ROOT));
ui.separator(); ui.separator();
}); });
} }
pub fn draw_bottom_tray(&mut self, ctx: &egui::Context) { pub fn draw_bottom_tray(&mut self, ctx: &egui::Context) {
egui::TopBottomPanel::bottom("tray") egui::TopBottomPanel::bottom("tray")
.default_height(self.font_size * 1.2) .default_height(self.font_size * 1.2)
.resizable(false) .resizable(false)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.label(self.profiler()); ui.label(self.profiler());
}); });
} }
pub fn draw_terminal_panel(&mut self, ctx: &egui::Context) { pub fn draw_terminal_panel(&mut self, ctx: &egui::Context) {
if !self.terminal_visible { if !self.terminal_visible {
return; return;
} }
egui::TopBottomPanel::bottom("terminal") egui::TopBottomPanel::bottom("terminal")
.default_height(super::TERMINAL_HEIGHT) .default_height(super::TERMINAL_HEIGHT)
.height_range(Rangef::new( .height_range(Rangef::new(
super::TERMINAL_RANGE.start, super::TERMINAL_RANGE.start,
super::TERMINAL_RANGE.end, super::TERMINAL_RANGE.end,
)) ))
.resizable(true) .resizable(true)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
let command_color = hex_str_to_color(self.theme.functions); let command_color = hex_str_to_color(self.theme.functions);
let entry_color = hex_str_to_color(self.theme.literals); let entry_color = hex_str_to_color(self.theme.literals);
let bg_color = hex_str_to_color(self.theme.bg); let bg_color = hex_str_to_color(self.theme.bg);
ui.label(""); ui.label("");
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.add(egui::Button::new("")).clicked() { if ui.add(egui::Button::new("")).clicked() {
self.command_history = vec![]; self.command_history = vec![];
} }
ui.style_mut().visuals.extreme_bg_color = bg_color; ui.style_mut().visuals.extreme_bg_color = bg_color;
let Self { command, .. } = self; let Self { command, .. } = self;
ui.colored_label( ui.colored_label(
command_color, command_color,
tools::format_path( tools::format_path(
&env::current_dir().unwrap_or_else(|_| PathBuf::from("/")), &env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
), ),
); );
let response = ui.add( let response = ui.add(
egui::TextEdit::singleline(command) egui::TextEdit::singleline(command)
.desired_width(f32::INFINITY) .desired_width(f32::INFINITY)
.lock_focus(true), .lock_focus(true),
); );
if response.lost_focus() && ctx.input(|i| i.key_pressed(egui::Key::Enter)) { if response.lost_focus() && ctx.input(|i| i.key_pressed(egui::Key::Enter)) {
self.command_history self.command_history
.push(tools::send_command(self.command.clone())); .push(tools::send_command(self.command.clone()));
self.command = "".into(); self.command = "".into();
response.request_focus(); response.request_focus();
} }
}); });
ui.separator(); ui.separator();
egui::ScrollArea::vertical() egui::ScrollArea::vertical()
.stick_to_bottom(true) .stick_to_bottom(true)
.show(ui, |ui| { .show(ui, |ui| {
ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| {
ui.separator(); ui.separator();
ui.horizontal_wrapped(|ui| { ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.y = 0.0; ui.spacing_mut().item_spacing.y = 0.0;
for entry in &mut self.command_history { for entry in &mut self.command_history {
entry.update(); entry.update();
ui.colored_label( ui.colored_label(
command_color, command_color,
format!("\n{} {}", entry.env, entry.command), format!("\n{} {}", entry.env, entry.command),
); );
ui.end_row(); ui.end_row();
if !entry.output.is_empty() { for line in &entry.result {
ui.colored_label(entry_color, &entry.output); let color =
ui.end_row(); if line.error { super::RED } else { entry_color };
} ui.colored_label(color, &line.text);
if !entry.error.is_empty() { ui.end_row();
ui.colored_label(super::RED, &entry.error); }
ui.end_row(); }
} });
} });
}); });
}); });
}); });
}); }
});
}
pub fn draw_tab_panel(&mut self, ctx: &egui::Context) { pub fn draw_tab_panel(&mut self, ctx: &egui::Context) {
egui::TopBottomPanel::top("tabs") egui::TopBottomPanel::top("tabs")
.resizable(false) .resizable(false)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.style_mut().visuals.selection.bg_fill = ui.style_mut().visuals.selection.bg_fill =
hex_str_to_color(self.theme.functions); hex_str_to_color(self.theme.functions);
ui.style_mut().visuals.hyperlink_color = 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() { for (index, tab) in self.tabs.clone().iter().enumerate() {
let mut title = tab.get_name(); let mut title = tab.get_name();
if !tab.saved { if !tab.saved {
title += " ~"; title += " ~";
} }
if self.selected_tab == tools::TabNumber::from_index(index) { if self.selected_tab == tools::TabNumber::from_index(index) {
ui.style_mut().visuals.override_text_color = ui.style_mut().visuals.override_text_color =
Some(hex_str_to_color(self.theme.bg)); Some(hex_str_to_color(self.theme.bg));
} }
ui.selectable_value( ui.selectable_value(
&mut self.selected_tab, &mut self.selected_tab,
tools::TabNumber::from_index(index), tools::TabNumber::from_index(index),
title, title,
); );
ui.style_mut().visuals.override_text_color = None; ui.style_mut().visuals.override_text_color = None;
if ui.link("X").clicked() && !self.close_tab_confirm.visible { if ui.link("X").clicked() && !self.close_tab_confirm.visible {
if self.tabs.len() > 1 { if self.tabs.len() > 1 {
if tab.saved { if tab.saved {
self.delete_tab(index); self.delete_tab(index);
} else { } else {
self.close_tab_confirm.ask(); self.close_tab_confirm.ask();
self.tab_to_close = index; self.tab_to_close = index;
} }
} else { } else {
egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::Close); egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::Close);
} }
} }
ui.separator(); ui.separator();
} }
if self.tabs.len() < MAX_TABS { if self.tabs.len() < MAX_TABS {
ui.selectable_value(&mut self.selected_tab, tools::TabNumber::Open, "+"); ui.selectable_value(&mut self.selected_tab, tools::TabNumber::Open, "+");
} }
if self.selected_tab == tools::TabNumber::Open { if self.selected_tab == tools::TabNumber::Open {
self.open_file(None); self.open_file(None);
} }
}); });
}); });
} }
pub fn draw_content_panel(&mut self, ctx: &egui::Context) { pub fn draw_content_panel(&mut self, ctx: &egui::Context) {
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.add(egui::Button::new("open in terminal")).clicked() { if ui.add(egui::Button::new("open in terminal")).clicked() {
let mut path = self.tabs[self.selected_tab.to_index()].path.clone(); let mut path = self.tabs[self.selected_tab.to_index()].path.clone();
path.pop(); path.pop();
tools::send_command(format!("cd {}", path.display())); tools::send_command(format!("cd {}", path.display()));
} }
ui.label("Picked file:"); ui.label("Picked file:");
ui.monospace( ui.monospace(
self.tabs[self.selected_tab.to_index()] self.tabs[self.selected_tab.to_index()]
.path .path
.to_string_lossy() .to_string_lossy()
.to_string(), .to_string(),
); );
}); });
ui.separator(); ui.separator();
if self.project_mode && self.tabs[self.selected_tab.to_index()].language == PROJECT_EXTENSION { if self.project_mode
self.draw_project_file(ui); && self.tabs[self.selected_tab.to_index()].language == PROJECT_EXTENSION
} else { {
self.draw_code_file(ui); self.draw_project_file(ui);
} } else {
}); self.draw_code_file(ui);
} }
});
}
fn draw_code_file(&mut self, ui: &mut egui::Ui) { fn draw_code_file(&mut self, ui: &mut egui::Ui) {
let current_tab = &mut self.tabs[self.selected_tab.to_index()]; let current_tab = &mut self.tabs[self.selected_tab.to_index()];
let lines = current_tab.code.chars().filter(|&c| c == '\n').count() + 1; let lines = current_tab.code.chars().filter(|&c| c == '\n').count() + 1;
let mut override_cursor: Option<CCursorRange> = None; let mut override_cursor: Option<CCursorRange> = None;
if !self.search_menu.result_selected { if !self.search_menu.result_selected {
override_cursor = Some(CCursorRange::two( override_cursor = Some(CCursorRange::two(
CCursor::new(self.search_menu.get_cursor_start()), CCursor::new(self.search_menu.get_cursor_start()),
CCursor::new(self.search_menu.get_cursor_end()), CCursor::new(self.search_menu.get_cursor_end()),
)); ));
self.search_menu.result_selected = true; self.search_menu.result_selected = true;
} }
CodeEditor::default() CodeEditor::default()
.id_source("code editor") .id_source("code editor")
.with_rows(max(45, lines)) .with_rows(max(45, lines))
.with_fontsize(self.font_size) .with_fontsize(self.font_size)
.with_theme(self.theme) .with_theme(self.theme)
.with_syntax(tools::to_syntax(&current_tab.language)) .with_syntax(tools::to_syntax(&current_tab.language))
.with_numlines(true) .with_numlines(true)
.show( .show(
ui, ui,
&mut current_tab.code, &mut current_tab.code,
&mut current_tab.saved, &mut current_tab.saved,
&mut current_tab.last_cursor, &mut current_tab.last_cursor,
&mut current_tab.scroll_offset, &mut current_tab.scroll_offset,
override_cursor, override_cursor,
); );
} }
fn draw_project_file(&mut self, ui: &mut egui::Ui) { fn draw_project_file(&mut self, ui: &mut egui::Ui) {
ui.label("project mode"); ui.label("project mode");
} }
pub fn draw_windows(&mut self, ctx: &egui::Context) { pub fn draw_windows(&mut self, ctx: &egui::Context) {
if self.search_menu.visible { if self.search_menu.visible {
self.search_menu self.search_menu
.show(ctx, &mut self.tabs, &mut self.selected_tab); .show(ctx, &mut self.tabs, &mut self.selected_tab);
} }
if self.close_tab_confirm.visible { if self.close_tab_confirm.visible {
self.close_tab_confirm.show(ctx); self.close_tab_confirm.show(ctx);
} }
if self.refresh_confirm.visible { if self.refresh_confirm.visible {
self.refresh_confirm.show(ctx); self.refresh_confirm.show(ctx);
} }
if self.exit_confirm.visible { if self.exit_confirm.visible {
self.exit_confirm.show(ctx); self.exit_confirm.show(ctx);
} }
if self.exit_confirm.proceed { if self.exit_confirm.proceed {
for tab in self.tabs.iter_mut() { for tab in self.tabs.iter_mut() {
tab.saved = true; tab.saved = true;
} }
egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::Close); egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::Close);
} }
if self.shortcuts_menu.visible { if self.shortcuts_menu.visible {
self.shortcuts_menu.show(ctx); self.shortcuts_menu.show(ctx);
} }
if self.settings_menu.visible { if self.settings_menu.visible {
self.settings_menu.show(ctx); self.settings_menu.show(ctx);
} }
if self.settings_menu.updated { if self.settings_menu.updated {
self.theme = self.settings_menu.theme; self.theme = self.settings_menu.theme;
} }
self.handle_confirm(); self.handle_confirm();
} }
} }

View file

@ -12,226 +12,226 @@ use crate::TIME_LABELS;
use tools::hex_str_to_color; use tools::hex_str_to_color;
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 { if self.refresh_confirm.proceed {
self.refresh_confirm.close(); self.refresh_confirm.close();
self.tabs[self.selected_tab.to_index()].refresh(); self.tabs[self.selected_tab.to_index()].refresh();
} }
} }
pub fn save_tab(&self) -> Option<PathBuf> { pub fn save_tab(&self) -> Option<PathBuf> {
if self.tabs[self.selected_tab.to_index()] if self.tabs[self.selected_tab.to_index()]
.path .path
.file_name() .file_name()
.map_or(true, |name| name.to_string_lossy() == "untitled") .map_or(true, |name| name.to_string_lossy() == "untitled")
{ {
self.save_tab_as() self.save_tab_as()
} else { } else {
if let Err(err) = fs::write( if let Err(err) = fs::write(
&self.tabs[self.selected_tab.to_index()].path, &self.tabs[self.selected_tab.to_index()].path,
&self.tabs[self.selected_tab.to_index()].code, &self.tabs[self.selected_tab.to_index()].code,
) { ) {
eprintln!("Error writing file: {}", err); eprintln!("Error writing file: {}", err);
return None; return None;
} }
Some(self.tabs[self.selected_tab.to_index()].path.clone()) Some(self.tabs[self.selected_tab.to_index()].path.clone())
} }
} }
pub fn save_tab_as(&self) -> Option<PathBuf> { pub fn save_tab_as(&self) -> Option<PathBuf> {
if let Some(path) = rfd::FileDialog::new() if let Some(path) = rfd::FileDialog::new()
.set_directory(Path::new(&PATH_ROOT)) .set_directory(Path::new(&PATH_ROOT))
.save_file() .save_file()
{ {
if let Err(err) = fs::write(&path, &self.tabs[self.selected_tab.to_index()].code) { if let Err(err) = fs::write(&path, &self.tabs[self.selected_tab.to_index()].code) {
eprintln!("Error writing file: {}", err); eprintln!("Error writing file: {}", err);
return None; return None;
} }
return Some(path); return Some(path);
} }
None None
} }
pub fn handle_save_file(&mut self, path_option: Option<PathBuf>) { pub fn handle_save_file(&mut self, path_option: Option<PathBuf>) {
if let Some(path) = path_option { if let Some(path) = path_option {
println!("File saved successfully at: {:?}", path); println!("File saved successfully at: {:?}", path);
self.tabs[self.selected_tab.to_index()].path = path; self.tabs[self.selected_tab.to_index()].path = path;
self.tabs[self.selected_tab.to_index()].saved = true; self.tabs[self.selected_tab.to_index()].saved = true;
} else { } else {
println!("File save failed."); println!("File save failed.");
} }
} }
pub fn from_app_state(app_state: tools::AppState) -> Self { pub fn from_app_state(app_state: tools::AppState) -> Self {
let mut new = Self { let mut new = Self {
theme: DEFAULT_THEMES[min(app_state.theme, DEFAULT_THEMES.len() - 1)], theme: DEFAULT_THEMES[min(app_state.theme, DEFAULT_THEMES.len() - 1)],
tabs: Vec::new(), tabs: Vec::new(),
settings_menu: tools::settings::SettingsWindow::new(DEFAULT_THEMES[app_state.theme]), settings_menu: tools::settings::SettingsWindow::new(DEFAULT_THEMES[app_state.theme]),
..Default::default() ..Default::default()
}; };
for path in app_state.tabs { for path in app_state.tabs {
if !path if !path
.file_name() .file_name()
.map_or(true, |name| name.to_string_lossy() == "untitled") .map_or(true, |name| name.to_string_lossy() == "untitled")
{ {
new.open_file(Some(&path)); new.open_file(Some(&path));
} }
} }
if new.tabs == vec![] { if new.tabs == vec![] {
new.open_file(None); new.open_file(None);
} }
new new
} }
pub fn save_state(&self) { pub fn save_state(&self) {
let mut state_theme: usize = 0; let mut state_theme: usize = 0;
if let Some(theme) = DEFAULT_THEMES.iter().position(|&r| r == self.theme) { if let Some(theme) = DEFAULT_THEMES.iter().position(|&r| r == self.theme) {
state_theme = theme; state_theme = theme;
} }
let mut state_tabs = vec![]; let mut state_tabs = vec![];
for tab in &self.tabs { for tab in &self.tabs {
state_tabs.push(tab.path.clone()); state_tabs.push(tab.path.clone());
} }
let app_state = tools::AppState { let app_state = tools::AppState {
tabs: state_tabs, tabs: state_tabs,
theme: state_theme, theme: state_theme,
}; };
let _ = tools::save_state(&app_state, SAVE_PATH); let _ = tools::save_state(&app_state, SAVE_PATH);
} }
pub fn move_through_tabs(&mut self, forward: bool) { pub fn move_through_tabs(&mut self, forward: bool) {
let new_index = if forward { let new_index = if forward {
(self.selected_tab.to_index() + 1) % self.tabs.len() (self.selected_tab.to_index() + 1) % self.tabs.len()
} else { } else {
self.selected_tab self.selected_tab
.to_index() .to_index()
.checked_sub(1) .checked_sub(1)
.unwrap_or(self.tabs.len() - 1) .unwrap_or(self.tabs.len() - 1)
}; };
self.selected_tab = tools::TabNumber::from_index(new_index); self.selected_tab = tools::TabNumber::from_index(new_index);
} }
pub fn list_files(&mut self, ui: &mut egui::Ui, path: &Path) -> io::Result<()> { pub fn list_files(&mut self, ui: &mut egui::Ui, path: &Path) -> io::Result<()> {
if path.file_name().is_none() { if path.file_name().is_none() {
return Ok(()); return Ok(());
} }
let name = path let name = path
.file_name() .file_name()
.unwrap_or_else(|| OsStr::new("")) .unwrap_or_else(|| OsStr::new(""))
.to_string_lossy() .to_string_lossy()
.into_owned(); .into_owned();
if !path.is_dir() { if !path.is_dir() {
if ui.button(name).clicked() { if ui.button(name).clicked() {
self.open_file(Some(path)); self.open_file(Some(path));
} }
return Ok(()); return Ok(());
} }
egui::CollapsingHeader::new(name).show(ui, |ui| match fs::read_dir(path) { egui::CollapsingHeader::new(name).show(ui, |ui| match fs::read_dir(path) {
Err(err) => { Err(err) => {
ui.label(format!("Error reading directory: {}", err)); ui.label(format!("Error reading directory: {}", err));
} }
Ok(entries) => { Ok(entries) => {
let mut paths: Vec<Result<fs::DirEntry, io::Error>> = entries let mut paths: Vec<Result<fs::DirEntry, io::Error>> = entries
.map(|r| r.map_err(|e| io::Error::new(io::ErrorKind::Other, e))) .map(|r| r.map_err(|e| io::Error::new(io::ErrorKind::Other, e)))
.collect(); .collect();
paths.sort_by(|a, b| match (a, b) { paths.sort_by(|a, b| match (a, b) {
(Ok(entry_a), Ok(entry_b)) => tools::sort_directories_first(entry_a, entry_b), (Ok(entry_a), Ok(entry_b)) => tools::sort_directories_first(entry_a, entry_b),
(Err(_), Ok(_)) => std::cmp::Ordering::Greater, (Err(_), Ok(_)) => std::cmp::Ordering::Greater,
(Ok(_), Err(_)) => std::cmp::Ordering::Less, (Ok(_), Err(_)) => std::cmp::Ordering::Less,
(Err(_), Err(_)) => std::cmp::Ordering::Equal, (Err(_), Err(_)) => std::cmp::Ordering::Equal,
}); });
for result in paths { for result in paths {
match result { match result {
Ok(entry) => { Ok(entry) => {
let _ = self.list_files(ui, &entry.path()); let _ = self.list_files(ui, &entry.path());
} }
Err(err) => { Err(err) => {
ui.label(format!("Error processing directory entry: {}", err)); ui.label(format!("Error processing directory entry: {}", err));
} }
} }
} }
} }
}); });
Ok(()) Ok(())
} }
pub fn open_file(&mut self, path_option: Option<&Path>) { pub fn open_file(&mut self, path_option: Option<&Path>) {
if let Some(path) = path_option.clone() { if let Some(path) = path_option.clone() {
for (index, tab) in self.tabs.clone().iter().enumerate() { for (index, tab) in self.tabs.clone().iter().enumerate() {
if tab.path == path { if tab.path == path {
self.selected_tab = tools::TabNumber::from_index(index); self.selected_tab = tools::TabNumber::from_index(index);
return return;
} }
} }
} }
if self.tabs.len() < MAX_TABS { if self.tabs.len() < MAX_TABS {
if let Some(path) = path_option { if let Some(path) = path_option {
self.tabs.push(tools::Tab::new(path.to_path_buf())); self.tabs.push(tools::Tab::new(path.to_path_buf()));
} else { } else {
self.tabs.push(tools::Tab::default()); self.tabs.push(tools::Tab::default());
} }
self.selected_tab = tools::TabNumber::from_index(self.tabs.len() - 1); self.selected_tab = tools::TabNumber::from_index(self.tabs.len() - 1);
} }
} }
pub fn delete_tab(&mut self, index: usize) { pub fn delete_tab(&mut self, index: usize) {
self.tabs.remove(index); self.tabs.remove(index);
self.selected_tab = tools::TabNumber::from_index(min(index, self.tabs.len() - 1)); 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 { pub fn toggle(&self, ui: &mut egui::Ui, display: bool, title: &str) -> bool {
let bg_color: Color32; let bg_color: Color32;
let text_color: Color32; let text_color: Color32;
if display { if display {
bg_color = hex_str_to_color(self.theme.functions); bg_color = hex_str_to_color(self.theme.functions);
text_color = hex_str_to_color(self.theme.bg); text_color = hex_str_to_color(self.theme.bg);
} else { } else {
bg_color = hex_str_to_color(self.theme.bg); bg_color = hex_str_to_color(self.theme.bg);
text_color = hex_str_to_color(self.theme.literals); text_color = hex_str_to_color(self.theme.literals);
}; };
ui.style_mut().visuals.override_text_color = Some(text_color); ui.style_mut().visuals.override_text_color = Some(text_color);
if ui.add(egui::Button::new(title).fill(bg_color)).clicked() { if ui.add(egui::Button::new(title).fill(bg_color)).clicked() {
return !display; return !display;
} }
ui.style_mut().visuals.override_text_color = None; ui.style_mut().visuals.override_text_color = None;
display display
} }
pub fn profiler(&self) -> String { pub fn profiler(&self) -> String {
if !self.profiler_visible { if !self.profiler_visible {
return "".to_string(); return "".to_string();
} }
let combined_string: Vec<String> = TIME_LABELS let combined_string: Vec<String> = TIME_LABELS
.into_iter() .into_iter()
.zip(self.time_watch.clone()) .zip(self.time_watch.clone())
.map(|(s, v)| format!("{} : {:.1} ms", s, v)) .map(|(s, v)| format!("{} : {:.1} ms", s, v))
.collect(); .collect();
let mut result = combined_string.join(" ; "); let mut result = combined_string.join(" ; ");
result.push_str(&format!( result.push_str(&format!(
" total : {:.1} ms", " total : {:.1} ms",
self.time_watch.clone().iter().sum::<f32>() self.time_watch.clone().iter().sum::<f32>()
)); ));
result result
} }
} }

View file

@ -12,25 +12,25 @@ 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 PROJECT_EXTENSION : &str = "project"; const PROJECT_EXTENSION: &str = "project";
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] = [ const TIME_LABELS: [&str; 7] = [
"input", "settings", "tree", "terminal", "tabs", "content", "windows", "input", "settings", "tree", "terminal", "tabs", "content", "windows",
]; ];
const MAX_FPS: f32 = 30.0; const MAX_FPS: f32 = 30.0;
const PATH_ROOT: &str = "/home/penwing/Documents/"; const PATH_ROOT: &str = "/home/penwing/Documents/";
@ -38,215 +38,215 @@ 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 = tools::load_icon().unwrap_or_default();
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()
}; };
// Attempt to load previous state // Attempt to load previous state
let app_state: tools::AppState = if Path::new(SAVE_PATH).exists() { let app_state: tools::AppState = if Path::new(SAVE_PATH).exists() {
match tools::load_state(SAVE_PATH) { match tools::load_state(SAVE_PATH) {
Ok(app_state) => app_state, Ok(app_state) => app_state,
Err(_) => tools::AppState::default(), Err(_) => tools::AppState::default(),
} }
} else { } else {
tools::AppState::default() tools::AppState::default()
}; };
eframe::run_native( eframe::run_native(
&format!("Calcifer{}", TITLE), &format!("Calcifer{}", TITLE),
options, options,
Box::new(move |_cc| Box::from(Calcifer::from_app_state(app_state))), 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,
project_mode: bool, project_mode: bool,
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,
project_mode: true, project_mode: true,
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( close_tab_confirm: tools::confirm::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: tools::confirm::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: tools::confirm::ConfirmWindow::new("", "Confirm Exit"),
search_menu: tools::search::SearchWindow::default(), search_menu: tools::search::SearchWindow::default(),
settings_menu: tools::settings::SettingsWindow::new(DEFAULT_THEMES[0]), settings_menu: tools::settings::SettingsWindow::new(DEFAULT_THEMES[0]),
shortcuts_menu: tools::shortcuts::ShortcutsWindow::new(), shortcuts_menu: tools::shortcuts::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(),
} }
} }
} }
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( thread::sleep(time::Duration::from_secs_f32(
((1.0 / MAX_FPS) - self.next_frame.elapsed().as_secs_f32()).max(0.0), ((1.0 / MAX_FPS) - self.next_frame.elapsed().as_secs_f32()).max(0.0),
)); ));
self.next_frame = time::Instant::now(); self.next_frame = time::Instant::now();
let mut watch = time::Instant::now(); let mut watch = time::Instant::now();
let mut style = (*ctx.style()).clone(); let mut style = (*ctx.style()).clone();
style.text_styles = [ style.text_styles = [
(Heading, FontId::new(self.font_size * 1.6, Proportional)), (Heading, FontId::new(self.font_size * 1.6, Proportional)),
(Body, FontId::new(self.font_size, Proportional)), (Body, FontId::new(self.font_size, Proportional)),
(Monospace, FontId::new(self.font_size, Proportional)), (Monospace, FontId::new(self.font_size, Proportional)),
(Button, FontId::new(self.font_size, Proportional)), (Button, FontId::new(self.font_size, Proportional)),
(Small, FontId::new(self.font_size, Proportional)), (Small, FontId::new(self.font_size, Proportional)),
] ]
.into(); .into();
ctx.set_style(style); ctx.set_style(style);
if ctx.input(|i| i.key_pressed(egui::Key::R) && i.modifiers.ctrl) if ctx.input(|i| i.key_pressed(egui::Key::R) && i.modifiers.ctrl)
&& !self.refresh_confirm.visible && !self.refresh_confirm.visible
{ {
if self.tabs[self.selected_tab.to_index()].saved { if self.tabs[self.selected_tab.to_index()].saved {
self.tabs[self.selected_tab.to_index()].refresh(); self.tabs[self.selected_tab.to_index()].refresh();
} else { } else {
self.refresh_confirm.ask(); self.refresh_confirm.ask();
} }
} }
if ctx.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) { if ctx.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl) {
self.handle_save_file(self.save_tab()); self.handle_save_file(self.save_tab());
} }
if ctx.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl && i.modifiers.shift) { if ctx.input(|i| i.key_pressed(egui::Key::S) && i.modifiers.ctrl && i.modifiers.shift) {
self.handle_save_file(self.save_tab_as()); self.handle_save_file(self.save_tab_as());
} }
if ctx.input(|i| i.key_pressed(egui::Key::ArrowLeft) && i.modifiers.alt) { if ctx.input(|i| i.key_pressed(egui::Key::ArrowLeft) && i.modifiers.alt) {
self.move_through_tabs(false); self.move_through_tabs(false);
} }
if ctx.input(|i| i.key_pressed(egui::Key::ArrowRight) && i.modifiers.alt) { if ctx.input(|i| i.key_pressed(egui::Key::ArrowRight) && i.modifiers.alt) {
self.move_through_tabs(true); self.move_through_tabs(true);
} }
if ctx.input(|i| i.zoom_delta() > 1.0) { if ctx.input(|i| i.zoom_delta() > 1.0) {
self.font_size = (self.font_size * 1.1).min(30.0); self.font_size = (self.font_size * 1.1).min(30.0);
} }
if ctx.input(|i| i.zoom_delta() < 1.0) { if ctx.input(|i| i.zoom_delta() < 1.0) {
self.font_size = (self.font_size / 1.1).max(10.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) { if ctx.input(|i| i.key_pressed(egui::Key::F) && i.modifiers.ctrl) {
self.search_menu.visible = !self.search_menu.visible; self.search_menu.visible = !self.search_menu.visible;
self.search_menu.initialized = !self.search_menu.visible; self.search_menu.initialized = !self.search_menu.visible;
} }
if ctx.input(|i| i.viewport().close_requested()) { if ctx.input(|i| i.viewport().close_requested()) {
let mut unsaved_tabs: Vec<usize> = vec![]; let mut unsaved_tabs: Vec<usize> = vec![];
for (index, tab) in self.tabs.iter().enumerate() { for (index, tab) in self.tabs.iter().enumerate() {
if !tab.saved { if !tab.saved {
unsaved_tabs.push(index); unsaved_tabs.push(index);
} }
} }
if !unsaved_tabs.is_empty() { if !unsaved_tabs.is_empty() {
let mut unsaved_tabs_names: String = "".to_string(); let mut unsaved_tabs_names: String = "".to_string();
for index in unsaved_tabs.iter() { for index in unsaved_tabs.iter() {
unsaved_tabs_names.push_str(&self.tabs[*index].get_name()); unsaved_tabs_names.push_str(&self.tabs[*index].get_name());
} }
egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::CancelClose); egui::Context::send_viewport_cmd(ctx, egui::ViewportCommand::CancelClose);
self.exit_confirm.prompt = format!( self.exit_confirm.prompt = format!(
"You have some unsaved changes :\n{}\nDo you still want to exit ?", "You have some unsaved changes :\n{}\nDo you still want to exit ?",
unsaved_tabs_names unsaved_tabs_names
); );
self.exit_confirm.ask(); self.exit_confirm.ask();
} }
} }
self.time_watch[0] = watch.elapsed().as_micros() as f32 / 1000.0; self.time_watch[0] = watch.elapsed().as_micros() as f32 / 1000.0;
watch = time::Instant::now(); watch = time::Instant::now();
self.draw_settings(ctx); self.draw_settings(ctx);
self.time_watch[1] = watch.elapsed().as_micros() as f32 / 1000.0; self.time_watch[1] = watch.elapsed().as_micros() as f32 / 1000.0;
watch = time::Instant::now(); watch = time::Instant::now();
self.draw_tree_panel(ctx); self.draw_tree_panel(ctx);
self.time_watch[2] = watch.elapsed().as_micros() as f32 / 1000.0; self.time_watch[2] = watch.elapsed().as_micros() as f32 / 1000.0;
watch = time::Instant::now(); watch = time::Instant::now();
self.draw_bottom_tray(ctx); self.draw_bottom_tray(ctx);
self.draw_terminal_panel(ctx); self.draw_terminal_panel(ctx);
self.time_watch[3] = watch.elapsed().as_micros() as f32 / 1000.0; self.time_watch[3] = watch.elapsed().as_micros() as f32 / 1000.0;
watch = time::Instant::now(); watch = time::Instant::now();
self.draw_tab_panel(ctx); self.draw_tab_panel(ctx);
self.time_watch[4] = watch.elapsed().as_micros() as f32 / 1000.0; self.time_watch[4] = watch.elapsed().as_micros() as f32 / 1000.0;
watch = time::Instant::now(); watch = time::Instant::now();
self.draw_content_panel(ctx); self.draw_content_panel(ctx);
self.time_watch[5] = watch.elapsed().as_micros() as f32 / 1000.0; self.time_watch[5] = watch.elapsed().as_micros() as f32 / 1000.0;
watch = time::Instant::now(); watch = time::Instant::now();
self.draw_windows(ctx); self.draw_windows(ctx);
self.time_watch[6] = watch.elapsed().as_micros() as f32 / 1000.0; self.time_watch[6] = watch.elapsed().as_micros() as f32 / 1000.0;
} }
fn on_exit(&mut self, _gl: std::option::Option<&eframe::glow::Context>) { fn on_exit(&mut self, _gl: std::option::Option<&eframe::glow::Context>) {
self.save_state(); self.save_state();
} }
} }

View file

@ -5,96 +5,96 @@ 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 {
let text = read_file_contents(&path).replace(&" ".repeat(4), "\t"); let text = read_file_contents(&path).replace(&" ".repeat(4), "\t");
let file_path = format_file_path(&path, &text); let file_path = format_file_path(&path, &text);
let extension = file_path let extension = file_path
.extension() .extension()
.and_then(|ext| ext.to_str()) .and_then(|ext| ext.to_str())
.unwrap_or_default(); .unwrap_or_default();
Self { Self {
path: file_path.clone(), path: file_path.clone(),
code: text, code: text,
language: extension.into(), language: extension.into(),
saved: true, saved: true,
scroll_offset: 0.0, scroll_offset: 0.0,
last_cursor: None, last_cursor: None,
} }
} }
pub fn get_name(&self) -> String { pub fn get_name(&self) -> String {
self.path self.path
.file_name() .file_name()
.map_or("untitled".to_string(), |name| { .map_or("untitled".to_string(), |name| {
name.to_string_lossy().to_string() name.to_string_lossy().to_string()
}) })
} }
pub fn refresh(&mut self) { pub fn refresh(&mut self) {
let text = read_file_contents(&self.path).replace(&" ".repeat(4), "\t"); let text = read_file_contents(&self.path).replace(&" ".repeat(4), "\t");
let file_path = format_file_path(&self.path, &text); let file_path = format_file_path(&self.path, &text);
self.code = text; self.code = text;
self.path = file_path; self.path = file_path;
self.saved = true; self.saved = true;
} }
} }
fn read_file_contents(path: &Path) -> String { fn read_file_contents(path: &Path) -> String {
read_to_string(path) read_to_string(path)
.map_err(|err| format!("// Error reading file: {}", err)) .map_err(|err| format!("// Error reading file: {}", err))
.unwrap_or_else(|err_msg| err_msg) .unwrap_or_else(|err_msg| err_msg)
} }
fn format_file_path(path: &Path, contents: &str) -> PathBuf { fn format_file_path(path: &Path, contents: &str) -> PathBuf {
if contents.contains("Error reading file") { if contents.contains("Error reading file") {
"untitled".into() "untitled".into()
} else { } else {
path.to_path_buf() path.to_path_buf()
} }
} }

View file

@ -4,7 +4,7 @@ use nix::fcntl::FcntlArg;
use nix::fcntl::OFlag; 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::os::fd::AsRawFd;
use std::process::Stdio; use std::process::Stdio;
use std::{env, path::Path, path::PathBuf, process::Command}; use std::{env, path::Path, path::PathBuf, process::Command};
@ -14,44 +14,62 @@ pub struct Buffer {
pub error_buffer: BufReader<std::process::ChildStderr>, pub error_buffer: BufReader<std::process::ChildStderr>,
} }
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 struct CommandEntry {
pub env: String, pub env: String,
pub command: String, pub command: String,
pub output: String, pub result: Vec<Line>,
pub error: String,
pub buffer: Option<Buffer>, pub buffer: Option<Buffer>,
} }
impl CommandEntry { impl CommandEntry {
pub fn new(env: String, command: String) -> Self { pub fn new(env: String, command: String) -> Self {
let (buffer, error) = match execute(command.clone()) { let (buffer, result) = match execute(command.clone()) {
Ok(command_buffer) => (Some(command_buffer), String::new()), Ok(command_buffer) => (Some(command_buffer), vec![]),
Err(err) => (None, format!("failed to get results: {}", err)), Err(err) => (
None,
vec![Line::error(format!("failed to get results: {}", err))],
),
}; };
CommandEntry { CommandEntry {
env, env,
command, command,
output: String::new(), result,
error,
buffer, buffer,
} }
} }
pub fn update(&mut self) { pub fn update(&mut self) {
if let Some(buffer) = &mut self.buffer { if let Some(buffer) = &mut self.buffer {
for line in buffer.output_buffer.by_ref().lines() { let mut output = String::new();
match line { let _ = buffer.output_buffer.read_line(&mut output);
Ok(line) => self.output += &format!("{}\n", line), if !remove_line_break(output.to_string()).is_empty() {
Err(_) => return, self.result.push(Line::output(format!("{}\n", output)));
}
} }
let mut error = String::new();
for line in buffer.error_buffer.by_ref().lines() { let _ = buffer.error_buffer.read_line(&mut error);
match line { if !remove_line_break(error.to_string()).is_empty() {
Ok(line) => self.error += &format!("{}\n", line), self.result.push(Line::error(format!("{}\n", error)));
Err(_) => return,
}
} }
} }
} }
@ -129,3 +147,14 @@ pub fn execute(command: String) -> Result<Buffer, std::io::Error> {
error_buffer, error_buffer,
}) })
} }
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
}