use std::path::{Path, PathBuf};
use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::app::App;
use crate::storage;
const E32: &str = "E32: No file name";
const E37: &str = "E37: No write since last change (add ! to override)";
const E471: &str = "E471: Argument required";
struct Parsed<'a> {
name: &'a str,
bang: bool,
arg: Option<&'a str>,
}
fn parse(line: &str) -> Parsed<'_> {
let line = line.trim();
let end = line
.char_indices()
.find(|(_, c)| !c.is_ascii_alphabetic())
.map_or(line.len(), |(i, _)| i);
let (name, rest) = line.split_at(end);
let (bang, rest) = match rest.strip_prefix('!') {
Some(r) => (true, r),
None => (false, rest),
};
let rest = rest.trim();
Parsed {
name,
bang,
arg: (!rest.is_empty()).then_some(rest),
}
}
impl App {
pub(super) fn command_key(&mut self, key: KeyEvent) {
let Some(input) = self.command.as_mut() else {
return;
};
let plain = (key.modifiers - KeyModifiers::SHIFT).is_empty();
match key.code {
KeyCode::Esc => self.command = None,
KeyCode::Enter => {
let line = std::mem::take(input);
self.command = None;
self.execute_command(&line);
}
KeyCode::Backspace => {
if input.pop().is_none() {
self.command = None;
}
}
KeyCode::Char(c) if plain => input.push(c),
_ => {}
}
}
pub(super) fn execute_command(&mut self, line: &str) {
let Parsed { name, bang, arg } = parse(line);
match name {
"" if line.trim().is_empty() => {}
"w" | "write" => {
self.write(arg);
}
"e" | "edit" => self.edit(arg, bang),
"q" | "quit" => self.quit(bang),
"wq" => self.write_quit(arg),
"x" => self.exit(arg),
_ => {
let word = line.split_whitespace().next().unwrap_or_default();
self.status_msg = Some(format!("Unknown command: {word}"));
}
}
}
pub(super) fn quit(&mut self, force: bool) {
if !force && self.modified && self.file_path.is_some() {
self.status_msg = Some(E37.into());
} else {
self.should_quit = true;
}
}
pub(super) fn write(&mut self, path: Option<&str>) -> bool {
let Some(path) = path.map(expand).or_else(|| self.file_path.clone()) else {
self.status_msg = Some(E32.into());
return false;
};
if let Err(e) = storage::save(&path, self.lines()) {
self.status_msg = Some(io_error(&path, &e));
return false;
}
self.status_msg = Some(format!("\"{}\" written", path.display()));
self.file_path = Some(path);
self.session_mode = false;
self.modified = false;
true
}
fn write_quit(&mut self, path: Option<&str>) {
if (path.is_none() && self.file_path.is_none()) || self.write(path) {
self.should_quit = true;
}
}
pub(super) fn exit(&mut self, path: Option<&str>) {
if path.is_none() && !(self.modified && self.file_path.is_some()) {
self.should_quit = true;
} else {
self.write_quit(path);
}
}
fn edit(&mut self, path: Option<&str>, force: bool) {
let path = match (path, force) {
(Some(p), _) => expand(p),
(None, false) => {
self.status_msg = Some(E471.into());
return;
}
(None, true) => match self.file_path.clone() {
Some(p) => p,
None => {
self.status_msg = Some(E32.into());
return;
}
},
};
if !force && self.modified && self.file_path.is_some() {
self.status_msg = Some(E37.into());
return;
}
let lines = match storage::load(&path) {
Ok(lines) => lines,
Err(e) => {
self.status_msg = Some(io_error(&path, &e));
return;
}
};
if let Err((session, e)) = self.save_session() {
self.status_msg = Some(io_error(&session, &e));
return;
}
self.status_msg = Some(format!("\"{}\" {} lines", path.display(), lines.len()));
self.replace_lines(&lines);
self.file_path = Some(path);
self.session_mode = false;
}
pub fn save_session(&self) -> Result<bool, (PathBuf, std::io::Error)> {
if self.file_path.is_some() || self.lines().iter().all(|l| l.trim().is_empty()) {
return Ok(false);
}
let Some(path) = &self.session_path else {
return Ok(false);
};
storage::save(path, self.lines()).map_err(|e| (path.clone(), e))?;
Ok(true)
}
}
fn expand(path: &str) -> PathBuf {
storage::expand_tilde(Path::new(path))
}
fn io_error(path: &Path, e: &std::io::Error) -> String {
format!("Error: {}: {e}", path.display())
}