Skip to main content

max / shop

Keyboard input: Wayland key events -> PTY bytes SCTK 0.21 ships xkbcommon-backed keyboard handling with the default feature set, so we don't need a shop-xkb crate yet. App now implements KeyboardHandler and gets a WlKeyboard on the seat's Keyboard capability. Key translation: - Text keys use event.utf8 as-is; xkbcommon has already applied the keymap, so Ctrl+C arrives as "\x03" and non-Latin layouts do the right thing. - Non-printable keys mapped to xterm-style sequences: Enter=\r, Backspace=\x7f (xterm convention, not \x08), Tab, Escape, arrows, Home/End, PageUp/Down, Insert/Delete. Modifier state stored in App but not yet consulted for modifier+arrow combos or the kitty keyboard protocol — a follow-up.
Co-Authored-By
Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-23 19:52 UTC
Signed with PGP, not checked
Commit: 341f33e236f6638bb1ec1b2f52531bdbfba5ffcd
Parent: a0f2325
1 file changed, +137 insertions, -5 deletions
@@ -21,8 +21,13 @@
21 21 WaylandChrome, WaylandSurface, WindowConfigure, WindowDecorations, WindowHandler, WindowSpec,
22 22 XdgShell, XdgWindow, registry_queue_init,
23 23 };
24 - use smithay_client_toolkit::{delegate_dispatch2, delegate_registry, registry_handlers};
24 + use smithay_client_toolkit::{
25 + delegate_dispatch2, delegate_registry,
26 + registry_handlers,
27 + seat::keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers, RepeatInfo},
28 + };
25 29 use std::collections::VecDeque;
30 + use wayland_client::protocol::wl_keyboard;
26 31 use tracing::{info, warn};
27 32 use wayland_client::protocol::{wl_output, wl_seat, wl_surface};
28 33
@@ -154,6 +159,8 @@
154 159 parser,
155 160 font_data: std::fs::read(FONT_PATH)?,
156 161 scale: 1,
162 + keyboard: None,
163 + modifiers: Modifiers::default(),
157 164 pending_redraw: true,
158 165 };
159 166 app.surface.configure(&app.device, &app.surface_config);
@@ -270,6 +277,8 @@
270 277 font_data: Vec<u8>,
271 278 /// HiDPI integer scale from `wl_surface.enter` outputs.
272 279 scale: u32,
280 + keyboard: Option<wl_keyboard::WlKeyboard>,
281 + modifiers: Modifiers,
273 282 pending_redraw: bool,
274 283 }
275 284
@@ -417,22 +426,145 @@
417 426 fn new_capability(
418 427 &mut self,
419 428 _: &Connection,
420 - _: &QueueHandle<Self>,
421 - _: wl_seat::WlSeat,
422 - _: Capability,
429 + qh: &QueueHandle<Self>,
430 + seat: wl_seat::WlSeat,
431 + capability: Capability,
423 432 ) {
433 + if capability == Capability::Keyboard && self.keyboard.is_none() {
434 + match self.chrome.seat.get_keyboard(qh, &seat, None) {
435 + Ok(kb) => self.keyboard = Some(kb),
436 + Err(e) => warn!("get_keyboard: {e}"),
437 + }
438 + }
424 439 }
425 440 fn remove_capability(
426 441 &mut self,
427 442 _: &Connection,
428 443 _: &QueueHandle<Self>,
429 444 _: wl_seat::WlSeat,
430 - _: Capability,
445 + capability: Capability,
431 446 ) {
447 + if capability == Capability::Keyboard
448 + && let Some(kb) = self.keyboard.take()
449 + {
450 + kb.release();
451 + }
432 452 }
433 453 fn remove_seat(&mut self, _: &Connection, _: &QueueHandle<Self>, _: wl_seat::WlSeat) {}
434 454 }
435 455
456 + impl KeyboardHandler for App {
457 + fn enter(
458 + &mut self,
459 + _: &Connection,
460 + _: &QueueHandle<Self>,
461 + _: &wl_keyboard::WlKeyboard,
462 + _: &wl_surface::WlSurface,
463 + _: u32,
464 + _: &[u32],
465 + _: &[Keysym],
466 + ) {
467 + }
468 + fn leave(
469 + &mut self,
470 + _: &Connection,
471 + _: &QueueHandle<Self>,
472 + _: &wl_keyboard::WlKeyboard,
473 + _: &wl_surface::WlSurface,
474 + _: u32,
475 + ) {
476 + }
477 + fn press_key(
478 + &mut self,
479 + _: &Connection,
480 + _: &QueueHandle<Self>,
481 + _: &wl_keyboard::WlKeyboard,
482 + _: u32,
483 + event: KeyEvent,
484 + ) {
485 + self.handle_key(&event);
486 + }
487 + fn repeat_key(
488 + &mut self,
489 + _: &Connection,
490 + _: &QueueHandle<Self>,
491 + _: &wl_keyboard::WlKeyboard,
492 + _: u32,
493 + event: KeyEvent,
494 + ) {
495 + self.handle_key(&event);
496 + }
497 + fn release_key(
498 + &mut self,
499 + _: &Connection,
500 + _: &QueueHandle<Self>,
501 + _: &wl_keyboard::WlKeyboard,
502 + _: u32,
503 + _: KeyEvent,
504 + ) {
505 + }
506 + fn update_modifiers(
507 + &mut self,
508 + _: &Connection,
509 + _: &QueueHandle<Self>,
510 + _: &wl_keyboard::WlKeyboard,
511 + _: u32,
512 + modifiers: Modifiers,
513 + _: RawModifiers,
514 + _: u32,
515 + ) {
516 + self.modifiers = modifiers;
517 + }
518 + fn update_repeat_info(
519 + &mut self,
520 + _: &Connection,
521 + _: &QueueHandle<Self>,
522 + _: &wl_keyboard::WlKeyboard,
523 + _: RepeatInfo,
524 + ) {
525 + }
526 + }
527 +
528 + impl App {
529 + fn handle_key(&mut self, event: &KeyEvent) {
530 + let bytes = key_to_bytes(event);
531 + if !bytes.is_empty()
532 + && let Err(e) = self.pty.write(&bytes)
533 + {
534 + warn!("pty write: {e}");
535 + }
536 + }
537 + }
538 +
539 + /// Translate a Wayland key event into the byte sequence the shell expects.
540 + /// SCTK/xkbcommon already runs the keymap so `event.utf8` carries the printable
541 + /// bytes (including Ctrl-modified chars like Ctrl+C -> "\x03"). Special keys
542 + /// with no printable form get an xterm-style escape sequence.
543 + fn key_to_bytes(event: &KeyEvent) -> Vec<u8> {
544 + match event.keysym {
545 + Keysym::Return | Keysym::KP_Enter => return b"\r".to_vec(),
546 + Keysym::BackSpace => return b"\x7f".to_vec(),
547 + Keysym::Tab => return b"\t".to_vec(),
548 + Keysym::Escape => return b"\x1b".to_vec(),
549 + Keysym::Up => return b"\x1b[A".to_vec(),
550 + Keysym::Down => return b"\x1b[B".to_vec(),
551 + Keysym::Right => return b"\x1b[C".to_vec(),
552 + Keysym::Left => return b"\x1b[D".to_vec(),
553 + Keysym::Home => return b"\x1b[H".to_vec(),
554 + Keysym::End => return b"\x1b[F".to_vec(),
555 + Keysym::Page_Up => return b"\x1b[5~".to_vec(),
556 + Keysym::Page_Down => return b"\x1b[6~".to_vec(),
557 + Keysym::Insert => return b"\x1b[2~".to_vec(),
558 + Keysym::Delete => return b"\x1b[3~".to_vec(),
559 + _ => {}
560 + }
561 + event
562 + .utf8
563 + .as_ref()
564 + .map(|s| s.as_bytes().to_vec())
565 + .unwrap_or_default()
566 + }
567 +
436 568 impl WindowHandler for App {
437 569 fn request_close(&mut self, _: &Connection, _: &QueueHandle<Self>, _: &XdgWindow) {
438 570 self.chrome.exit = true;