use std::{ cmp::min, fmt::Display, }; use ratatui::{ prelude, style::{Color, Style}, widgets::{Paragraph, Widget} }; use crate::app::{app::App, error_msg::ErrorMessage, logic::calc::{CellType, LEN}}; pub enum Mode { Insert(Chord), Chord(Chord), Normal, Command(Chord), Visual((usize, usize)), } impl Display for Mode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Mode::Normal => write!(f, "NORMAL"), Mode::Insert(_) => write!(f, "INSERT"), Mode::Chord(_) => write!(f, "CHORD"), Mode::Command(_) => write!(f, "COMMAND"), Mode::Visual(_) => write!(f, "VISUAL"), } } } impl Mode { pub fn get_style(&self) -> Style { match self { // Where you are typing Mode::Insert(_) => Style::new().fg(Color::White).bg(Color::Blue), Mode::Command(_) => Style::new().fg(Color::Black).bg(Color::Magenta), Mode::Chord(_) => Style::new().fg(Color::Black).bg(Color::LightBlue), // Movement-based modes Mode::Visual(_) => Style::new().fg(Color::Yellow), Mode::Normal => Style::new().fg(Color::Green), } } pub fn process_cmd(app: &mut App) { if let Mode::Command(editor) = &mut app.mode { // [':', 'q'] let cmd = &editor.as_string()[1..]; let args = cmd.split_ascii_whitespace().collect::>(); // we are guaranteed at least 1 arg if args.is_empty() { return; } match args[0] { "w" => { if let Some(file) = &app.file { unimplemented!("Figure out how we want to save Grid to a csv or something") } else { if let Some(arg) = args.get(1) { unimplemented!("Saving a file") } app.error_msg = ErrorMessage::new("No file selected"); } } "q" => app.exit = true, "set" => { if let Some(arg) = args.get(1) { let parts: Vec<&str> = arg.split('=').collect(); if parts.len() != 2 { app.error_msg = ErrorMessage::new("set ="); return; } let key = parts[0]; let value = parts[1]; app.vars.insert(key.to_owned(), value.to_owned()); } app.error_msg = ErrorMessage::new("set =") } _ => {} } } } pub fn process_key(app: &mut App, key: char) { match &mut app.mode { Mode::Normal | Mode::Visual(_) => { match key { // < 'h' => { app.grid.selected_cell.0 = app.grid.selected_cell.0.saturating_sub(1); return; } // v 'j' => { app.grid.selected_cell.1 = min(app.grid.selected_cell.1.saturating_add(1), LEN - 1); return; } // ^ 'k' => { app.grid.selected_cell.1 = app.grid.selected_cell.1.saturating_sub(1); return; } // > 'l' => { app.grid.selected_cell.0 = min(app.grid.selected_cell.0.saturating_add(1), LEN - 1); return; } '0' => { app.grid.selected_cell.0 = 0; return; } // edit cell 'i' | 'a' => { let (x, y) = app.grid.selected_cell; let val = app.grid.get_cell_raw(x, y).as_ref().map(|f| f.to_string()).unwrap_or(String::new()); app.mode = Mode::Insert(Chord::from(val)); } // replace cell 'r' => { app.mode = Mode::Insert(Chord::from(String::new())); } 'I' => { /* insert col before */ } 'A' => { /* insert col after */ } 'o' => { /* insert row below */ } 'O' => { /* insert row above */ } 'v' => app.mode = Mode::Visual(app.grid.selected_cell), ':' => app.mode = Mode::Command(Chord::new(':')), // loose chars will put you into chord mode c => { if let Mode::Normal = app.mode { app.mode = Mode::Chord(Chord::new(c)) } } } if let Mode::Visual((x1, y1)) = app.mode { // TODO visual copy, paste, etc let (x2, y2) = app.grid.selected_cell; let (low_x, hi_x) = if x1 < x2 { (x1, x2) } else { (x2, x1) }; let (low_y, hi_y) = if y1 < y2 { (y1, y2) } else { (y2, y1) }; if key == 'd' { for x in low_x..=hi_x { for y in low_y..=hi_y { app.grid.set_cell_raw::((x, y), None); } } app.mode = Mode::Normal } } } Mode::Chord(chord) => { chord.add_char(key); // the chord starts with a :, send it over to be a comman if chord.buf[0] == ':' { app.mode = Mode::Command(Chord::new(':')); return; } // Try and parse out a preceding number match chord.as_string()[0..chord.as_string().len() - 1].parse::() { // For chords that can take a numeric input Ok(num) => match key { 'G' => { let sel = app.grid.selected_cell; app.grid.selected_cell = (sel.0, num); app.mode = Mode::Normal; } _ => { if key.is_alphabetic() { app.mode = Mode::Normal; for _ in 0..num { Mode::process_key(app, key); } } } }, Err(_) => match chord.as_string().as_str() { "d " | "dw" => { let loc = app.grid.selected_cell; app.grid.set_cell_raw::(loc, None); app.mode = Mode::Normal; } "gg" => { app.grid.selected_cell.1 = 0; app.mode = Mode::Normal; } "zz" => { app.screen.center_x(app.grid.selected_cell, &app.vars); app.screen.center_y(app.grid.selected_cell, &app.vars); app.mode = Mode::Normal; } _ => {} }, } } _ => todo!(), } } } pub struct Chord { buf: Vec, } impl From for Chord { fn from(value: String) -> Self { let b = value.as_bytes().iter().map(|f| *f as char).collect(); Chord { buf: b, } } } impl Chord { pub fn new(inital: char) -> Self { let mut buf = Vec::new(); buf.push(inital); Self { buf, } } pub fn backspace(&mut self) { self.buf.pop(); } pub fn add_char(&mut self, c: char) { self.buf.push(c) } pub fn as_string(&self) -> String { self.buf.iter().collect() } } impl Widget for &Chord { fn render(self, area: prelude::Rect, buf: &mut prelude::Buffer) { Paragraph::new(self.buf.iter().collect::()).render(area, buf); } }