Files
neoscim/src/app/mode.rs
2025-11-12 08:40:56 -07:00

248 lines
8.5 KiB
Rust

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::<Vec<&str>>();
// 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 <key>=<value>");
return;
}
let key = parts[0];
let value = parts[1];
app.vars.insert(key.to_owned(), value.to_owned());
}
app.error_msg = ErrorMessage::new("set <key>=<value>")
}
_ => {}
}
}
}
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::<CellType>((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::<usize>() {
// 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::<CellType>(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<char>,
}
impl From<String> 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::<String>()).render(area, buf);
}
}