Skip to content

Commit

Permalink
Move resolve_term to tui_editor, add keybindings
Browse files Browse the repository at this point in the history
Making keybindings explicit.
  • Loading branch information
durbanlegend committed Sep 17, 2024
1 parent 0963775 commit 656b8a5
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 70 deletions.
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,10 +113,18 @@ cat my_file.rs | thag --edit # Short form: -d
This allows you to edit or append to the stdin input before submitting it to `thag_rs`. It has file-backed history so you don't lose your edits.

#### A note on the TUI editor
In order for the Shift-Up and Shift-Down key combinations to work on Apple Terminal, you may need to add the following to your Apple Terminal Settings | Profiles | Keyboard settings:
`thag_rs` tries to define all key bindings explicitly. However, the terminal emulator you use is bound to intercept some of these keys, rendering them unavailable to us.
If specific key bindings don't work for you, you may have to adjust your terminal settings. For example:

1. In order for the Option key on Mac to behave correctly in `iterm2`, you may need to choose `iterm2` Settings | Profiles | Keys to specify how to handle the left and right Option keys.
You can choose one of: Normal, Meta or Esc+. The `iterm2` recommended setting of Esc+ is what works in testing.

2. In order for the Shift-Up and Shift-Down key combinations to work on Apple Terminal, you may need to add the following to your Apple Terminal Settings | Profiles | Keyboard settings:
Shift-Up: `\033;[2A` and `Shift-Down`: \033;[2B. Use the Esc key to generate \033. This is not necessary on Iterm2 or WezTerm.

In general, if you don't experience the key bindings you want, it is probably because your terminal has intercepted them and you may be able to resolve the issue by adjusting your terminal settings. The same applies to the REPL.
If all else fails, try another terminal emulator.

All of the above also applies to `REPL` mode.

### * As a filter on standard input (loop mode):

Expand Down
2 changes: 1 addition & 1 deletion demo/crokey_print_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub fn main() {
println!("Arg! You savagely killed me with a {}", key.red());
break;
}
key!(ctrl - q) => {
key!(ctrl - q) | key!(ctrl - q - q - q) => {
println!("You typed {} which gracefully quits", key.green());
break;
}
Expand Down
62 changes: 7 additions & 55 deletions src/repl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,37 @@ use crate::colors::{TuiSelectionBg, TUI_SELECTION_BG};
#[cfg(debug_assertions)]
use crate::debug_log;
use crate::errors::ThagError;
use crate::log;
use crate::logging::Verbosity;
use crate::shared::Ast;
use crate::stdin::{apply_highlights, normalize_newlines, reset_term, show_popup};
use crate::stdin::{apply_highlights, normalize_newlines, show_popup};
use crate::tui_editor::{
edit as tui_edit, CrosstermEventReader, Display, EditData, EventReader, History, KeyAction,
ResetTermClosure, TermScopeGuard,
TermScopeGuard,
};
use crate::{
colors::{nu_resolve_style, MessageLevel},
gen_build_run, nu_color_println,
shared::BuildState,
};
use crate::{log, tui_editor};

use clap::{CommandFactory, Parser};
use crokey::{crossterm, key, KeyCombination, KeyCombinationFormat};
use crossterm::event::{
EnableBracketedPaste,
EnableMouseCapture,
Event::{self, Paste},
KeyEvent, // KeyCode, KeyEvent, KeyModifiers,
KeyEvent,
};
use crossterm::terminal::{enable_raw_mode, EnterAlternateScreen};
use firestorm::profile_fn;
use lazy_static::lazy_static;
use ratatui::prelude::CrosstermBackend;
use ratatui::style::{Color, Style, Stylize};
use ratatui::widgets::{Block, Borders};
use ratatui::Terminal;
use reedline::{
default_emacs_keybindings, ColumnarMenu, DefaultCompleter, DefaultHinter, DefaultValidator,
EditCommand, Emacs, FileBackedHistory, HistoryItem, KeyCode, KeyModifiers, Keybindings,
MenuBuilder, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, Reedline,
ReedlineEvent, ReedlineMenu, Signal,
};
use regex::Regex;
use scopeguard::guard;
use std::borrow::Cow;
use std::collections::HashMap;
use std::env::var;
Expand Down Expand Up @@ -553,7 +547,7 @@ fn review_history(
eprintln!("saved_history={saved_history}");
history_mut.clear()?;
for line in saved_history.lines() {
eprintln!("saving line={line}");
// eprintln!("saving line={line}");
let _ = history_mut.save(HistoryItem::from_command_line(line))?;
}
history_mut.sync()?;
Expand Down Expand Up @@ -962,7 +956,7 @@ pub fn edit_history<R: EventReader + Debug>(
let mut tui_highlight_bg = &*TUI_SELECTION_BG;
let mut saved = false;

let mut maybe_term = resolve_term()?;
let mut maybe_term = tui_editor::resolve_term()?;

let mut textarea = TextArea::from(initial_content.lines());

Expand Down Expand Up @@ -1000,7 +994,7 @@ pub fn edit_history<R: EventReader + Debug>(
e
})?;

// terminal::enable_raw_mode()?;
// NB: leave in raw mode until end of session to avoid random appearance of OSC codes on screen
let event = event_reader.read_event();
// terminal::disable_raw_mode()?;
event.map_err(Into::<ThagError>::into) // Convert io::Error to ThagError
Expand Down Expand Up @@ -1074,48 +1068,6 @@ pub fn edit_history<R: EventReader + Debug>(
}
}

/// Determine whether a terminal is in use (as opposed to testing or headless CI), and
/// if so, wrap it in a scopeguard in order to reset it regardless of success or failure.
///
/// # Panics
///
/// Panics if a `crossterm` error is encountered resetting the terminal inside a
/// `scopeguard::guard` closure.
///
/// # Errors
///
pub fn resolve_term() -> Result<Option<TermScopeGuard>, ThagError> {
let maybe_term = if var("TEST_ENV").is_ok() {
None
} else {
let mut stdout = std::io::stdout().lock();

enable_raw_mode()?;

crossterm::execute!(
stdout,
EnterAlternateScreen,
EnableMouseCapture,
EnableBracketedPaste
)
.map_err(|e| e)?;

let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;

// Box the closure explicitly as `Box<dyn FnOnce>`
let term = guard(
terminal,
Box::new(|term| {
reset_term(term).expect("Error resetting terminal");
}) as ResetTermClosure,
);

Some(term)
};
Ok(maybe_term)
}

/// Save the `textarea` contents to a history staging file.
///
/// # Errors
Expand Down
13 changes: 8 additions & 5 deletions src/stdin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ pub fn edit<R: EventReader>(event_reader: &R) -> Result<Vec<String>, ThagError>
println!("Error drawing terminal: {:?}", e);
e
})?;
// terminal::enable_raw_mode()?;
// NB: leave in raw mode until end of session to avoid random appearance of OSC codes on screen
// let event = crossterm::event::read();
let event = event_reader.read_event();
// terminal::disable_raw_mode()?;
Expand Down Expand Up @@ -916,10 +916,13 @@ fn centered_rect(max_width: u16, max_height: u16, r: Rect) -> Rect {

const MAPPINGS: &[[&str; 2]; 35] = &[
["Key Bindings", "Description"],
["Shift+arrow keys", "Select/deselect ← chars→ / ↑ lines↓"],
[
"Shift+arrow keys",
"Select/deselect chars (←→) or lines (↑↓)",
],
[
"Shift+Ctrl+arrow keys",
"Select/deselect words→ / ↑ paras↓",
"Select/deselect words (←→) or paras (↑↓)",
],
["Ctrl+D", "Submit"],
["Ctrl+Q", "Cancel and quit"],
Expand All @@ -928,7 +931,7 @@ const MAPPINGS: &[[&str; 2]; 35] = &[
["Ctrl+M, Enter", "Insert newline"],
["Ctrl+K", "Delete from cursor to end of line"],
["Ctrl+J", "Delete from cursor to start of line"],
["Ctrl+W, Alt+<, Backspace", "Delete one word before cursor"],
["Ctrl+W, Backspace", "Delete one word before cursor"],
["Alt+D, Delete", "Delete one word from cursor position"],
["Ctrl+U", "Undo"],
["Ctrl+R", "Redo"],
Expand All @@ -953,7 +956,7 @@ const MAPPINGS: &[[&str; 2]; 35] = &[
"Move cursor to start of line",
],
["Alt+<, Ctrl+Alt+P or ↑", "Move cursor to top of file"],
["Alt+>, Ctrl+Alt+N or↓", "Move cursor to bottom of file"],
["Alt+>, Ctrl+Alt+N or ↓", "Move cursor to bottom of file"],
["PageDown, Cmd+↓", "Page down"],
["Alt+V, PageUp, Cmd+↑", "Page up"],
["Ctrl+T", "Toggle highlight colours"],
Expand Down
81 changes: 74 additions & 7 deletions src/tui_editor.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use crokey::{key, KeyCombination};
use crossterm::event::Event::{self, Paste};
use crossterm::event::KeyEvent;
use crossterm::event::{
EnableBracketedPaste, EnableMouseCapture,
Event::{self, Paste},
KeyEvent,
};
use crossterm::terminal::{enable_raw_mode, EnterAlternateScreen};
use mockall::automock;
use ratatui::prelude::CrosstermBackend;
use ratatui::style::{Color, Style};
use ratatui::widgets::{Block, Borders};
use ratatui::Terminal;
use scopeguard::ScopeGuard;
use scopeguard::{guard, ScopeGuard};
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::convert::Into;
Expand All @@ -17,15 +21,56 @@ use std::path::PathBuf;
use tui_textarea::{CursorMove, Input, TextArea};

use crate::colors::{TuiSelectionBg, TUI_SELECTION_BG};
use crate::repl::resolve_term;
use crate::stdin::{apply_highlights, normalize_newlines, show_popup};
use crate::stdin::{apply_highlights, normalize_newlines, reset_term, show_popup};
use crate::{ThagError, ThagResult};

pub type BackEnd = CrosstermBackend<std::io::StdoutLock<'static>>;
pub type Term = Terminal<BackEnd>;
pub type ResetTermClosure = Box<dyn FnOnce(Term)>;
pub type TermScopeGuard = ScopeGuard<Term, ResetTermClosure>;

/// Determine whether a terminal is in use (as opposed to testing or headless CI), and
/// if so, wrap it in a scopeguard in order to reset it regardless of success or failure.
///
/// # Panics
///
/// Panics if a `crossterm` error is encountered resetting the terminal inside a
/// `scopeguard::guard` closure.
///
/// # Errors
///
pub fn resolve_term() -> Result<Option<TermScopeGuard>, ThagError> {
let maybe_term = if var("TEST_ENV").is_ok() {
None
} else {
let mut stdout = std::io::stdout().lock();

enable_raw_mode()?;

crossterm::execute!(
stdout,
EnterAlternateScreen,
EnableMouseCapture,
EnableBracketedPaste
)
.map_err(|e| e)?;

let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;

// Box the closure explicitly as `Box<dyn FnOnce>`
let term = guard(
terminal,
Box::new(|term| {
reset_term(term).expect("Error resetting terminal");
}) as ResetTermClosure,
);

Some(term)
};
Ok(maybe_term)
}

#[derive(Default, Serialize, Deserialize)]
pub struct History {
entries: VecDeque<String>,
Expand Down Expand Up @@ -158,9 +203,8 @@ where
e
})?;

// terminal::enable_raw_mode()?;
// NB: leave in raw mode until end of session to avoid random appearance of OSC codes on screen
let event = event_reader.read_event();
// terminal::disable_raw_mode()?;
event.map_err(Into::<ThagError>::into)
},
)?
Expand All @@ -171,8 +215,31 @@ where
} else if let Event::Key(key_event) = event {
let key_combination = KeyCombination::from(key_event); // Derive KeyCombination

// If using iterm2, ensure Settings | Profiles | Keys | Left Option key is set to Esc+.
#[allow(clippy::unnested_or_patterns)]
match key_combination {
key!(ctrl - h) | key!(backspace) => {
textarea.delete_char();
}
key!(ctrl - i) | key!(tab) => {
textarea.indent();
}
key!(ctrl - m) | key!(enter) => {
textarea.insert_newline();
}
key!(ctrl - k) => {
textarea.delete_line_by_end();
}
key!(ctrl - j) => {
textarea.delete_line_by_head();
}
key!(ctrl - j) => {
textarea.delete_line_by_head();
}
key!(ctrl - w) | key!(enter) | key!(enter) => {
textarea.delete_line_by_head();
}
//
key!(ctrl - alt - p) | key!(alt - '<') | key!(alt - up) => {
textarea.move_cursor(CursorMove::Top);
}
Expand Down

0 comments on commit 656b8a5

Please sign in to comment.