From 028026049d369a19131173cb155bda6eb97886ee Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Sun, 2 Jan 2022 19:41:21 -0700 Subject: [PATCH] add window:dead_key_is_active window:leader_is_active This commit adds a couple of helper methods that provide insight into the state of the keyboard layer. The intent is for users to add status information about the keyboard state. This commit also ensures that we schedule an update when the leader key duration expires, and ensures that we close out the leader state for an invalid key press. refs: #686 closes: #688 ```lua local wezterm = require 'wezterm'; wezterm.on("update-right-status", function(window, pane) local leader = "" if window:leader_is_active() then leader = "LEADER" end local dead = "" if window:dead_key_active() then dead = "DEAD" end window:set_right_status(leader .. " " .. dead) end); return { leader = { key="a", mods="CTRL" }, colors = { dead_key_cursor = "orange", }, } ``` --- docs/changelog.md | 4 + docs/config/lua/window/dead_key_is_active.md | 30 ++++++ docs/config/lua/window/leader_is_active.md | 29 +++++ wezterm-gui/src/scripting/guiwin.rs | 28 +++++ wezterm-gui/src/termwindow/keyevent.rs | 107 +++++++++++++------ wezterm-gui/src/termwindow/mod.rs | 4 +- wezterm-gui/src/termwindow/render.rs | 4 +- 7 files changed, 171 insertions(+), 35 deletions(-) create mode 100644 docs/config/lua/window/dead_key_is_active.md create mode 100644 docs/config/lua/window/leader_is_active.md diff --git a/docs/changelog.md b/docs/changelog.md index a96d58b80..bfa89e10f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -12,6 +12,10 @@ usually the best available version. As features stabilize some brief notes about them will accumulate here. #### New + +* [window:dead_key_is_active()](config/lua/window/dead_key_is_active.md) and [window:leader_is_active()](config/lua/window/leader_is_active.md) methods that can help populate [window:set_right_status()](config/lua/window/set_right_status.md) [#686](https://github.com/wez/wezterm/issues/686) +* You may now use `colors = { dead_key_cursor = "orange" }` to change the cursor color when either a dead key or leader key is active. + #### Changed #### Updated and Improved #### Fixed diff --git a/docs/config/lua/window/dead_key_is_active.md b/docs/config/lua/window/dead_key_is_active.md new file mode 100644 index 000000000..8153436bc --- /dev/null +++ b/docs/config/lua/window/dead_key_is_active.md @@ -0,0 +1,30 @@ +# wezterm:dead_key_is_active() + +*Since: nightly builds only* + +Returns `true` if a dead key is active in the window, or false otherwise. + +This example shows `DEAD` in the right status area, and turns the cursor orange, +when a dead key is active: + +```lua +local wezterm = require 'wezterm'; + +wezterm.on("update-right-status", function(window, pane) + local dead = "" + if window:dead_key_is_active() then + dead = "DEAD" + end + window:set_right_status(dead) +end); + +return { + leader = { key="a", mods="CTRL" }, + colors = { + dead_key_cursor = "orange", + }, +} +``` + +See also: [window:leader_is_active()](leader_is_active.md). + diff --git a/docs/config/lua/window/leader_is_active.md b/docs/config/lua/window/leader_is_active.md new file mode 100644 index 000000000..d6c641cd1 --- /dev/null +++ b/docs/config/lua/window/leader_is_active.md @@ -0,0 +1,29 @@ +# wezterm:leader_is_active() + +*Since: nightly builds only* + +Returns `true` if the [Leader Key](../../keys.md) is active in the window, or false otherwise. + +This example shows `LEADER` in the right status area, and turns the cursor orange, +when the leader is active: + +```lua +local wezterm = require 'wezterm'; + +wezterm.on("update-right-status", function(window, pane) + local leader = "" + if window:leader_is_active() then + leader = "LEADER" + end + window:set_right_status(leader) +end); + +return { + leader = { key="a", mods="CTRL" }, + colors = { + dead_key_cursor = "orange", + }, +} +``` + +See also: [window:dead_key_is_active()](dead_key_is_active.md). diff --git a/wezterm-gui/src/scripting/guiwin.rs b/wezterm-gui/src/scripting/guiwin.rs index 55a7ede35..aac16ea7a 100644 --- a/wezterm-gui/src/scripting/guiwin.rs +++ b/wezterm-gui/src/scripting/guiwin.rs @@ -132,5 +132,33 @@ impl UserData for GuiWin { .notify(TermWindowNotif::SetConfigOverrides(value.0)); Ok(()) }); + methods.add_async_method("leader_is_active", |_, this, _: ()| async move { + let (tx, rx) = smol::channel::bounded(1); + this.window + .notify(TermWindowNotif::Apply(Box::new(move |term_window| { + tx.try_send(term_window.leader_is_active()).ok(); + }))); + let result = rx + .recv() + .await + .map_err(|e| anyhow::anyhow!("{:#}", e)) + .map_err(luaerr)?; + + Ok(result) + }); + methods.add_async_method("dead_key_is_active", |_, this, _: ()| async move { + let (tx, rx) = smol::channel::bounded(1); + this.window + .notify(TermWindowNotif::Apply(Box::new(move |term_window| { + tx.try_send(term_window.dead_key_active()).ok(); + }))); + let result = rx + .recv() + .await + .map_err(|e| anyhow::anyhow!("{:#}", e)) + .map_err(luaerr)?; + + Ok(result) + }); } } diff --git a/wezterm-gui/src/termwindow/keyevent.rs b/wezterm-gui/src/termwindow/keyevent.rs index c4a4cbbdb..9ec68bf50 100644 --- a/wezterm-gui/src/termwindow/keyevent.rs +++ b/wezterm-gui/src/termwindow/keyevent.rs @@ -1,5 +1,6 @@ -use ::window::{KeyCode, KeyEvent, Modifiers, RawKeyEvent, WindowOps}; +use ::window::{DeadKeyStatus, KeyCode, KeyEvent, Modifiers, RawKeyEvent, WindowOps}; use mux::pane::Pane; +use smol::Timer; use std::rc::Rc; pub fn window_mods_to_termwiz_mods(modifiers: ::window::Modifiers) -> termwiz::input::Modifiers { @@ -56,8 +57,18 @@ impl super::TermWindow { // Check to see if this key-press is the leader activating if let Some(duration) = self.input_map.is_leader(&keycode, raw_modifiers) { // Yes; record its expiration - self.leader_is_down - .replace(std::time::Instant::now() + duration); + let target = std::time::Instant::now() + duration; + self.leader_is_down.replace(target); + self.update_title(); + // schedule an invalidation so that the cursor or status + // area will be repainted at the right time + if let Some(window) = self.window.clone() { + promise::spawn::spawn(async move { + Timer::at(target).await; + window.invalidate(); + }) + .detach(); + } return true; } } @@ -72,7 +83,7 @@ impl super::TermWindow { if leader_active { // A successful leader key-lookup cancels the leader // virtual modifier state - self.leader_is_down.take(); + self.leader_done(); } return true; } @@ -144,17 +155,11 @@ impl super::TermWindow { // The leader key is a kind of modal modifier key. // It is allowed to be active for up to the leader timeout duration, // after which it auto-deactivates. - let (leader_active, leader_mod) = match self.leader_is_down.as_ref() { - Some(expiry) if *expiry > std::time::Instant::now() => { - // Currently active - (true, Modifiers::LEADER) - } - Some(_) => { - // Expired; clear out the old expiration time - self.leader_is_down.take(); - (false, Modifiers::NONE) - } - _ => (false, Modifiers::NONE), + let (leader_active, leader_mod) = if self.leader_is_active_mut() { + // Currently active + (true, Modifiers::LEADER) + } else { + (false, Modifiers::NONE) }; let pane = match self.get_active_pane_or_overlay() { @@ -220,6 +225,43 @@ impl super::TermWindow { } } + pub fn leader_is_active(&self) -> bool { + match self.leader_is_down.as_ref() { + Some(expiry) if *expiry > std::time::Instant::now() => { + self.update_next_frame_time(Some(*expiry)); + true + } + Some(_) => false, + None => false, + } + } + + pub fn leader_is_active_mut(&mut self) -> bool { + match self.leader_is_down.as_ref() { + Some(expiry) if *expiry > std::time::Instant::now() => { + self.update_next_frame_time(Some(*expiry)); + true + } + Some(_) => { + self.leader_done(); + false + } + None => false, + } + } + + pub fn dead_key_active(&self) -> bool { + self.dead_key_status == DeadKeyStatus::Holding + } + + fn leader_done(&mut self) { + self.leader_is_down.take(); + self.update_title(); + if let Some(window) = &self.window { + window.invalidate(); + } + } + pub fn key_event_impl(&mut self, window_key: KeyEvent, context: &dyn WindowOps) { if !window_key.key_is_down { return; @@ -239,17 +281,11 @@ impl super::TermWindow { // The leader key is a kind of modal modifier key. // It is allowed to be active for up to the leader timeout duration, // after which it auto-deactivates. - let (leader_active, leader_mod) = match self.leader_is_down.as_ref() { - Some(expiry) if *expiry > std::time::Instant::now() => { - // Currently active - (true, Modifiers::LEADER) - } - Some(_) => { - // Expired; clear out the old expiration time - self.leader_is_down.take(); - (false, Modifiers::NONE) - } - _ => (false, Modifiers::NONE), + let (leader_active, leader_mod) = if self.leader_is_active_mut() { + // Currently active + (true, Modifiers::LEADER) + } else { + (false, Modifiers::NONE) }; let modifiers = window_mods_to_termwiz_mods(window_key.modifiers); @@ -318,8 +354,17 @@ impl super::TermWindow { } let key = self.win_key_code_to_termwiz_key_code(&window_key.key); + match key { Key::Code(key) => { + if leader_active && !key.is_modifier() { + // Leader was pressed and this non-modifier keypress isn't + // a registered key binding; swallow this event and cancel + // the leader modifier. + self.leader_done(); + return; + } + if pane.key_down(key, modifiers).is_ok() { if !key.is_modifier() && self.pane_state(pane.pane_id()).overlay.is_none() { self.maybe_scroll_to_bottom_for_input(&pane); @@ -335,12 +380,12 @@ impl super::TermWindow { // Leader was pressed and this non-modifier keypress isn't // a registered key binding; swallow this event and cancel // the leader modifier. - self.leader_is_down.take(); - } else { - pane.writer().write_all(s.as_bytes()).ok(); - self.maybe_scroll_to_bottom_for_input(&pane); - context.invalidate(); + self.leader_done(); + return; } + pane.writer().write_all(s.as_bytes()).ok(); + self.maybe_scroll_to_bottom_for_input(&pane); + context.invalidate(); } Key::None => {} } diff --git a/wezterm-gui/src/termwindow/mod.rs b/wezterm-gui/src/termwindow/mod.rs index 3649f9b33..4003c90f4 100644 --- a/wezterm-gui/src/termwindow/mod.rs +++ b/wezterm-gui/src/termwindow/mod.rs @@ -812,9 +812,9 @@ impl TermWindow { Ok(true) } WindowEvent::AdviseDeadKeyStatus(status) => { - log::warn!("DeadKeyStatus now: {:?}", status); + log::trace!("DeadKeyStatus now: {:?}", status); self.dead_key_status = status; - window.invalidate(); + self.update_title(); Ok(true) } WindowEvent::NeedRepaint => Ok(self.do_paint(window)), diff --git a/wezterm-gui/src/termwindow/render.rs b/wezterm-gui/src/termwindow/render.rs index 4bb1461eb..bef9a67be 100644 --- a/wezterm-gui/src/termwindow/render.rs +++ b/wezterm-gui/src/termwindow/render.rs @@ -315,7 +315,7 @@ impl super::TermWindow { } } - fn update_next_frame_time(&self, next_due: Option) { + pub fn update_next_frame_time(&self, next_due: Option) { if let Some(next_due) = next_due { let mut has_anim = self.has_animation.borrow_mut(); match *has_anim { @@ -2408,7 +2408,7 @@ impl super::TermWindow { } let dead_key_or_leader = - self.dead_key_status == DeadKeyStatus::Holding || self.leader_is_down.is_some(); + self.dead_key_status == DeadKeyStatus::Holding || self.leader_is_active(); if dead_key_or_leader { let (fg_color, bg_color) = if self.config.force_reverse_video_cursor {