clone
ssh://git@git.notashelf.dev:33/notashelf/beer.gitCommit b2d656e7bdb3
tarballunverified5 files changed+322-8
@@ -124,6 +124,8 @@ pub struct Grid { alt_saved: Option<Vec<Vec<Cell>>>, cursor_shape: CursorShape, cursor_visible: bool,+ /// Application cursor-keys mode (DECCKM): arrows send SS3 instead of CSI.+ app_cursor: bool, /// Cursor colour from OSC 12; `None` follows the cell under the cursor. cursor_color: Option<(u8, u8, u8)>, }@@ -154,6 +156,7 @@ impl Grid { cursor_shape: CursorShape::default(), cursor_visible: true, cursor_color: None,+ app_cursor: false, } } @@ -250,6 +253,14 @@ impl Grid { self.cursor_color } + pub fn set_app_cursor(&mut self, on: bool) {+ self.app_cursor = on;+ }++ pub fn app_cursor(&self) -> bool {+ self.app_cursor+ }+ // --- printing --- /// Place a printable character at the cursor, honouring width and autowrap.@@ -0,0 +1,186 @@+//! Keyboard encoding: translate decoded key events into the byte sequences a+//! terminal application expects (xterm/VT-style).++use smithay_client_toolkit::seat::keyboard::{KeyEvent, Keysym, Modifiers};++/// Encode a key press into bytes for the PTY, or `None` if it produces no input.+pub fn encode(event: &KeyEvent, mods: Modifiers, app_cursor: bool) -> Option<Vec<u8>> {+ let seq = match event.keysym {+ Keysym::Return | Keysym::KP_Enter => prefix_alt(b"\r".to_vec(), mods),+ Keysym::BackSpace => prefix_alt(b"\x7f".to_vec(), mods),+ Keysym::Tab if mods.shift => b"\x1b[Z".to_vec(),+ Keysym::Tab => b"\t".to_vec(),+ Keysym::Escape => b"\x1b".to_vec(),++ Keysym::Up => csi_letter(b'A', mods, app_cursor),+ Keysym::Down => csi_letter(b'B', mods, app_cursor),+ Keysym::Right => csi_letter(b'C', mods, app_cursor),+ Keysym::Left => csi_letter(b'D', mods, app_cursor),+ Keysym::Home => csi_letter(b'H', mods, app_cursor),+ Keysym::End => csi_letter(b'F', mods, app_cursor),++ Keysym::Insert => csi_tilde(2, mods),+ Keysym::Delete => csi_tilde(3, mods),+ Keysym::Page_Up => csi_tilde(5, mods),+ Keysym::Page_Down => csi_tilde(6, mods),++ // F1-F4 are SS3-introduced; F5+ use the CSI tilde forms.+ Keysym::F1 => fkey(b'P', mods),+ Keysym::F2 => fkey(b'Q', mods),+ Keysym::F3 => fkey(b'R', mods),+ Keysym::F4 => fkey(b'S', mods),+ Keysym::F5 => csi_tilde(15, mods),+ Keysym::F6 => csi_tilde(17, mods),+ Keysym::F7 => csi_tilde(18, mods),+ Keysym::F8 => csi_tilde(19, mods),+ Keysym::F9 => csi_tilde(20, mods),+ Keysym::F10 => csi_tilde(21, mods),+ Keysym::F11 => csi_tilde(23, mods),+ Keysym::F12 => csi_tilde(24, mods),++ // Everything else: the xkb-composed text (which already folds in Ctrl),+ // with Alt sending an ESC prefix (meta).+ _ => {+ let text = event.utf8.as_ref()?;+ if text.is_empty() {+ return None;+ }+ prefix_alt(text.as_bytes().to_vec(), mods)+ }+ };+ Some(seq)+}++fn prefix_alt(bytes: Vec<u8>, mods: Modifiers) -> Vec<u8> {+ if mods.alt {+ let mut out = Vec::with_capacity(bytes.len() + 1);+ out.push(0x1b);+ out.extend_from_slice(&bytes);+ out+ } else {+ bytes+ }+}++/// xterm modifier parameter: 1 + a bitfield of the held modifiers.+fn modifier_param(mods: Modifiers) -> u8 {+ 1 + u8::from(mods.shift)+ + (u8::from(mods.alt) << 1)+ + (u8::from(mods.ctrl) << 2)+ + (u8::from(mods.logo) << 3)+}++/// Cursor/edit keys: `ESC [ X` (or `ESC O X` in application-cursor mode), and+/// `ESC [ 1 ; m X` when modifiers are held.+fn csi_letter(final_byte: u8, mods: Modifiers, app_cursor: bool) -> Vec<u8> {+ let m = modifier_param(mods);+ if m == 1 {+ vec![0x1b, if app_cursor { b'O' } else { b'[' }, final_byte]+ } else {+ let mut v = format!("\x1b[1;{m}").into_bytes();+ v.push(final_byte);+ v+ }+}++/// Keypad-style keys: `ESC [ n ~`, with `ESC [ n ; m ~` when modifiers are held.+fn csi_tilde(n: u8, mods: Modifiers) -> Vec<u8> {+ let m = modifier_param(mods);+ if m == 1 {+ format!("\x1b[{n}~").into_bytes()+ } else {+ format!("\x1b[{n};{m}~").into_bytes()+ }+}++fn fkey(final_byte: u8, mods: Modifiers) -> Vec<u8> {+ let m = modifier_param(mods);+ if m == 1 {+ vec![0x1b, b'O', final_byte]+ } else {+ let mut v = format!("\x1b[1;{m}").into_bytes();+ v.push(final_byte);+ v+ }+}++#[cfg(test)]+mod tests {+ use super::*;++ fn key(keysym: Keysym, utf8: Option<&str>) -> KeyEvent {+ KeyEvent {+ time: 0,+ raw_code: 0,+ keysym,+ utf8: utf8.map(str::to_owned),+ }+ }++ const NONE: Modifiers = Modifiers {+ ctrl: false,+ alt: false,+ shift: false,+ caps_lock: false,+ logo: false,+ num_lock: false,+ };++ #[test]+ fn plain_text_passes_through() {+ assert_eq!(+ encode(&key(Keysym::a, Some("a")), NONE, false),+ Some(b"a".to_vec())+ );+ }++ #[test]+ fn alt_prefixes_escape() {+ let mods = Modifiers { alt: true, ..NONE };+ assert_eq!(+ encode(&key(Keysym::a, Some("a")), mods, false),+ Some(b"\x1ba".to_vec())+ );+ }++ #[test]+ fn arrows_respect_application_mode() {+ assert_eq!(+ encode(&key(Keysym::Up, None), NONE, false),+ Some(b"\x1b[A".to_vec())+ );+ assert_eq!(+ encode(&key(Keysym::Up, None), NONE, true),+ Some(b"\x1bOA".to_vec())+ );+ }++ #[test]+ fn modified_arrow_uses_csi_param() {+ let mods = Modifiers { ctrl: true, ..NONE };+ assert_eq!(+ encode(&key(Keysym::Right, None), mods, false),+ Some(b"\x1b[1;5C".to_vec())+ );+ }++ #[test]+ fn special_keys() {+ assert_eq!(+ encode(&key(Keysym::Return, None), NONE, false),+ Some(b"\r".to_vec())+ );+ assert_eq!(+ encode(&key(Keysym::BackSpace, None), NONE, false),+ Some(b"\x7f".to_vec())+ );+ assert_eq!(+ encode(&key(Keysym::Delete, None), NONE, false),+ Some(b"\x1b[3~".to_vec())+ );+ assert_eq!(+ encode(&key(Keysym::F5, None), NONE, false),+ Some(b"\x1b[15~".to_vec())+ );+ }+}@@ -2,6 +2,7 @@ mod font; mod grid;+mod input; mod pty; mod render; mod vt;@@ -133,6 +133,7 @@ impl Term { } } (false, 4) => self.grid.set_insert(on),+ (true, 1) => self.grid.set_app_cursor(on), (true, 25) => self.grid.set_cursor_visible(on), // App-cursor/bracketed-paste/mouse/sync modes affect input and // rendering, which arrive with the keyboard and renderer.@@ -19,12 +19,15 @@ use crate::render::Renderer; use crate::vt::Term; use smithay_client_toolkit::{ compositor::{CompositorHandler, CompositorState},- delegate_compositor, delegate_output, delegate_registry, delegate_seat, delegate_shm,- delegate_xdg_shell, delegate_xdg_window,+ delegate_compositor, delegate_keyboard, delegate_output, delegate_registry, delegate_seat,+ delegate_shm, delegate_xdg_shell, delegate_xdg_window, output::{OutputHandler, OutputState}, registry::{ProvidesRegistryState, RegistryState}, registry_handlers,- seat::{Capability, SeatHandler, SeatState},+ seat::{+ Capability, SeatHandler, SeatState,+ keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers, RepeatInfo},+ }, shell::{ WaylandSurface, xdg::{@@ -37,7 +40,7 @@ use smithay_client_toolkit::{ use wayland_client::{ Connection, QueueHandle, globals::registry_queue_init,- protocol::{wl_output, wl_seat, wl_shm, wl_surface},+ protocol::{wl_keyboard, wl_output, wl_seat, wl_shm, wl_surface}, }; /// Default window size in pixels before the compositor suggests one.@@ -87,6 +90,8 @@ pub fn run() -> anyhow::Result<ExitCode> { window, renderer, loop_handle: event_loop.handle(),+ keyboard: None,+ modifiers: Modifiers::default(), // The PTY is spawned on the first configure, once the real window size // is known, so the shell starts at the final size and is not hit by a // startup SIGWINCH storm that makes it reprint its prompt.@@ -151,6 +156,8 @@ struct App { window: Window, renderer: Renderer, loop_handle: LoopHandle<'static, App>,+ keyboard: Option<wl_keyboard::WlKeyboard>,+ modifiers: Modifiers, /// `None` until the first configure spawns the shell. session: Option<Session>, /// Last title applied to the toplevel, to avoid redundant requests.@@ -225,6 +232,20 @@ impl App { }); } + /// Encode a key event and write it to the shell.+ fn send_key(&self, event: &KeyEvent) {+ let app_cursor = self+ .session+ .as_ref()+ .is_some_and(|s| s.term.grid().app_cursor());+ if let Some(bytes) = crate::input::encode(event, self.modifiers, app_cursor)+ && let Some(session) = self.session.as_ref()+ && let Err(err) = write_all(session.pty.master(), &bytes)+ {+ tracing::warn!("write key to pty: {err}");+ }+ }+ /// After parsing child output: send any replies, sync the title, repaint. fn after_feed(&mut self) { let Some(session) = self.session.as_mut() else {@@ -404,10 +425,16 @@ impl SeatHandler for App { fn new_capability( &mut self, _: &Connection,- _: &QueueHandle<Self>,- _: wl_seat::WlSeat,- _: Capability,+ qh: &QueueHandle<Self>,+ seat: wl_seat::WlSeat,+ capability: Capability, ) {+ if capability == Capability::Keyboard && self.keyboard.is_none() {+ match self.seat_state.get_keyboard(qh, &seat, None) {+ Ok(keyboard) => self.keyboard = Some(keyboard),+ Err(err) => tracing::warn!("get keyboard: {err}"),+ }+ } } fn remove_capability(@@ -415,13 +442,100 @@ impl SeatHandler for App { _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat,- _: Capability,+ capability: Capability, ) {+ if capability == Capability::Keyboard+ && let Some(keyboard) = self.keyboard.take()+ {+ keyboard.release();+ } } fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {} } +impl KeyboardHandler for App {+ fn enter(+ &mut self,+ _: &Connection,+ _: &QueueHandle<Self>,+ _: &wl_keyboard::WlKeyboard,+ _: &wl_surface::WlSurface,+ _: u32,+ _: &[u32],+ _: &[Keysym],+ ) {+ self.focused = true;+ self.dirty = true;+ }++ fn leave(+ &mut self,+ _: &Connection,+ _: &QueueHandle<Self>,+ _: &wl_keyboard::WlKeyboard,+ _: &wl_surface::WlSurface,+ _: u32,+ ) {+ self.focused = false;+ self.dirty = true;+ }++ fn press_key(+ &mut self,+ _: &Connection,+ _: &QueueHandle<Self>,+ _: &wl_keyboard::WlKeyboard,+ _: u32,+ event: KeyEvent,+ ) {+ self.send_key(&event);+ }++ fn repeat_key(+ &mut self,+ _: &Connection,+ _: &QueueHandle<Self>,+ _: &wl_keyboard::WlKeyboard,+ _: u32,+ event: KeyEvent,+ ) {+ self.send_key(&event);+ }++ fn release_key(+ &mut self,+ _: &Connection,+ _: &QueueHandle<Self>,+ _: &wl_keyboard::WlKeyboard,+ _: u32,+ _: KeyEvent,+ ) {+ }++ fn update_modifiers(+ &mut self,+ _: &Connection,+ _: &QueueHandle<Self>,+ _: &wl_keyboard::WlKeyboard,+ _: u32,+ modifiers: Modifiers,+ _: RawModifiers,+ _: u32,+ ) {+ self.modifiers = modifiers;+ }++ fn update_repeat_info(+ &mut self,+ _: &Connection,+ _: &QueueHandle<Self>,+ _: &wl_keyboard::WlKeyboard,+ _: RepeatInfo,+ ) {+ }+}+ impl OutputHandler for App { fn output_state(&mut self) -> &mut OutputState { &mut self.output_state@@ -445,6 +559,7 @@ delegate_compositor!(App); delegate_output!(App); delegate_shm!(App); delegate_seat!(App);+delegate_keyboard!(App); delegate_xdg_shell!(App); delegate_xdg_window!(App); delegate_registry!(App);