diff --git a/CHANGELOG.md b/CHANGELOG.md index d5b87c1b..f4a57ca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ ### Fixed - Differentiate history between different collections [#10](https://github.com/LucasPickering/slumber/issues/10) +- Ensure ctrl-c can't get eaten by text boxes (it guarantees exit now) [#18](https://github.com/LucasPickering/slumber/issues/18) ## [0.4.0] - 2023-11-02 diff --git a/src/tui/input.rs b/src/tui/input.rs index a8a6ef67..6e135b89 100644 --- a/src/tui/input.rs +++ b/src/tui/input.rs @@ -12,6 +12,12 @@ use tracing::trace; /// events to actions, but then the actions are actually processed by the view. #[derive(Debug)] pub struct InputEngine { + /// Intuitively this should be binding:action, but we can't look up a + /// binding from the map based on an input event, because event<=>binding + /// matching is more nuanced that simple equality (e.g. bonus modifiers + /// keys can be ignored). We have to iterate over map when checking inputs, + /// but keying by action at least allows us to look up action=>binding for + /// help text. bindings: HashMap, } @@ -19,17 +25,14 @@ impl InputEngine { pub fn new() -> Self { Self { bindings: [ - InputBinding { - action: Action::Quit, - primary: KeyCombination { - key_code: KeyCode::Char('q'), - modifiers: KeyModifiers::NONE, - }, - secondary: Some(KeyCombination { + InputBinding::new(KeyCode::Char('q'), Action::Quit), + InputBinding::new( + KeyCombination { key_code: KeyCode::Char('c'), modifiers: KeyModifiers::CONTROL, - }), - }, + }, + Action::ForceQuit, + ), InputBinding::new(KeyCode::Char('r'), Action::ReloadCollection), InputBinding::new(KeyCode::F(11), Action::Fullscreen), InputBinding::new(KeyCode::BackTab, Action::PreviousPane), @@ -91,6 +94,10 @@ impl InputEngine { pub enum Action { /// Exit the app Quit, + /// A special keybinding that short-circuits the standard view input + /// process to force an exit. Standard shutdown will *still run*, but this + /// input can't be consumed by any components in the view tree. + ForceQuit, /// Reload the request collection from the same file as the initial load #[display(fmt = "Reload Collection")] ReloadCollection, @@ -120,36 +127,26 @@ pub enum Action { #[derive(Copy, Clone, Debug)] pub struct InputBinding { action: Action, - primary: KeyCombination, - secondary: Option, + input: KeyCombination, } impl InputBinding { - /// Create a binding with only a primary - const fn new(key_code: KeyCode, action: Action) -> Self { + fn new(input: impl Into, action: Action) -> Self { Self { action, - primary: KeyCombination { - key_code, - modifiers: KeyModifiers::NONE, - }, - secondary: None, + input: input.into(), } } fn matches(&self, event: &KeyEvent) -> bool { - self.primary.matches(event) - || self - .secondary - .map(|secondary| secondary.matches(event)) - .unwrap_or_default() + self.input.matches(event) } } impl Display for InputBinding { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // Don't display secondary binding in help text - write!(f, "{} {}", self.primary, self.action) + write!(f, "{} {}", self.input, self.action) } } @@ -185,3 +182,12 @@ impl Display for KeyCombination { } } } + +impl From for KeyCombination { + fn from(key_code: KeyCode) -> Self { + Self { + key_code, + modifiers: KeyModifiers::NONE, + } + } +} diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 9dbe1d2f..995a301b 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -7,7 +7,7 @@ use crate::{ http::{HttpEngine, Repository, RequestBuilder}, template::TemplateContext, tui::{ - input::InputEngine, + input::{Action, InputEngine}, message::{Message, MessageSender}, view::{ModalPriority, RequestState, View}, }, @@ -123,7 +123,13 @@ impl Tui { // editors and such let event = crossterm::event::read()?; let action = self.input_engine.action(&event); - self.view.handle_input(event, action); + if let Some(Action::ForceQuit) = action { + // Short-circuit the view/message cycle, to make sure this + // doesn't get ate + self.quit(); + } else { + self.view.handle_input(event, action); + } } if last_tick.elapsed() >= Self::TICK_TIME { last_tick = Instant::now(); @@ -143,13 +149,18 @@ impl Tui { // ===== Signal Phase ===== if quit_signals.pending().next().is_some() { - self.should_run = false; + self.quit(); } } Ok(()) } + /// GOODBYE + fn quit(&mut self) { + self.should_run = false; + } + /// Handle an incoming message. Any error here will be displayed as a modal fn handle_message(&mut self, message: Message) -> anyhow::Result<()> { match message { @@ -217,7 +228,7 @@ impl Tui { Message::Error { error } => { self.view.open_modal(error, ModalPriority::High) } - Message::Quit => self.should_run = false, + Message::Quit => self.quit(), } Ok(()) }