From b75bb9c1510d9b0975821b532a0ef9752ee59de8 Mon Sep 17 00:00:00 2001 From: VagnoDev Date: Sun, 25 Oct 2026 14:20:18 +0100 Subject: [PATCH] Fix terminal cleanup and save-and-quit behavior - Restore terminal state when exiting the editor - Avoid duplicate terminal cleanup in the command executor - Prompt for a filename when using :w or :wq without an associated file path - Exit the editor after successfully saving with :wq - Keep the editor open and report an error if saving fails --- src/app/context.rs | 6 +- src/commands/executor.rs | 43 +++++++-- src/commands/parser.rs | 12 ++- src/editor/buffer.rs | 29 ++++-- src/editor/editing.rs | 10 +- src/gui/display.rs | 28 +++--- src/gui/status_bar.rs | 8 +- src/io/save.rs | 4 +- src/main.rs | 195 +++++++++++++++++++++++++++------------ 9 files changed, 226 insertions(+), 109 deletions(-) diff --git a/src/app/context.rs b/src/app/context.rs index 9d9e570..dcfe124 100644 --- a/src/app/context.rs +++ b/src/app/context.rs @@ -1,9 +1,13 @@ +use std::path::PathBuf; + use crate::app::messages::Message; use crate::app::mode::Mode; pub struct EditorContext { pub mode: Mode, pub command_buffer: String, - pub file_path: String, + pub file_path: Option, pub current_message: Option, + pub savedialog: bool, + pub quit_after_save: bool } diff --git a/src/commands/executor.rs b/src/commands/executor.rs index 15fdfc2..492cbc3 100644 --- a/src/commands/executor.rs +++ b/src/commands/executor.rs @@ -1,36 +1,59 @@ -use std::io::stdout; +use std::{ + io::stdout, + path::{self, Path, PathBuf}, +}; use crossterm::{ execute, terminal::{self, LeaveAlternateScreen}, }; -use crate::{app::{context::{self, EditorContext}, messages::MessageLevel}, gui::status_bar::send_message, io::save::save}; use crate::{Buffer, Status}; +use crate::{ + app::{ + context::{self, EditorContext}, + messages::MessageLevel, + }, + gui::status_bar::send_message, + io::save::save, +}; use super::parser::Command; pub fn execute( command: Command, buffer: &Buffer, - path: &str, + path: &Option, ) -> std::io::Result { match command { Command::Quit => Ok(Status::Quit), - Command::Write => { - save(buffer, &path.to_string())?; + Command::Write(command_path) => { + match command_path { + Some(new_path) => save(buffer, std::path::Path::new(&new_path))?, + None => { + if let Some(path) = path { + save(buffer, path)?; + } + } + } execute!(stdout(), crossterm::cursor::Hide)?; Ok(Status::Continue) } - Command::WriteQuit => { - save(buffer, &path.to_string())?; - terminal::disable_raw_mode()?; - execute!(stdout(), LeaveAlternateScreen)?; + Command::WriteQuit(command_path) => { + match command_path { + Some(new_path) => save(buffer, std::path::Path::new(&new_path))?, + None => { + if let Some(path) = path { + save(buffer, path)?; + } + } + } + Ok(Status::Quit) } Command::Unknown => Ok(Status::Continue), } -} \ No newline at end of file +} diff --git a/src/commands/parser.rs b/src/commands/parser.rs index 7cf817c..df4a571 100644 --- a/src/commands/parser.rs +++ b/src/commands/parser.rs @@ -1,8 +1,8 @@ #[derive(Debug, PartialEq, Eq)] pub enum Command { Quit, - Write, - WriteQuit, + Write(Option), + WriteQuit(Option), Unknown, } @@ -13,10 +13,12 @@ pub fn parse(input: &str) -> Command { return Command::Unknown; }; + let path = parts.next().map(String::from); + match name { - "q" => Command::Quit, - "w" => Command::Write, - "wq" => Command::WriteQuit, + "q" if path.is_none() => Command::Quit, + "w" => Command::Write(path), + "wq" => Command::WriteQuit(path), _ => Command::Unknown, } } \ No newline at end of file diff --git a/src/editor/buffer.rs b/src/editor/buffer.rs index 890b362..1590249 100644 --- a/src/editor/buffer.rs +++ b/src/editor/buffer.rs @@ -1,4 +1,4 @@ -use std::fs::read_to_string; +use std::{fs::read_to_string, io::ErrorKind}; #[derive(Debug)] pub struct Buffer { @@ -7,24 +7,33 @@ pub struct Buffer { pub column: usize, } -pub fn fetch(file: &str) -> Buffer { +pub fn fetch(file: &str) -> Result { let mut result = Vec::new(); - for line in read_to_string(file) - .unwrap() - .lines() - { - result.push(line.to_string()); + let content = read_to_string(file); + + match content { + Ok(content) => { + for line in content.lines() { + result.push(line.to_string()); + } + } + Err(error) => { + if error.kind() == ErrorKind::NotFound { + result.push(String::new()); + } else { + return Err(error); + } + } } if result.is_empty() { result.push(String::new()); } - Buffer { + Ok(Buffer { lines: result, row: 0, column: 0, - } + }) } - diff --git a/src/editor/editing.rs b/src/editor/editing.rs index 737bcca..6ada881 100644 --- a/src/editor/editing.rs +++ b/src/editor/editing.rs @@ -1,6 +1,6 @@ -use crate::editor::buffer::Buffer; use crate::app::context::EditorContext; use crate::app::mode::Mode; +use crate::editor::buffer::Buffer; pub fn insert_char(c: char, buffer: &mut Buffer) { let Some(line) = buffer.lines.get_mut(buffer.column) else { @@ -16,11 +16,14 @@ pub fn insert_char(c: char, buffer: &mut Buffer) { } pub fn insert_forward(buffer: &mut Buffer, context: &mut EditorContext) { - buffer.row += 1; + let line_len = buffer.lines[buffer.column].chars().count(); + + if line_len > 0 && buffer.row < line_len { + buffer.row += 1; + } context.mode = Mode::Insert; } - pub fn insert_backward(buffer: &mut Buffer, context: &mut EditorContext) { if buffer.row == 0 { return; @@ -30,7 +33,6 @@ pub fn insert_backward(buffer: &mut Buffer, context: &mut EditorContext) { context.mode = Mode::Insert; } - pub fn remove_char(buffer: &mut Buffer) { if buffer.row > 0 { let Some(line) = buffer.lines.get_mut(buffer.column) else { diff --git a/src/gui/display.rs b/src/gui/display.rs index 487801f..fa6d300 100644 --- a/src/gui/display.rs +++ b/src/gui/display.rs @@ -1,8 +1,5 @@ use crate::{ - editor::buffer::Buffer, - app::context::EditorContext, - explorer::tree::Explorer, - app::mode::Mode, + app::context::EditorContext, app::mode::Mode, editor::buffer::Buffer, explorer::tree::Explorer, }; use crossterm::{ @@ -12,7 +9,7 @@ use crossterm::{ terminal, }; -use std::io::{stdout, Write}; +use std::io::{Write, stdout}; use super::{editor_view, explorer_view, status_bar}; @@ -84,7 +81,7 @@ impl Display { ) -> std::io::Result<()> { let mut stdout = stdout(); - execute!(stdout, Hide)?; + queue!(stdout, Hide)?; for cell in &mut self.cells { *cell = Cell::default(); @@ -119,16 +116,16 @@ impl Display { )?; } - stdout.flush()?; - match context.mode { Mode::Command => { - execute!( + let x = if context.savedialog { + (context.command_buffer.chars().count() + 9) as u16 + } else { + (context.command_buffer.chars().count() + 1) as u16 + }; + queue!( stdout, - MoveTo( - (context.command_buffer.chars().count() + 1) as u16, - self.height.saturating_sub(1) as u16, - ), + MoveTo(x, self.height.saturating_sub(1) as u16,), Show )?; } @@ -136,13 +133,14 @@ impl Display { execute!(stdout, Hide)?; } _ => { - execute!( + queue!( stdout, MoveTo(buffer.row as u16, buffer.column as u16), Show )?; } } + stdout.flush()?; Ok(()) } @@ -158,4 +156,4 @@ impl Display { self.cells = cells; } -} \ No newline at end of file +} diff --git a/src/gui/status_bar.rs b/src/gui/status_bar.rs index 191a770..aef9983 100644 --- a/src/gui/status_bar.rs +++ b/src/gui/status_bar.rs @@ -24,7 +24,13 @@ pub(super) fn draw_status_bar( Mode::Normal => "-- NORMAL --".to_string(), Mode::Insert => "-- INSERT --".to_string(), Mode::Explorer => "-- EXPLORER --".to_string(), - Mode::Command => format!(":{}", context.command_buffer), + Mode::Command => { + if context.savedialog { + format!("Save as: {}", context.command_buffer) + } else { + format!(":{}", context.command_buffer) + } + }, }; let status = match context.current_message.as_ref() { diff --git a/src/io/save.rs b/src/io/save.rs index 57a6e92..067ac99 100644 --- a/src/io/save.rs +++ b/src/io/save.rs @@ -1,8 +1,8 @@ use crate::editor::buffer::Buffer; -use std::fs::write; +use std::{fs::write, path::Path}; -pub fn save(buffer: &Buffer, path: &String) -> std::io::Result<()> { +pub fn save(buffer: &Buffer, path: &Path) -> std::io::Result<()> { let content = buffer.lines.join("\n"); write(path, content)?; diff --git a/src/main.rs b/src/main.rs index fca5d3f..063e1bb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,25 +2,30 @@ use std::{io::stdout, path::PathBuf, time::Instant}; use crate::{ Status::Continue, - app::mode::{Mode, set_command, set_normal}, + app::{ + messages::MessageLevel, + mode::{Mode, set_command, set_normal}, + }, commands::{executor, parser}, - editor::editing::{insert_newline, remove_char}, - editor::movement::{ - move_down, move_left, move_right, move_toend, move_tonext, move_toprev, move_tostart, - move_up, + editor::{ + editing::{insert_backward, insert_newline, remove_char}, + movement::{ + move_down, move_left, move_right, move_toend, move_tonext, move_toprev, move_tostart, + move_up, + }, }, explorer::tree::{Explorer, read_dir}, - gui::display::Display, + gui::{display::Display, status_bar::send_message}, input::keyboard::{PendingKey, keybinding}, - gui::status_bar::send_message, - app::messages::MessageLevel + io::save::save, }; use crossterm::{ - cursor, event::{ + event::{ self, Event::{self}, KeyEventKind, - }, execute, + }, + execute, }; use std::path::Path; @@ -59,48 +64,62 @@ pub enum Status { } fn main() -> std::io::Result<()> { - terminal::enable_raw_mode()?; - execute!( - stdout(), - terminal::EnterAlternateScreen, - crossterm::terminal::Clear(crossterm::terminal::ClearType::All), - crossterm::cursor::MoveTo(0, 0) - )?; - let path = std::env::args().nth(1).expect("Use: rusk "); + let path = std::env::args().nth(1); + + let mut app = match &path { + Some(path) => match Path::new(&path).is_dir() { + true => App { + editor: Buffer { + lines: vec![String::new()], + row: 0, + column: 0, + }, + explorer: read_dir(&PathBuf::from(&path), 0), + sidebar_visible: false, + }, + false => { + let file_path = Path::new(&path); - let mut app = match Path::new(&path).is_dir() { - true => App { + let parent = file_path + .parent() + .filter(|p| !p.as_os_str().is_empty()) + .unwrap_or(Path::new(".")); + + App { + editor: buffer::fetch(&path)?, + explorer: read_dir(&PathBuf::from(parent), 0), + sidebar_visible: false, + } + } + }, + None => App { editor: Buffer { lines: vec![String::new()], row: 0, column: 0, }, - explorer: read_dir(&PathBuf::from(&path), 0), + explorer: read_dir(&PathBuf::from("."), 0), sidebar_visible: false, }, - false => { - let file_path = Path::new(&path); - - let parent = file_path - .parent() - .filter(|p| !p.as_os_str().is_empty()) - .unwrap_or(Path::new(".")); - - App { - editor: buffer::fetch(&path), - explorer: read_dir(&PathBuf::from(parent), 0), - sidebar_visible: false, - } - } }; let mut context = EditorContext { mode: Mode::Normal, command_buffer: String::new(), - file_path: path.clone(), + file_path: path.map(PathBuf::from), current_message: None, + savedialog: false, + quit_after_save: false, }; + terminal::enable_raw_mode()?; + execute!( + stdout(), + terminal::EnterAlternateScreen, + crossterm::terminal::Clear(crossterm::terminal::ClearType::All), + crossterm::cursor::MoveTo(0, 0) + )?; + let mut display = Display::new()?; let mut queue = VecDeque::new(); @@ -113,6 +132,8 @@ fn main() -> std::io::Result<()> { app.sidebar_visible, )?; + let mut needs_redraw = true; + loop { if event::poll(std::time::Duration::from_millis(100))? { match event::read()? { @@ -120,11 +141,13 @@ fn main() -> std::io::Result<()> { if key.kind == KeyEventKind::Press { if let Some(realbind) = keybinding(key, &context.mode, &mut pending_key) { queue.push_back(realbind); + needs_redraw = true; } } } Event::Resize(width, height) => { display.resize(width as u32, height as u32); + needs_redraw = true; } _ => {} } @@ -134,6 +157,7 @@ fn main() -> std::io::Result<()> { }; if expired { context.current_message = None; + needs_redraw = true; } } let status = manager(&mut queue, &mut app.editor, &mut context, &mut app.explorer)?; @@ -150,13 +174,16 @@ fn main() -> std::io::Result<()> { } Status::Quit => break, } + if needs_redraw { + display.draw( + &mut app.editor, + &context, + &app.explorer, + app.sidebar_visible, + )?; - display.draw( - &mut app.editor, - &context, - &app.explorer, - app.sidebar_visible, - )?; + needs_redraw = false; + } } terminal::disable_raw_mode()?; execute!(stdout(), terminal::LeaveAlternateScreen)?; @@ -179,7 +206,11 @@ fn manager( Action::MoveUp => move_up(lines), Action::MoveDown => move_down(lines), Action::InsertForward => insert_forward(lines, context), - Action::SetCommand => set_command(context), + Action::InserBackward => insert_backward(lines, context), + Action::SetCommand => { + context.current_message = None; + set_command(context); + } Action::StartLine => move_tostart(lines), Action::EndLine => move_toend(lines), Action::MoveNext => move_tonext(lines), @@ -203,25 +234,57 @@ fn manager( context.command_buffer.pop(); } Action::SendCommand => { + if context.savedialog { + let path = std::path::Path::new(&context.command_buffer); + + match save(lines, path) { + Ok(()) => { + context.file_path = Some(path.to_path_buf()); + context.command_buffer.clear(); + context.savedialog = false; + set_normal(context); + + if context.quit_after_save { + context.quit_after_save = false; + return Ok(Status::Quit); + } + + send_message(context, "File Saved.".to_string(), MessageLevel::Success); + return Ok(Status::Continue); + } + Err(error) => { + send_message( + context, + format!("Cannot save file: {error}"), + MessageLevel::Error, + ); + return Ok(Status::Continue); + } + } + } + let command = parser::parse(context.command_buffer.as_str()); - - let is_write = matches!(&command, parser::Command::Write); - - let status = executor::execute( - command, - lines, - &context.file_path, - )?; - + let needs_filename = matches!( + &command, + parser::Command::Write(None) | parser::Command::WriteQuit(None) + ); + + if needs_filename && context.file_path.is_none() { + context.quit_after_save = matches!(&command, parser::Command::WriteQuit(None)); + + context.savedialog = true; + context.command_buffer.clear(); + return Ok(Status::Continue); + } + + let is_write = matches!(&command, parser::Command::Write(_)); + let status = executor::execute(command, lines, &context.file_path)?; + if is_write { set_normal(context); - send_message( - context, - "File Saved.".to_string(), - MessageLevel::Success, - ); + send_message(context, "File Saved.".to_string(), MessageLevel::Success); } - + return Ok(status); } _ => {} @@ -238,9 +301,19 @@ fn manager( explorer.explorer_toggle() } else { let path = explorer.entries[explorer.selected].path.clone(); - if let Some(path) = path.to_str() { - *lines = buffer::fetch(path); - context.file_path = path.to_string(); + + match buffer::fetch(path.to_string_lossy().as_ref()) { + Ok(buffer) => { + *lines = buffer; + context.file_path = Some(path); + } + Err(error) => { + send_message( + context, + format!("Cannot open file: {error}"), + MessageLevel::Error, + ); + } } } }