explore: add more less key bindings and add Transition::None (#14468)

# Description
The `explore` command is `less`-like, but it's missing the `Emacs`
keybindings for up/down and PageUp/PageDown as well as the "q" to quit
out. When I looked into adding those additional keybindings, I noticed
there was a lot of duplicated code in the various views, so I refactored
the code into a new `trait CursorMoveHandler`. I also noticed that there
was an existing `TODO: should we add a noop transition instead of doing
Option<Transition> everywhere?` comment in the code. I went ahead and
implemented a new `Transition::None`, and that made the new `trait
CursorMoveHandler` code MUCH cleaner, in addition to making some of the
old code a little cleaner as well.

# User-Facing Changes
Users that are used to the keybindings for `less` should feel much more
comfortable using `explore`.

# Tests + Formatting
Unfortunately, there aren't any existing tests for the `explore`
command, so I didn't know where I should add new tests to cover my code
changes.

---------

Co-authored-by: paulie4 <203125+paulie4@users.noreply.github.com>
Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
This commit is contained in:
paulie4 2024-11-30 09:22:52 -05:00 committed by GitHub
parent 3d5f853b03
commit 88d27fd607
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 324 additions and 370 deletions

View File

@ -33,13 +33,14 @@ Launch Explore by piping data into it: {}
Move around: Use the cursor keys Move around: Use the cursor keys
Drill down into records+tables: Press <Enter> to select a cell, move around with cursor keys, press <Enter> again Drill down into records+tables: Press <Enter> to select a cell, move around with cursor keys, press <Enter> again
Go back/up a level: Press <Esc> Go back/up a level: Press <Esc> or "q"
Transpose (flip rows+columns): Press "t" Transpose (flip rows+columns): Press "t"
Expand (show all nested data): Press "e" Expand (show all nested data): Press "e"
Open this help page : Type ":help" then <Enter> Open this help page : Type ":help" then <Enter>
Open an interactive REPL: Type ":try" then <Enter> Open an interactive REPL: Type ":try" then <Enter>
Scroll up/down: Use the "Page Up" and "Page Down" keys Scroll up: Press "Page Up", Ctrl+B, or Alt+V
Exit Explore: Type ":q" then <Enter>, or Ctrl+D. Alternately, press <Esc> until Explore exits Scroll down: Press "Page Down", Ctrl+F, or Ctrl+V
Exit Explore: Type ":q" then <Enter>, or Ctrl+D. Alternately, press <Esc> or "q" until Explore exits
{} {}
Most commands support search via regular expressions. Most commands support search via regular expressions.

View File

@ -93,7 +93,7 @@ impl View for NuView {
layout: &Layout, layout: &Layout,
info: &mut crate::pager::ViewInfo, info: &mut crate::pager::ViewInfo,
key: crossterm::event::KeyEvent, key: crossterm::event::KeyEvent,
) -> Option<crate::pager::Transition> { ) -> crate::pager::Transition {
match self { match self {
NuView::Records(v) => v.handle_input(engine_state, stack, layout, info, key), NuView::Records(v) => v.handle_input(engine_state, stack, layout, info, key),
NuView::Preview(v) => v.handle_input(engine_state, stack, layout, info, key), NuView::Preview(v) => v.handle_input(engine_state, stack, layout, info, key),

View File

@ -128,10 +128,17 @@ impl<'a> Pager<'a> {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Transition { pub enum Transition {
// TODO: should we add a noop transition instead of doing Option<Transition> everywhere?
Ok, Ok,
Exit, Exit,
Cmd(String), Cmd(String),
None,
}
#[derive(Debug, Clone)]
pub enum StatusTopOrEnd {
Top,
End,
None,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -212,34 +219,32 @@ fn render_ui(
view_stack.curr_view.as_mut().map(|p| &mut p.view), view_stack.curr_view.as_mut().map(|p| &mut p.view),
); );
if let Some(transition) = transition { let (exit, cmd_name) = react_to_event_result(
let (exit, cmd_name) = react_to_event_result( transition,
transition, engine_state,
engine_state, &commands,
&commands, pager,
pager, &mut view_stack,
&mut view_stack, stack,
stack, info,
info, );
);
if let Some(value) = exit { if let Some(value) = exit {
break Ok(value); break Ok(value);
}
if !cmd_name.is_empty() {
if let Some(r) = info.report.as_mut() {
r.message = cmd_name;
r.level = Severity::Success;
} else {
info.report = Some(Report::success(cmd_name));
} }
if !cmd_name.is_empty() { let info = info.clone();
if let Some(r) = info.report.as_mut() { term.draw(|f| {
r.message = cmd_name; draw_info(f, pager, info);
r.level = Severity::Success; })?;
} else {
info.report = Some(Report::success(cmd_name));
}
let info = info.clone();
term.draw(|f| {
draw_info(f, pager, info);
})?;
}
} }
if pager.cmd_buf.run_cmd { if pager.cmd_buf.run_cmd {
@ -319,6 +324,7 @@ fn react_to_event_result(
} }
} }
} }
Transition::None => (None, String::default()),
} }
} }
@ -419,6 +425,7 @@ fn run_command(
Transition::Ok => Ok(CmdResult::new(false, false, String::new())), Transition::Ok => Ok(CmdResult::new(false, false, String::new())),
Transition::Exit => Ok(CmdResult::new(true, false, String::new())), Transition::Exit => Ok(CmdResult::new(true, false, String::new())),
Transition::Cmd { .. } => todo!("not used so far"), Transition::Cmd { .. } => todo!("not used so far"),
Transition::None => panic!("Transition::None not expected from command.react()"),
} }
} }
Command::View { mut cmd, stackable } => { Command::View { mut cmd, stackable } => {
@ -617,17 +624,17 @@ fn handle_events<V: View>(
search: &mut SearchBuf, search: &mut SearchBuf,
command: &mut CommandBuf, command: &mut CommandBuf,
mut view: Option<&mut V>, mut view: Option<&mut V>,
) -> Option<Transition> { ) -> Transition {
// We are only interested in Pressed events; // We are only interested in Pressed events;
// It's crucial because there are cases where terminal MIGHT produce false events; // It's crucial because there are cases where terminal MIGHT produce false events;
// 2 events 1 for release 1 for press. // 2 events 1 for release 1 for press.
// Want to react only on 1 of them so we do. // Want to react only on 1 of them so we do.
let mut key = match events.next_key_press() { let mut key = match events.next_key_press() {
Ok(Some(key)) => key, Ok(Some(key)) => key,
Ok(None) => return None, Ok(None) => return Transition::None,
Err(e) => { Err(e) => {
log::error!("Failed to read key event: {e}"); log::error!("Failed to read key event: {e}");
return None; return Transition::None;
} }
}; };
@ -647,15 +654,15 @@ fn handle_events<V: View>(
view.as_deref_mut(), view.as_deref_mut(),
key, key,
); );
if result.is_some() { if !matches!(result, Transition::None) {
return result; return result;
} }
match events.try_next_key_press() { match events.try_next_key_press() {
Ok(Some(next_key)) => key = next_key, Ok(Some(next_key)) => key = next_key,
Ok(None) => return None, Ok(None) => return Transition::None,
Err(e) => { Err(e) => {
log::error!("Failed to peek key event: {e}"); log::error!("Failed to peek key event: {e}");
return None; return Transition::None;
} }
} }
} }
@ -671,29 +678,29 @@ fn handle_event<V: View>(
command: &mut CommandBuf, command: &mut CommandBuf,
mut view: Option<&mut V>, mut view: Option<&mut V>,
key: KeyEvent, key: KeyEvent,
) -> Option<Transition> { ) -> Transition {
if handle_exit_key_event(&key) { if handle_exit_key_event(&key) {
return Some(Transition::Exit); return Transition::Exit;
} }
if handle_general_key_events1(&key, search, command, view.as_deref_mut()) { if handle_general_key_events1(&key, search, command, view.as_deref_mut()) {
return None; return Transition::None;
} }
if let Some(view) = &mut view { if let Some(view) = &mut view {
let t = view.handle_input(engine_state, stack, layout, info, key); let t = view.handle_input(engine_state, stack, layout, info, key);
match t { match t {
Some(Transition::Exit) => return Some(Transition::Ok), Transition::Exit => return Transition::Ok,
Some(Transition::Cmd(cmd)) => return Some(Transition::Cmd(cmd)), Transition::Cmd(cmd) => return Transition::Cmd(cmd),
Some(Transition::Ok) => return None, Transition::Ok => return Transition::None,
None => {} Transition::None => {}
} }
} }
// was not handled so we must check our default controls // was not handled so we must check our default controls
handle_general_key_events2(&key, search, command, view, info); handle_general_key_events2(&key, search, command, view, info);
None Transition::None
} }
fn handle_exit_key_event(key: &KeyEvent) -> bool { fn handle_exit_key_event(key: &KeyEvent) -> bool {

View File

@ -2,7 +2,7 @@
mod binary_widget; mod binary_widget;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::KeyEvent;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
Value, Value,
@ -21,7 +21,7 @@ use crate::{
use self::binary_widget::{BinarySettings, BinaryStyle, BinaryWidget}; use self::binary_widget::{BinarySettings, BinaryStyle, BinaryWidget};
use super::{cursor::WindowCursor2D, Layout, View, ViewConfig}; use super::{cursor::CursorMoveHandler, cursor::WindowCursor2D, Layout, View, ViewConfig};
/// An interactive view that displays binary data in a hex dump format. /// An interactive view that displays binary data in a hex dump format.
/// Not finished; many aspects are still WIP. /// Not finished; many aspects are still WIP.
@ -66,15 +66,14 @@ impl View for BinaryView {
_: &Layout, _: &Layout,
info: &mut ViewInfo, info: &mut ViewInfo,
key: KeyEvent, key: KeyEvent,
) -> Option<Transition> { ) -> Transition {
let result = handle_event_view_mode(self, &key); // currently only handle_enter() in crates/nu-explore/src/views/record/mod.rs raises an Err()
if let Ok((Transition::Ok, ..)) = self.handle_input_key(&key) {
if matches!(&result, Some(Transition::Ok)) {
let report = create_report(self.cursor); let report = create_report(self.cursor);
info.status = Some(report); info.status = Some(report);
} }
None Transition::None
} }
fn collect_data(&self) -> Vec<NuText> { fn collect_data(&self) -> Vec<NuText> {
@ -93,6 +92,12 @@ impl View for BinaryView {
} }
} }
impl CursorMoveHandler for BinaryView {
fn get_cursor(&mut self) -> &mut WindowCursor2D {
&mut self.cursor
}
}
fn create_binary_widget(v: &BinaryView) -> BinaryWidget<'_> { fn create_binary_widget(v: &BinaryView) -> BinaryWidget<'_> {
let start_line = v.cursor.window_origin().row; let start_line = v.cursor.window_origin().row;
let count_elements = let count_elements =
@ -106,73 +111,6 @@ fn create_binary_widget(v: &BinaryView) -> BinaryWidget<'_> {
w w
} }
fn handle_event_view_mode(view: &mut BinaryView, key: &KeyEvent) -> Option<Transition> {
match key {
KeyEvent {
code: KeyCode::Char('u'),
modifiers: KeyModifiers::CONTROL,
..
}
| KeyEvent {
code: KeyCode::PageUp,
..
} => {
view.cursor.prev_row_page();
return Some(Transition::Ok);
}
KeyEvent {
code: KeyCode::Char('d'),
modifiers: KeyModifiers::CONTROL,
..
}
| KeyEvent {
code: KeyCode::PageDown,
..
} => {
view.cursor.next_row_page();
return Some(Transition::Ok);
}
_ => {}
}
match key.code {
KeyCode::Esc => Some(Transition::Exit),
KeyCode::Up | KeyCode::Char('k') => {
view.cursor.prev_row_i();
Some(Transition::Ok)
}
KeyCode::Down | KeyCode::Char('j') => {
view.cursor.next_row_i();
Some(Transition::Ok)
}
KeyCode::Left | KeyCode::Char('h') => {
view.cursor.prev_column_i();
Some(Transition::Ok)
}
KeyCode::Right | KeyCode::Char('l') => {
view.cursor.next_column_i();
Some(Transition::Ok)
}
KeyCode::Home | KeyCode::Char('g') => {
view.cursor.row_move_to_start();
Some(Transition::Ok)
}
KeyCode::End | KeyCode::Char('G') => {
view.cursor.row_move_to_end();
Some(Transition::Ok)
}
_ => None,
}
}
fn settings_from_config(config: &ExploreConfig) -> Settings { fn settings_from_config(config: &ExploreConfig) -> Settings {
// Most of this is hardcoded for now, add it to the config later if needed // Most of this is hardcoded for now, add it to the config later if needed
Settings { Settings {

View File

@ -3,7 +3,7 @@ mod window_cursor_2d;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
pub use window_cursor::WindowCursor; pub use window_cursor::WindowCursor;
pub use window_cursor_2d::{Position, WindowCursor2D}; pub use window_cursor_2d::{CursorMoveHandler, Position, WindowCursor2D};
/// A 1-dimensional cursor to track a position from 0 to N /// A 1-dimensional cursor to track a position from 0 to N
/// ///

View File

@ -1,3 +1,6 @@
use crate::pager::{StatusTopOrEnd, Transition};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use super::WindowCursor; use super::WindowCursor;
use anyhow::Result; use anyhow::Result;
@ -170,3 +173,133 @@ impl WindowCursor2D {
.set_window_start_position(self.y.window_starts_at() - 1) .set_window_start_position(self.y.window_starts_at() - 1)
} }
} }
pub trait CursorMoveHandler {
fn get_cursor(&mut self) -> &mut WindowCursor2D;
// standard handle_EVENT handlers that can be overwritten
fn handle_enter(&mut self) -> Result<Transition> {
Ok(Transition::None)
}
fn handle_esc(&mut self) -> Transition {
Transition::Exit
}
fn handle_expand(&mut self) -> Transition {
Transition::None
}
fn handle_left(&mut self) {
self.get_cursor().prev_column_i()
}
fn handle_right(&mut self) {
self.get_cursor().next_column_i()
}
fn handle_up(&mut self) {
self.get_cursor().prev_row_i()
}
fn handle_down(&mut self) {
self.get_cursor().next_row_i()
}
fn handle_transpose(&mut self) -> Transition {
Transition::None
}
// top-level event handler should not be overwritten
fn handle_input_key(&mut self, key: &KeyEvent) -> Result<(Transition, StatusTopOrEnd)> {
let key_combo_status = match key {
// PageUp supports Vi (Ctrl+b) and Emacs (Alt+v) keybindings
KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: KeyCode::Char('b'),
..
}
| KeyEvent {
modifiers: KeyModifiers::ALT,
code: KeyCode::Char('v'),
..
}
| KeyEvent {
code: KeyCode::PageUp,
..
} => {
self.get_cursor().prev_row_page();
StatusTopOrEnd::Top
}
// PageDown supports Vi (Ctrl+f) and Emacs (Ctrl+v) keybindings
KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: KeyCode::Char('f'),
..
}
| KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: KeyCode::Char('v'),
..
}
| KeyEvent {
code: KeyCode::PageDown,
..
} => {
self.get_cursor().next_row_page();
self.get_cursor().prev_row();
StatusTopOrEnd::End
}
// Up support Emacs (Ctrl+p) keybinding
KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: KeyCode::Char('p'),
..
} => {
self.handle_up();
StatusTopOrEnd::Top
}
// Down support Emacs (Ctrl+n) keybinding
KeyEvent {
modifiers: KeyModifiers::CONTROL,
code: KeyCode::Char('n'),
..
} => {
self.handle_down();
StatusTopOrEnd::End
}
_ => StatusTopOrEnd::None,
};
match key_combo_status {
StatusTopOrEnd::Top | StatusTopOrEnd::End => {
return Ok((Transition::Ok, key_combo_status));
}
_ => {} // not page up or page down, so don't return; continue to next match block
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => Ok((self.handle_esc(), StatusTopOrEnd::None)),
KeyCode::Char('i') | KeyCode::Enter => Ok((self.handle_enter()?, StatusTopOrEnd::None)),
KeyCode::Char('t') => Ok((self.handle_transpose(), StatusTopOrEnd::None)),
KeyCode::Char('e') => Ok((self.handle_expand(), StatusTopOrEnd::None)),
KeyCode::Up | KeyCode::Char('k') => {
self.handle_up();
Ok((Transition::Ok, StatusTopOrEnd::Top))
}
KeyCode::Down | KeyCode::Char('j') => {
self.handle_down();
Ok((Transition::Ok, StatusTopOrEnd::End))
}
KeyCode::Left | KeyCode::Char('h') => {
self.handle_left();
Ok((Transition::Ok, StatusTopOrEnd::None))
}
KeyCode::Right | KeyCode::Char('l') => {
self.handle_right();
Ok((Transition::Ok, StatusTopOrEnd::None))
}
KeyCode::Home | KeyCode::Char('g') => {
self.get_cursor().row_move_to_start();
Ok((Transition::Ok, StatusTopOrEnd::Top))
}
KeyCode::End | KeyCode::Char('G') => {
self.get_cursor().row_move_to_end();
Ok((Transition::Ok, StatusTopOrEnd::End))
}
_ => Ok((Transition::None, StatusTopOrEnd::None)),
}
}
}

View File

@ -89,7 +89,7 @@ pub trait View {
layout: &Layout, layout: &Layout,
info: &mut ViewInfo, info: &mut ViewInfo,
key: KeyEvent, key: KeyEvent,
) -> Option<Transition>; ) -> Transition;
fn show_data(&mut self, _: usize) -> bool { fn show_data(&mut self, _: usize) -> bool {
false false
@ -116,7 +116,7 @@ impl View for Box<dyn View> {
layout: &Layout, layout: &Layout,
info: &mut ViewInfo, info: &mut ViewInfo,
key: KeyEvent, key: KeyEvent,
) -> Option<Transition> { ) -> Transition {
self.as_mut() self.as_mut()
.handle_input(engine_state, stack, layout, info, key) .handle_input(engine_state, stack, layout, info, key)
} }

View File

@ -1,11 +1,12 @@
use super::{ use super::{
colored_text_widget::ColoredTextWidget, cursor::WindowCursor2D, Layout, View, ViewConfig, colored_text_widget::ColoredTextWidget, cursor::CursorMoveHandler, cursor::WindowCursor2D,
Layout, View, ViewConfig,
}; };
use crate::{ use crate::{
nu_common::{NuSpan, NuText}, nu_common::{NuSpan, NuText},
pager::{report::Report, Frame, Transition, ViewInfo}, pager::{report::Report, Frame, StatusTopOrEnd, Transition, ViewInfo},
}; };
use crossterm::event::{KeyCode, KeyEvent}; use crossterm::event::KeyEvent;
use nu_color_config::TextStyle; use nu_color_config::TextStyle;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
@ -65,58 +66,17 @@ impl View for Preview {
_: &Layout, _: &Layout,
info: &mut ViewInfo, // add this arg to draw too? info: &mut ViewInfo, // add this arg to draw too?
key: KeyEvent, key: KeyEvent,
) -> Option<Transition> { ) -> Transition {
match key.code { match self.handle_input_key(&key) {
KeyCode::Left => { Ok((transition, status_top_or_end)) => {
self.cursor match status_top_or_end {
.prev_column_by(max(1, self.cursor.window_width_in_columns() / 2)); StatusTopOrEnd::Top => set_status_top(self, info),
StatusTopOrEnd::End => set_status_end(self, info),
Some(Transition::Ok) _ => {}
}
transition
} }
KeyCode::Right => { _ => Transition::None, // currently only handle_enter() in crates/nu-explore/src/views/record/mod.rs raises an Err()
self.cursor
.next_column_by(max(1, self.cursor.window_width_in_columns() / 2));
Some(Transition::Ok)
}
KeyCode::Up => {
self.cursor.prev_row_i();
set_status_top(self, info);
Some(Transition::Ok)
}
KeyCode::Down => {
self.cursor.next_row_i();
set_status_end(self, info);
Some(Transition::Ok)
}
KeyCode::PageUp => {
self.cursor.prev_row_page();
set_status_top(self, info);
Some(Transition::Ok)
}
KeyCode::PageDown => {
self.cursor.next_row_page();
set_status_end(self, info);
Some(Transition::Ok)
}
KeyCode::Home => {
self.cursor.row_move_to_start();
set_status_top(self, info);
Some(Transition::Ok)
}
KeyCode::End => {
self.cursor.row_move_to_end();
set_status_end(self, info);
Some(Transition::Ok)
}
KeyCode::Esc => Some(Transition::Exit),
_ => None,
} }
} }
@ -147,6 +107,20 @@ impl View for Preview {
} }
} }
impl CursorMoveHandler for Preview {
fn get_cursor(&mut self) -> &mut WindowCursor2D {
&mut self.cursor
}
fn handle_left(&mut self) {
self.cursor
.prev_column_by(max(1, self.cursor.window_width_in_columns() / 2));
}
fn handle_right(&mut self) {
self.cursor
.next_column_by(max(1, self.cursor.window_width_in_columns() / 2));
}
}
fn set_status_end(view: &Preview, info: &mut ViewInfo) { fn set_status_end(view: &Preview, info: &mut ViewInfo) {
if view.cursor.row() + 1 == view.cursor.row_limit() { if view.cursor.row() + 1 == view.cursor.row_limit() {
info.status = Some(Report::info("END")); info.status = Some(Report::info("END"));

View File

@ -2,7 +2,7 @@ mod table_widget;
use self::table_widget::{TableWidget, TableWidgetState}; use self::table_widget::{TableWidget, TableWidgetState};
use super::{ use super::{
cursor::{Position, WindowCursor2D}, cursor::{CursorMoveHandler, Position, WindowCursor2D},
util::{make_styled_string, nu_style_to_tui}, util::{make_styled_string, nu_style_to_tui},
Layout, View, ViewConfig, Layout, View, ViewConfig,
}; };
@ -16,7 +16,7 @@ use crate::{
views::ElementInfo, views::ElementInfo,
}; };
use anyhow::Result; use anyhow::Result;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::KeyEvent;
use nu_color_config::StyleComputer; use nu_color_config::StyleComputer;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
@ -213,26 +213,21 @@ impl View for RecordView {
_: &Layout, _: &Layout,
info: &mut ViewInfo, info: &mut ViewInfo,
key: KeyEvent, key: KeyEvent,
) -> Option<Transition> { ) -> Transition {
let result = match self.mode { match self.handle_input_key(&key) {
UIMode::View => Ok(handle_key_event_view_mode(self, &key)), Ok((transition, ..)) => {
UIMode::Cursor => handle_key_event_cursor_mode(self, &key), if matches!(&transition, Transition::Ok | Transition::Cmd { .. }) {
};
match result {
Ok(result) => {
if matches!(&result, Some(Transition::Ok) | Some(Transition::Cmd { .. })) {
let report = self.create_records_report(); let report = self.create_records_report();
info.status = Some(report); info.status = Some(report);
} }
result transition
} }
Err(e) => { Err(e) => {
log::error!("Error handling input in RecordView: {e}"); log::error!("Error handling input in RecordView: {e}");
let report = Report::message(e.to_string(), Severity::Err); let report = Report::message(e.to_string(), Severity::Err);
info.status = Some(report); info.status = Some(report);
None Transition::None
} }
} }
} }
@ -372,188 +367,94 @@ impl RecordLayer {
} }
} }
fn handle_key_event_view_mode(view: &mut RecordView, key: &KeyEvent) -> Option<Transition> { impl CursorMoveHandler for RecordView {
match key { fn get_cursor(&mut self) -> &mut WindowCursor2D {
KeyEvent { &mut self.get_top_layer_mut().cursor
code: KeyCode::Char('u'),
modifiers: KeyModifiers::CONTROL,
..
}
| KeyEvent {
code: KeyCode::PageUp,
..
} => {
view.get_top_layer_mut().cursor.prev_row_page();
return Some(Transition::Ok);
}
KeyEvent {
code: KeyCode::Char('d'),
modifiers: KeyModifiers::CONTROL,
..
}
| KeyEvent {
code: KeyCode::PageDown,
..
} => {
view.get_top_layer_mut().cursor.next_row_page();
return Some(Transition::Ok);
}
_ => {}
} }
fn handle_enter(&mut self) -> Result<Transition> {
match self.mode {
UIMode::View => self.set_cursor_mode(),
UIMode::Cursor => {
let value = self.get_current_value();
match key.code { // ...but it only makes sense to drill down into a few types of values
KeyCode::Esc => { if !matches!(
if view.layer_stack.len() > 1 { value,
view.layer_stack.pop(); Value::Record { .. } | Value::List { .. } | Value::Custom { .. }
view.mode = UIMode::Cursor; ) {
return Ok(Transition::None);
}
Some(Transition::Ok) let is_record = matches!(value, Value::Record { .. });
} else { let next_layer = create_layer(value.clone())?;
Some(Transition::Exit) push_layer(self, next_layer);
if is_record {
self.set_top_layer_orientation(Orientation::Left);
} else {
self.set_top_layer_orientation(self.orientation);
}
} }
} }
KeyCode::Char('i') | KeyCode::Enter => {
view.set_cursor_mode();
Some(Transition::Ok) Ok(Transition::Ok)
}
KeyCode::Char('t') => {
view.transpose();
Some(Transition::Ok)
}
KeyCode::Char('e') => Some(Transition::Cmd(String::from("expand"))),
KeyCode::Up | KeyCode::Char('k') => {
view.get_top_layer_mut().cursor.prev_row_i();
Some(Transition::Ok)
}
KeyCode::Down | KeyCode::Char('j') => {
view.get_top_layer_mut().cursor.next_row_i();
Some(Transition::Ok)
}
KeyCode::Left | KeyCode::Char('h') => {
view.get_top_layer_mut().cursor.prev_column_i();
Some(Transition::Ok)
}
KeyCode::Right | KeyCode::Char('l') => {
view.get_top_layer_mut().cursor.next_column_i();
Some(Transition::Ok)
}
KeyCode::Home | KeyCode::Char('g') => {
view.get_top_layer_mut().cursor.row_move_to_start();
Some(Transition::Ok)
}
KeyCode::End | KeyCode::Char('G') => {
view.get_top_layer_mut().cursor.row_move_to_end();
Some(Transition::Ok)
}
_ => None,
} }
} fn handle_esc(&mut self) -> Transition {
match self.mode {
fn handle_key_event_cursor_mode( UIMode::View => {
view: &mut RecordView, if self.layer_stack.len() > 1 {
key: &KeyEvent, self.layer_stack.pop();
) -> Result<Option<Transition>> { self.mode = UIMode::Cursor;
match key { } else {
KeyEvent { return Transition::Exit;
code: KeyCode::Char('u'), }
modifiers: KeyModifiers::CONTROL, }
.. UIMode::Cursor => self.set_view_mode(),
} }
| KeyEvent { Transition::Ok
code: KeyCode::PageUp,
..
} => {
view.get_top_layer_mut().cursor.prev_row_page();
return Ok(Some(Transition::Ok));
}
KeyEvent {
code: KeyCode::Char('d'),
modifiers: KeyModifiers::CONTROL,
..
}
| KeyEvent {
code: KeyCode::PageDown,
..
} => {
view.get_top_layer_mut().cursor.next_row_page();
return Ok(Some(Transition::Ok));
}
_ => {}
} }
fn handle_expand(&mut self) -> Transition {
match key.code { match self.mode {
KeyCode::Esc => { UIMode::View => Transition::Cmd(String::from("expand")),
view.set_view_mode(); _ => Transition::None,
Ok(Some(Transition::Ok))
} }
KeyCode::Up | KeyCode::Char('k') => { }
view.get_top_layer_mut().cursor.prev_row(); fn handle_transpose(&mut self) -> Transition {
match self.mode {
UIMode::View => {
self.transpose();
Ok(Some(Transition::Ok)) Transition::Ok
}
KeyCode::Down | KeyCode::Char('j') => {
view.get_top_layer_mut().cursor.next_row();
Ok(Some(Transition::Ok))
}
KeyCode::Left | KeyCode::Char('h') => {
view.get_top_layer_mut().cursor.prev_column();
Ok(Some(Transition::Ok))
}
KeyCode::Right | KeyCode::Char('l') => {
view.get_top_layer_mut().cursor.next_column();
Ok(Some(Transition::Ok))
}
KeyCode::Home | KeyCode::Char('g') => {
view.get_top_layer_mut().cursor.row_move_to_start();
Ok(Some(Transition::Ok))
}
KeyCode::End | KeyCode::Char('G') => {
view.get_top_layer_mut().cursor.row_move_to_end();
Ok(Some(Transition::Ok))
}
// Try to "drill down" into the selected value
KeyCode::Enter => {
let value = view.get_current_value();
// ...but it only makes sense to drill down into a few types of values
if !matches!(
value,
Value::Record { .. } | Value::List { .. } | Value::Custom { .. }
) {
return Ok(None);
} }
_ => Transition::None,
let is_record = matches!(value, Value::Record { .. }); }
let next_layer = create_layer(value.clone())?; }
push_layer(view, next_layer); // for these, copy standard CursorMoveHandler for UIMode::View, but use special handling for UIMode::Cursor
// NOTE: https://stackoverflow.com/a/31462293/2016290 says there's plans for Rust to allow calling super functions,
if is_record { // but not yet, and since they're all one line, it seems simpler to copy than make a lot of helper functions
view.set_top_layer_orientation(Orientation::Left); fn handle_left(&mut self) {
} else { match self.mode {
view.set_top_layer_orientation(view.orientation); UIMode::View => self.get_top_layer_mut().cursor.prev_column_i(),
} _ => self.get_top_layer_mut().cursor.prev_column(),
}
Ok(Some(Transition::Ok)) }
fn handle_right(&mut self) {
match self.mode {
UIMode::View => self.get_top_layer_mut().cursor.next_column_i(),
_ => self.get_top_layer_mut().cursor.next_column(),
}
}
fn handle_up(&mut self) {
match self.mode {
UIMode::View => self.get_top_layer_mut().cursor.prev_row_i(),
_ => self.get_top_layer_mut().cursor.prev_row(),
}
}
fn handle_down(&mut self) {
match self.mode {
UIMode::View => self.get_top_layer_mut().cursor.next_row_i(),
_ => self.get_top_layer_mut().cursor.next_row(),
} }
_ => Ok(None),
} }
} }

View File

@ -149,7 +149,7 @@ impl View for TryView {
layout: &Layout, layout: &Layout,
info: &mut ViewInfo, info: &mut ViewInfo,
key: KeyEvent, key: KeyEvent,
) -> Option<Transition> { ) -> Transition {
if self.view_mode { if self.view_mode {
let table = self let table = self
.table .table
@ -160,28 +160,28 @@ impl View for TryView {
if was_at_the_top && matches!(key.code, KeyCode::Up | KeyCode::PageUp) { if was_at_the_top && matches!(key.code, KeyCode::Up | KeyCode::PageUp) {
self.view_mode = false; self.view_mode = false;
return Some(Transition::Ok); return Transition::Ok;
} }
if matches!(key.code, KeyCode::Tab) { if matches!(key.code, KeyCode::Tab) {
self.view_mode = false; self.view_mode = false;
return Some(Transition::Ok); return Transition::Ok;
} }
let result = table.handle_input(engine_state, stack, layout, info, key); let result = table.handle_input(engine_state, stack, layout, info, key);
return match result { return match result {
Some(Transition::Ok | Transition::Cmd { .. }) => Some(Transition::Ok), Transition::Ok | Transition::Cmd { .. } => Transition::Ok,
Some(Transition::Exit) => { Transition::Exit => {
self.view_mode = false; self.view_mode = false;
Some(Transition::Ok) Transition::Ok
} }
None => None, Transition::None => Transition::None,
}; };
} }
match &key.code { match &key.code {
KeyCode::Esc => Some(Transition::Exit), KeyCode::Esc => Transition::Exit,
KeyCode::Backspace => { KeyCode::Backspace => {
if !self.command.is_empty() { if !self.command.is_empty() {
self.command.pop(); self.command.pop();
@ -194,7 +194,7 @@ impl View for TryView {
} }
} }
Some(Transition::Ok) Transition::Ok
} }
KeyCode::Char(c) => { KeyCode::Char(c) => {
self.command.push(*c); self.command.push(*c);
@ -206,14 +206,14 @@ impl View for TryView {
} }
} }
Some(Transition::Ok) Transition::Ok
} }
KeyCode::Down | KeyCode::Tab => { KeyCode::Down | KeyCode::Tab => {
if self.table.is_some() { if self.table.is_some() {
self.view_mode = true; self.view_mode = true;
} }
Some(Transition::Ok) Transition::Ok
} }
KeyCode::Enter => { KeyCode::Enter => {
match self.try_run(engine_state, stack) { match self.try_run(engine_state, stack) {
@ -221,9 +221,9 @@ impl View for TryView {
Err(err) => info.report = Some(Report::error(format!("Error: {err}"))), Err(err) => info.report = Some(Report::error(format!("Error: {err}"))),
} }
Some(Transition::Ok) Transition::Ok
} }
_ => None, _ => Transition::None,
} }
} }