forked from NotAShelf/beer
input: encode keyboard events and send them to the shell
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I6ee2acd5f74575f4bcc2f41417207c626a6a6964
This commit is contained in:
parent
0df5588f02
commit
b2d656e7bd
5 changed files with 322 additions and 8 deletions
11
src/grid.rs
11
src/grid.rs
|
|
@ -124,6 +124,8 @@ pub struct Grid {
|
||||||
alt_saved: Option<Vec<Vec<Cell>>>,
|
alt_saved: Option<Vec<Vec<Cell>>>,
|
||||||
cursor_shape: CursorShape,
|
cursor_shape: CursorShape,
|
||||||
cursor_visible: bool,
|
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 colour from OSC 12; `None` follows the cell under the cursor.
|
||||||
cursor_color: Option<(u8, u8, u8)>,
|
cursor_color: Option<(u8, u8, u8)>,
|
||||||
}
|
}
|
||||||
|
|
@ -154,6 +156,7 @@ impl Grid {
|
||||||
cursor_shape: CursorShape::default(),
|
cursor_shape: CursorShape::default(),
|
||||||
cursor_visible: true,
|
cursor_visible: true,
|
||||||
cursor_color: None,
|
cursor_color: None,
|
||||||
|
app_cursor: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -250,6 +253,14 @@ impl Grid {
|
||||||
self.cursor_color
|
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 ---
|
// --- printing ---
|
||||||
|
|
||||||
/// Place a printable character at the cursor, honouring width and autowrap.
|
/// Place a printable character at the cursor, honouring width and autowrap.
|
||||||
|
|
|
||||||
186
src/input.rs
Normal file
186
src/input.rs
Normal file
|
|
@ -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 font;
|
||||||
mod grid;
|
mod grid;
|
||||||
|
mod input;
|
||||||
mod pty;
|
mod pty;
|
||||||
mod render;
|
mod render;
|
||||||
mod vt;
|
mod vt;
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,7 @@ impl Term {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(false, 4) => self.grid.set_insert(on),
|
(false, 4) => self.grid.set_insert(on),
|
||||||
|
(true, 1) => self.grid.set_app_cursor(on),
|
||||||
(true, 25) => self.grid.set_cursor_visible(on),
|
(true, 25) => self.grid.set_cursor_visible(on),
|
||||||
// App-cursor/bracketed-paste/mouse/sync modes affect input and
|
// App-cursor/bracketed-paste/mouse/sync modes affect input and
|
||||||
// rendering, which arrive with the keyboard and renderer.
|
// rendering, which arrive with the keyboard and renderer.
|
||||||
|
|
|
||||||
131
src/wayland.rs
131
src/wayland.rs
|
|
@ -19,12 +19,15 @@ use crate::render::Renderer;
|
||||||
use crate::vt::Term;
|
use crate::vt::Term;
|
||||||
use smithay_client_toolkit::{
|
use smithay_client_toolkit::{
|
||||||
compositor::{CompositorHandler, CompositorState},
|
compositor::{CompositorHandler, CompositorState},
|
||||||
delegate_compositor, delegate_output, delegate_registry, delegate_seat, delegate_shm,
|
delegate_compositor, delegate_keyboard, delegate_output, delegate_registry, delegate_seat,
|
||||||
delegate_xdg_shell, delegate_xdg_window,
|
delegate_shm, delegate_xdg_shell, delegate_xdg_window,
|
||||||
output::{OutputHandler, OutputState},
|
output::{OutputHandler, OutputState},
|
||||||
registry::{ProvidesRegistryState, RegistryState},
|
registry::{ProvidesRegistryState, RegistryState},
|
||||||
registry_handlers,
|
registry_handlers,
|
||||||
seat::{Capability, SeatHandler, SeatState},
|
seat::{
|
||||||
|
Capability, SeatHandler, SeatState,
|
||||||
|
keyboard::{KeyEvent, KeyboardHandler, Keysym, Modifiers, RawModifiers, RepeatInfo},
|
||||||
|
},
|
||||||
shell::{
|
shell::{
|
||||||
WaylandSurface,
|
WaylandSurface,
|
||||||
xdg::{
|
xdg::{
|
||||||
|
|
@ -37,7 +40,7 @@ use smithay_client_toolkit::{
|
||||||
use wayland_client::{
|
use wayland_client::{
|
||||||
Connection, QueueHandle,
|
Connection, QueueHandle,
|
||||||
globals::registry_queue_init,
|
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.
|
/// Default window size in pixels before the compositor suggests one.
|
||||||
|
|
@ -87,6 +90,8 @@ pub fn run() -> anyhow::Result<ExitCode> {
|
||||||
window,
|
window,
|
||||||
renderer,
|
renderer,
|
||||||
loop_handle: event_loop.handle(),
|
loop_handle: event_loop.handle(),
|
||||||
|
keyboard: None,
|
||||||
|
modifiers: Modifiers::default(),
|
||||||
// The PTY is spawned on the first configure, once the real window size
|
// 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
|
// 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.
|
// startup SIGWINCH storm that makes it reprint its prompt.
|
||||||
|
|
@ -151,6 +156,8 @@ struct App {
|
||||||
window: Window,
|
window: Window,
|
||||||
renderer: Renderer,
|
renderer: Renderer,
|
||||||
loop_handle: LoopHandle<'static, App>,
|
loop_handle: LoopHandle<'static, App>,
|
||||||
|
keyboard: Option<wl_keyboard::WlKeyboard>,
|
||||||
|
modifiers: Modifiers,
|
||||||
/// `None` until the first configure spawns the shell.
|
/// `None` until the first configure spawns the shell.
|
||||||
session: Option<Session>,
|
session: Option<Session>,
|
||||||
/// Last title applied to the toplevel, to avoid redundant requests.
|
/// 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.
|
/// After parsing child output: send any replies, sync the title, repaint.
|
||||||
fn after_feed(&mut self) {
|
fn after_feed(&mut self) {
|
||||||
let Some(session) = self.session.as_mut() else {
|
let Some(session) = self.session.as_mut() else {
|
||||||
|
|
@ -404,10 +425,16 @@ impl SeatHandler for App {
|
||||||
fn new_capability(
|
fn new_capability(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &Connection,
|
_: &Connection,
|
||||||
_: &QueueHandle<Self>,
|
qh: &QueueHandle<Self>,
|
||||||
_: wl_seat::WlSeat,
|
seat: wl_seat::WlSeat,
|
||||||
_: Capability,
|
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(
|
fn remove_capability(
|
||||||
|
|
@ -415,13 +442,100 @@ impl SeatHandler for App {
|
||||||
_: &Connection,
|
_: &Connection,
|
||||||
_: &QueueHandle<Self>,
|
_: &QueueHandle<Self>,
|
||||||
_: wl_seat::WlSeat,
|
_: 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) {}
|
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 {
|
impl OutputHandler for App {
|
||||||
fn output_state(&mut self) -> &mut OutputState {
|
fn output_state(&mut self) -> &mut OutputState {
|
||||||
&mut self.output_state
|
&mut self.output_state
|
||||||
|
|
@ -445,6 +559,7 @@ delegate_compositor!(App);
|
||||||
delegate_output!(App);
|
delegate_output!(App);
|
||||||
delegate_shm!(App);
|
delegate_shm!(App);
|
||||||
delegate_seat!(App);
|
delegate_seat!(App);
|
||||||
|
delegate_keyboard!(App);
|
||||||
delegate_xdg_shell!(App);
|
delegate_xdg_shell!(App);
|
||||||
delegate_xdg_window!(App);
|
delegate_xdg_window!(App);
|
||||||
delegate_registry!(App);
|
delegate_registry!(App);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue