wip: command mode works now

starts to address #3
This commit is contained in:
Jeremy Wall 2024-11-20 17:33:01 -05:00
parent 54d026773a
commit bfc918e0d2
3 changed files with 270 additions and 61 deletions

130
Cargo.lock generated
View File

@ -625,6 +625,12 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.31"
@ -676,6 +682,12 @@ version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc46dd3ec48fdd8e693a98d2b8bafae273a2d54c1de02a2a7e3d57d501f39677"
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "hashbrown"
version = "0.15.0"
@ -731,6 +743,16 @@ dependencies = [
"cc",
]
[[package]]
name = "indexmap"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "indoc"
version = "2.0.5"
@ -1071,6 +1093,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.89"
@ -1140,6 +1171,15 @@ dependencies = [
"unicode-width 0.2.0",
]
[[package]]
name = "ratatui-macros"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fef540f80dbe8a0773266fa6077788ceb65ef624cdbf36e131aaf90b4a52df4"
dependencies = [
"ratatui",
]
[[package]]
name = "redox_syscall"
version = "0.5.7"
@ -1200,18 +1240,63 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "roxmltree"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
[[package]]
name = "rstest"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035"
dependencies = [
"futures",
"futures-timer",
"rstest_macros",
"rustc_version",
]
[[package]]
name = "rstest_macros"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a"
dependencies = [
"cfg-if",
"glob",
"proc-macro-crate",
"proc-macro2",
"quote",
"regex 1.11.1",
"relative-path",
"rustc_version",
"syn",
"unicode-ident",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.38.37"
@ -1243,6 +1328,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.214"
@ -1309,6 +1400,7 @@ dependencies = [
"ironcalc",
"ratatui",
"thiserror",
"tui-prompts",
"tui-textarea",
]
@ -1468,6 +1560,35 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "toml_datetime"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
[[package]]
name = "toml_edit"
version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "tui-prompts"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb6e0d8a972545cc209b933a1c06dab8932674b54ae19947834ec854fec2364f"
dependencies = [
"itertools 0.13.0",
"ratatui",
"ratatui-macros",
"rstest",
]
[[package]]
name = "tui-textarea"
version = "0.7.0"
@ -1718,6 +1839,15 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [
"memchr",
]
[[package]]
name = "zerocopy"
version = "0.7.35"

View File

@ -15,3 +15,4 @@ futures = "0.3.31"
ratatui = "0.29.0"
thiserror = "1.0.65"
tui-textarea = "0.7.0"
tui-prompts = "0.5.0"

View File

@ -14,6 +14,7 @@ use ratatui::{
widgets::{Block, Cell, Paragraph, Row, Table, TableState, Widget},
Frame,
};
use tui_prompts::{State, Status, TextPrompt, TextState};
use tui_textarea::{CursorMove, TextArea};
#[derive(Default, Debug, PartialEq)]
@ -21,13 +22,15 @@ pub enum Modality {
#[default]
Navigate,
CellEdit,
Command,
// TODO(zaphar): Command Mode?
}
#[derive(Default, Debug)]
pub struct AppState {
pub struct AppState<'ws> {
pub modality: Modality,
pub table_state: TableState,
pub command_state: TextState<'ws>,
}
// TODO(jwall): This should probably move to a different module.
@ -56,7 +59,7 @@ impl Default for Address {
pub struct Workspace<'ws> {
name: PathBuf,
book: Book,
state: AppState,
state: AppState<'ws>,
text_area: TextArea<'ws>,
dirty: bool,
show_help: bool,
@ -128,6 +131,7 @@ impl<'ws> Workspace<'ws> {
let result = match self.state.modality {
Modality::Navigate => self.handle_navigation_input(key)?,
Modality::CellEdit => self.handle_edit_input(key)?,
Modality::Command => self.handle_command_input(key)?,
};
return Ok(result);
}
@ -151,18 +155,34 @@ impl<'ws> Workspace<'ws> {
"* ESC: Exit edit mode".into(),
"Otherwise edit as normal".into(),
]),
Modality::Command => Text::from(vec![
"Command Mode:".into(),
"* ESC: Exit command mode".into(),
]),
})
.block(info_block)
}
fn handle_command_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Esc | KeyCode::Enter => self.exit_command_mode()?,
_ => {
// NOOP
}
}
}
self.state.command_state.handle_key_event(key);
Ok(None)
}
fn handle_edit_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('h') if key.modifiers == KeyModifiers::CONTROL => {
self.show_help = !self.show_help;
}
KeyCode::Esc => self.exit_edit_mode()?,
KeyCode::Enter => self.exit_edit_mode()?,
KeyCode::Esc | KeyCode::Enter => self.exit_edit_mode()?,
_ => {
// NOOP
}
@ -178,29 +198,21 @@ impl<'ws> Workspace<'ws> {
Ok(None)
}
fn exit_edit_mode(&mut self) -> Result<(), anyhow::Error> {
self.state.modality = Modality::Navigate;
self.text_area.set_cursor_line_style(Style::default());
self.text_area.set_cursor_style(Style::default());
let contents = self.text_area.lines().join("\n");
if self.dirty {
self.book.edit_current_cell(contents)?;
self.book.evaluate();
fn handle_command(&mut self, cmd: String) -> Result<bool> {
if cmd.is_empty() {
return Ok(true);
}
Ok(())
Ok(false)
}
fn handle_navigation_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('e') => {
self.state.modality = Modality::CellEdit;
self.text_area
.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
self.text_area
.set_cursor_style(Style::default().add_modifier(Modifier::SLOW_BLINK));
self.text_area.move_cursor(CursorMove::Bottom);
self.text_area.move_cursor(CursorMove::End);
self.enter_edit_mode();
}
KeyCode::Char(':') => {
self.enter_command_mode();
}
KeyCode::Char('h') if key.modifiers == KeyModifiers::CONTROL => {
self.show_help = !self.show_help;
@ -210,7 +222,13 @@ impl<'ws> Workspace<'ws> {
}
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
let (row_count, _) = self.book.get_size()?;
self.book.update_entry(&Address {row: row_count+1, col: 1 }, "")?;
self.book.update_entry(
&Address {
row: row_count + 1,
col: 1,
},
"",
)?;
let (row, _) = self.book.get_size()?;
let mut loc = self.book.location.clone();
if loc.row < row as usize {
@ -221,7 +239,13 @@ impl<'ws> Workspace<'ws> {
}
KeyCode::Char('t') if key.modifiers == KeyModifiers::CONTROL => {
let (_, col_count) = self.book.get_size()?;
self.book.update_entry(&Address {row: 1, col: col_count+1 }, "")?;
self.book.update_entry(
&Address {
row: 1,
col: col_count + 1,
},
"",
)?;
}
KeyCode::Char('q') => {
return Ok(Some(ExitCode::SUCCESS));
@ -255,6 +279,49 @@ impl<'ws> Workspace<'ws> {
return Ok(None);
}
fn enter_navigation_mode(&mut self) {
self.state.modality = Modality::Navigate;
}
fn enter_command_mode(&mut self) {
self.state.modality = Modality::Command;
self.state.command_state.truncate();
*self.state.command_state.status_mut() = Status::Pending;
self.state.command_state.focus();
}
fn enter_edit_mode(&mut self) {
self.state.modality = Modality::CellEdit;
self.text_area
.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
self.text_area
.set_cursor_style(Style::default().add_modifier(Modifier::SLOW_BLINK));
self.text_area.move_cursor(CursorMove::Bottom);
self.text_area.move_cursor(CursorMove::End);
}
fn exit_command_mode(&mut self) -> Result<()> {
let cmd = self.state.command_state.value().to_owned();
self.state.command_state.blur();
*self.state.command_state.status_mut() = Status::Done;
self.handle_command(cmd)?;
self.enter_navigation_mode();
Ok(())
}
fn exit_edit_mode(&mut self) -> Result<()> {
self.text_area.set_cursor_line_style(Style::default());
self.text_area.set_cursor_style(Style::default());
let contents = self.text_area.lines().join("\n");
if self.dirty {
self.book.edit_current_cell(contents)?;
self.book.evaluate();
self.dirty = false;
}
self.enter_navigation_mode();
Ok(())
}
fn handle_movement_change(&mut self) {
let contents = self
.book
@ -264,7 +331,8 @@ impl<'ws> Workspace<'ws> {
}
fn save_file(&self) -> Result<()> {
self.book.save_to_xlsx(&self.name.to_string_lossy().to_string())?;
self.book
.save_to_xlsx(&self.name.to_string_lossy().to_string())?;
Ok(())
}
}
@ -282,6 +350,7 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
where
Self: Sized,
{
use ratatui::widgets::StatefulWidget;
let outer_block = Block::bordered()
.title(Line::from(
self.name
@ -292,6 +361,7 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
.title_bottom(match &self.state.modality {
Modality::Navigate => "navigate",
Modality::CellEdit => "edit",
Modality::Command => "command",
})
.title_bottom(
Line::from(format!(
@ -300,33 +370,45 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
))
.right_aligned(),
);
let [edit_rect, table_rect] = if self.show_help {
let [edit_rect, table_rect] = if self.show_help || self.state.modality == Modality::Command
{
let [edit_rect, table_rect, info_rect] = Layout::vertical(&[
Constraint::Fill(4),
Constraint::Fill(30),
Constraint::Fill(9),
if self.state.modality == Modality::Command {
Constraint::Max(1)
} else {
Constraint::Fill(9)
},
])
.vertical_margin(2)
.horizontal_margin(2)
.flex(Flex::Legacy)
.areas(area.clone());
// Help panel widget display
let info_para = self.render_help_text();
info_para.render(info_rect, buf);
if self.state.modality == Modality::Command {
StatefulWidget::render(
TextPrompt::from("Command"),
info_rect,
buf,
&mut self.state.command_state,
);
} else if self.show_help {
let info_para = self.render_help_text();
info_para.render(info_rect, buf);
}
[edit_rect, table_rect]
} else {
let [edit_rect, table_rect] = Layout::vertical(&[
Constraint::Fill(4),
Constraint::Fill(30),
])
.vertical_margin(2)
.horizontal_margin(2)
.flex(Flex::Legacy)
.areas(area.clone());
let [edit_rect, table_rect] =
Layout::vertical(&[Constraint::Fill(4), Constraint::Fill(30)])
.vertical_margin(2)
.horizontal_margin(2)
.flex(Flex::Legacy)
.areas(area.clone());
[edit_rect, table_rect]
};
outer_block.render(area, buf);
// Input widget display
@ -341,9 +423,7 @@ impl<'widget, 'ws: 'widget> Widget for &'widget mut Workspace<'ws> {
// TODO(zaphar): Apparently scrolling by columns doesn't work?
self.state.table_state.select_cell(Some((row, col)));
self.state.table_state.select_column(Some(col));
use ratatui::widgets::StatefulWidget;
StatefulWidget::render(table, table_rect, buf, &mut self.state.table_state);
}
}
@ -361,28 +441,26 @@ impl<'t, 'book: 't> TryFrom<&'book Book> for Table<'t> {
.into_iter()
.map(|ri| {
let mut cells = vec![Cell::new(Text::from(ri.to_string()))];
cells.extend((1..=col_count)
.into_iter()
.map(|ci| {
// TODO(zaphar): Is this safe?
let content = value.get_cell_addr_rendered(ri, ci).unwrap();
let cell = Cell::new(Text::raw(content));
match (value.location.row == ri, value.location.col == ci) {
(true, true) => cell.fg(Color::White).underlined(),
_ => cell
.bg(if ri % 2 == 0 {
Color::Rgb(57, 61, 71)
} else {
Color::Rgb(165, 169, 160)
})
.fg(if ri % 2 == 0 {
Color::White
} else {
Color::Rgb(31, 32, 34)
}),
}
.bold()
}));
cells.extend((1..=col_count).into_iter().map(|ci| {
// TODO(zaphar): Is this safe?
let content = value.get_cell_addr_rendered(ri, ci).unwrap();
let cell = Cell::new(Text::raw(content));
match (value.location.row == ri, value.location.col == ci) {
(true, true) => cell.fg(Color::White).underlined(),
_ => cell
.bg(if ri % 2 == 0 {
Color::Rgb(57, 61, 71)
} else {
Color::Rgb(165, 169, 160)
})
.fg(if ri % 2 == 0 {
Color::White
} else {
Color::Rgb(31, 32, 34)
}),
}
.bold()
}));
Row::new(cells)
})
.collect();