mirror of
https://github.com/zaphar/sheetsui.git
synced 2025-07-23 05:19:48 -04:00
feat: ui: numeric prefixes for navigation commands
This commit is contained in:
parent
20faa0c0f3
commit
444bbf3c6d
193
src/ui/mod.rs
193
src/ui/mod.rs
@ -37,6 +37,7 @@ pub struct AppState<'ws> {
|
|||||||
pub modality_stack: Vec<Modality>,
|
pub modality_stack: Vec<Modality>,
|
||||||
pub viewport_state: ViewportState,
|
pub viewport_state: ViewportState,
|
||||||
pub command_state: TextState<'ws>,
|
pub command_state: TextState<'ws>,
|
||||||
|
pub numeric_prefix: Vec<char>,
|
||||||
dirty: bool,
|
dirty: bool,
|
||||||
popup: Vec<String>,
|
popup: Vec<String>,
|
||||||
}
|
}
|
||||||
@ -47,6 +48,7 @@ impl<'ws> Default for AppState<'ws> {
|
|||||||
modality_stack: vec![Modality::default()],
|
modality_stack: vec![Modality::default()],
|
||||||
viewport_state: Default::default(),
|
viewport_state: Default::default(),
|
||||||
command_state: Default::default(),
|
command_state: Default::default(),
|
||||||
|
numeric_prefix: Default::default(),
|
||||||
dirty: Default::default(),
|
dirty: Default::default(),
|
||||||
popup: Default::default(),
|
popup: Default::default(),
|
||||||
}
|
}
|
||||||
@ -62,6 +64,25 @@ impl<'ws> AppState<'ws> {
|
|||||||
self.modality_stack.pop();
|
self.modality_stack.pop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_n_prefix(&self) -> usize {
|
||||||
|
let prefix = self
|
||||||
|
.numeric_prefix
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.to_digit(10).unwrap())
|
||||||
|
.fold(Some(0 as usize), |acc, n| {
|
||||||
|
acc?.checked_mul(10)?.checked_add(n as usize)
|
||||||
|
})
|
||||||
|
.unwrap_or(1);
|
||||||
|
if prefix == 0 {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_n_prefix(&mut self) {
|
||||||
|
self.numeric_prefix.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(jwall): This should probably move to a different module.
|
// TODO(jwall): This should probably move to a different module.
|
||||||
@ -237,7 +258,9 @@ impl<'ws> Workspace<'ws> {
|
|||||||
if key.kind == KeyEventKind::Press {
|
if key.kind == KeyEventKind::Press {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => self.exit_dialog_mode()?,
|
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => self.exit_dialog_mode()?,
|
||||||
KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => self.exit_dialog_mode()?,
|
KeyCode::Char('h') if key.modifiers == KeyModifiers::ALT => {
|
||||||
|
self.exit_dialog_mode()?
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// NOOP
|
// NOOP
|
||||||
}
|
}
|
||||||
@ -336,9 +359,16 @@ impl<'ws> Workspace<'ws> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_numeric_prefix(&mut self, digit: char) {
|
||||||
|
self.state.numeric_prefix.push(digit);
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_navigation_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
|
fn handle_navigation_input(&mut self, key: event::KeyEvent) -> Result<Option<ExitCode>> {
|
||||||
if key.kind == KeyEventKind::Press {
|
if key.kind == KeyEventKind::Press {
|
||||||
match key.code {
|
match key.code {
|
||||||
|
KeyCode::Char(d) if d.is_ascii_digit() => {
|
||||||
|
self.handle_numeric_prefix(d);
|
||||||
|
}
|
||||||
KeyCode::Char('e') | KeyCode::Char('i') => {
|
KeyCode::Char('e') | KeyCode::Char('i') => {
|
||||||
self.enter_edit_mode();
|
self.enter_edit_mode();
|
||||||
}
|
}
|
||||||
@ -352,10 +382,16 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.enter_dialog_mode(self.render_help_text());
|
self.enter_dialog_mode(self.render_help_text());
|
||||||
}
|
}
|
||||||
KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => {
|
KeyCode::Char('n') if key.modifiers == KeyModifiers::CONTROL => {
|
||||||
self.book.select_next_sheet();
|
for _ in 1..=self.state.get_n_prefix() {
|
||||||
|
self.book.select_next_sheet();
|
||||||
|
}
|
||||||
|
self.state.reset_n_prefix();
|
||||||
}
|
}
|
||||||
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
|
KeyCode::Char('p') if key.modifiers == KeyModifiers::CONTROL => {
|
||||||
self.book.select_prev_sheet();
|
for _ in 1..=self.state.get_n_prefix() {
|
||||||
|
self.book.select_prev_sheet();
|
||||||
|
}
|
||||||
|
self.state.reset_n_prefix();
|
||||||
}
|
}
|
||||||
KeyCode::Char('s')
|
KeyCode::Char('s')
|
||||||
if key.modifiers == KeyModifiers::HYPER
|
if key.modifiers == KeyModifiers::HYPER
|
||||||
@ -364,90 +400,101 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.save_file()?;
|
self.save_file()?;
|
||||||
}
|
}
|
||||||
KeyCode::Char('l') if key.modifiers == KeyModifiers::CONTROL => {
|
KeyCode::Char('l') if key.modifiers == KeyModifiers::CONTROL => {
|
||||||
let Address { row: _, col } = &self.book.location;
|
for _ in 1..=self.state.get_n_prefix() {
|
||||||
self.book
|
let Address { row: _, col } = &self.book.location;
|
||||||
.set_col_size(*col, self.book.get_col_size(*col)? + 1)?;
|
self.book
|
||||||
|
.set_col_size(*col, self.book.get_col_size(*col)? + 1)?;
|
||||||
|
}
|
||||||
|
self.state.reset_n_prefix();
|
||||||
}
|
}
|
||||||
KeyCode::Char('h') if key.modifiers == KeyModifiers::CONTROL => {
|
KeyCode::Char('h') if key.modifiers == KeyModifiers::CONTROL => {
|
||||||
let Address { row: _, col } = &self.book.location;
|
for _ in 1..=self.state.get_n_prefix() {
|
||||||
let curr_size = self.book.get_col_size(*col)?;
|
let Address { row: _, col } = &self.book.location;
|
||||||
if curr_size > 1 {
|
let curr_size = self.book.get_col_size(*col)?;
|
||||||
self.book.set_col_size(*col, curr_size - 1)?;
|
if curr_size > 1 {
|
||||||
|
self.book.set_col_size(*col, curr_size - 1)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
self.state.reset_n_prefix();
|
||||||
}
|
}
|
||||||
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
|
KeyCode::Char('r') if key.modifiers == KeyModifiers::CONTROL => {
|
||||||
let (row_count, _) = self.book.get_size()?;
|
for _ in 1..=self.state.get_n_prefix() {
|
||||||
self.book.update_entry(
|
let (row_count, _) = self.book.get_size()?;
|
||||||
&Address {
|
self.book.update_entry(
|
||||||
row: row_count + 1,
|
&Address {
|
||||||
col: 1,
|
row: row_count + 1,
|
||||||
},
|
col: 1,
|
||||||
"",
|
},
|
||||||
)?;
|
"",
|
||||||
let (row, _) = self.book.get_size()?;
|
)?;
|
||||||
let mut loc = self.book.location.clone();
|
let (row, _) = self.book.get_size()?;
|
||||||
if loc.row < row as usize {
|
let mut loc = self.book.location.clone();
|
||||||
loc.row = row as usize;
|
if loc.row < row as usize {
|
||||||
self.book.move_to(&loc)?;
|
loc.row = row as usize;
|
||||||
|
self.book.move_to(&loc)?;
|
||||||
|
}
|
||||||
|
self.handle_movement_change();
|
||||||
}
|
}
|
||||||
self.handle_movement_change();
|
self.state.reset_n_prefix();
|
||||||
}
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
"",
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
return Ok(Some(ExitCode::SUCCESS));
|
return Ok(Some(ExitCode::SUCCESS));
|
||||||
}
|
}
|
||||||
KeyCode::Char('j') | KeyCode::Down
|
KeyCode::Char('j') | KeyCode::Down if key.modifiers != KeyModifiers::CONTROL => {
|
||||||
if key.modifiers != KeyModifiers::CONTROL =>
|
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||||
{
|
ws.move_down()?;
|
||||||
self.move_down()?;
|
ws.handle_movement_change();
|
||||||
self.handle_movement_change();
|
Ok(())
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
KeyCode::Enter
|
KeyCode::Enter if key.modifiers != KeyModifiers::SHIFT => {
|
||||||
if key.modifiers != KeyModifiers::SHIFT =>
|
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||||
{
|
ws.move_down()?;
|
||||||
self.move_down()?;
|
ws.handle_movement_change();
|
||||||
self.handle_movement_change();
|
Ok(())
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
KeyCode::Enter
|
KeyCode::Enter if key.modifiers == KeyModifiers::SHIFT => {
|
||||||
if key.modifiers == KeyModifiers::SHIFT =>
|
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||||
{
|
ws.move_up()?;
|
||||||
self.move_up()?;
|
ws.handle_movement_change();
|
||||||
self.handle_movement_change();
|
Ok(())
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
KeyCode::Char('k') | KeyCode::Up if key.modifiers != KeyModifiers::CONTROL => {
|
KeyCode::Char('k') | KeyCode::Up if key.modifiers != KeyModifiers::CONTROL => {
|
||||||
self.move_up()?;
|
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||||
self.handle_movement_change();
|
ws.move_up()?;
|
||||||
|
ws.handle_movement_change();
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
KeyCode::Char('h') | KeyCode::Left if key.modifiers != KeyModifiers::CONTROL => {
|
KeyCode::Char('h') | KeyCode::Left if key.modifiers != KeyModifiers::CONTROL => {
|
||||||
self.move_left()?;
|
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||||
self.handle_movement_change();
|
ws.move_left()?;
|
||||||
|
ws.handle_movement_change();
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
KeyCode::Char('l') | KeyCode::Right
|
KeyCode::Char('l') | KeyCode::Right if key.modifiers != KeyModifiers::CONTROL => {
|
||||||
if key.modifiers != KeyModifiers::CONTROL =>
|
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||||
{
|
ws.move_right()?;
|
||||||
self.move_right()?;
|
ws.handle_movement_change();
|
||||||
self.handle_movement_change();
|
Ok(())
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
KeyCode::Tab
|
KeyCode::Tab if key.modifiers != KeyModifiers::SHIFT => {
|
||||||
if key.modifiers != KeyModifiers::SHIFT =>
|
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||||
{
|
ws.move_right()?;
|
||||||
self.move_right()?;
|
ws.handle_movement_change();
|
||||||
self.handle_movement_change();
|
Ok(())
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
KeyCode::Tab
|
KeyCode::Tab if key.modifiers == KeyModifiers::SHIFT => {
|
||||||
if key.modifiers == KeyModifiers::SHIFT =>
|
self.run_with_prefix(|ws: &mut Workspace<'_>| -> Result<()> {
|
||||||
{
|
ws.move_left()?;
|
||||||
self.move_left()?;
|
ws.handle_movement_change();
|
||||||
self.handle_movement_change();
|
Ok(())
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// noop
|
// noop
|
||||||
@ -457,6 +504,14 @@ impl<'ws> Workspace<'ws> {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_with_prefix(&mut self, action: impl Fn(&mut Workspace<'_>) -> std::result::Result<(), anyhow::Error>) -> Result<(), anyhow::Error> {
|
||||||
|
for _ in 1..=self.state.get_n_prefix() {
|
||||||
|
action(self)?;
|
||||||
|
}
|
||||||
|
self.state.reset_n_prefix();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn enter_navigation_mode(&mut self) {
|
fn enter_navigation_mode(&mut self) {
|
||||||
self.state.modality_stack.push(Modality::Navigate);
|
self.state.modality_stack.push(Modality::Navigate);
|
||||||
}
|
}
|
||||||
@ -501,7 +556,7 @@ impl<'ws> Workspace<'ws> {
|
|||||||
self.text_area.set_cursor_line_style(Style::default());
|
self.text_area.set_cursor_line_style(Style::default());
|
||||||
self.text_area.set_cursor_style(Style::default());
|
self.text_area.set_cursor_style(Style::default());
|
||||||
let contents = self.text_area.lines().join("\n");
|
let contents = self.text_area.lines().join("\n");
|
||||||
if self.state.dirty && keep{
|
if self.state.dirty && keep {
|
||||||
self.book.edit_current_cell(contents)?;
|
self.book.edit_current_cell(contents)?;
|
||||||
self.book.evaluate();
|
self.book.evaluate();
|
||||||
} else {
|
} else {
|
||||||
|
@ -315,3 +315,39 @@ fn test_edit_mode_esc_keycode() {
|
|||||||
assert_eq!("", ws.text_area.lines().join("\n"));
|
assert_eq!("", ws.text_area.lines().join("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_navigation_numeric_prefix()
|
||||||
|
{
|
||||||
|
let mut ws =
|
||||||
|
Workspace::new_empty("en", "America/New_York").expect("Failed to get empty workbook");
|
||||||
|
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
||||||
|
ws.book.new_sheet(Some("Sheet2")).expect("failed to create sheet2");
|
||||||
|
ws.book.new_sheet(Some("Sheet3")).expect("failed to create sheet3");
|
||||||
|
ws.handle_input(construct_key_event(KeyCode::Char('2')))
|
||||||
|
.expect("Failed to handle '3' key event");
|
||||||
|
ws.handle_input(construct_key_event(KeyCode::Char('3')))
|
||||||
|
.expect("Failed to handle '3' key event");
|
||||||
|
ws.handle_input(construct_key_event(KeyCode::Char('9')))
|
||||||
|
.expect("Failed to handle '3' key event");
|
||||||
|
assert_eq!(239, ws.state.get_n_prefix());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_navigation_tab_next_numeric_prefix()
|
||||||
|
{
|
||||||
|
let mut ws =
|
||||||
|
Workspace::new_empty("en", "America/New_York").expect("Failed to get empty workbook");
|
||||||
|
assert_eq!(Some(&Modality::Navigate), ws.state.modality_stack.last());
|
||||||
|
ws.book.new_sheet(Some("Sheet2")).expect("failed to create sheet2");
|
||||||
|
ws.book.new_sheet(Some("Sheet3")).expect("failed to create sheet3");
|
||||||
|
ws.handle_input(construct_key_event(KeyCode::Char('2')))
|
||||||
|
.expect("Failed to handle '3' key event");
|
||||||
|
assert_eq!(2, ws.state.get_n_prefix());
|
||||||
|
ws.handle_input(construct_modified_key_event(KeyCode::Char('n'), KeyModifiers::CONTROL))
|
||||||
|
.expect("Failed to handle 'Ctrl-n' key event");
|
||||||
|
assert_eq!("Sheet3", ws.book.get_sheet_name().expect("Failed to get sheet name"));
|
||||||
|
assert_eq!(1, ws.state.get_n_prefix());
|
||||||
|
ws.handle_input(construct_modified_key_event(KeyCode::Char('n'), KeyModifiers::CONTROL))
|
||||||
|
.expect("Failed to handle 'Ctrl-n' key event");
|
||||||
|
assert_eq!("Sheet1", ws.book.get_sheet_name().expect("Failed to get sheet name"));
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user