diff --git a/Cargo.toml b/Cargo.toml index f607ec0..723258d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ license = "GPL-3.0-only" [dependencies] ashpd = { version = "0.11", features = ["wayland"] } +evdev = "0.13" fontconfig = "0.9" # Disable freetype-sys, as it vendors libfreetype2 while fontconfig dynamically # links to it. Large dependencies should not be duplicated. @@ -13,10 +14,12 @@ freetype = { version = "0.7", default-features = false } futures-util = "0.3" imgref = "1" libc = "0.2" +linux-raw-sys = { version = "0.9", features = ["ioctl"] } memmap2 = "0.9" polling = "3" reis = "0.4" rgb = "0.8" +signal-hook = "0.3" tokio = { version = "1", features = ["macros", "rt"] } wayland-backend = "0.3" wayland-client = "0.31" @@ -30,6 +33,10 @@ zbus = { version = "5", default-features = false, features = ["tokio"] } [build-dependencies] bindgen = "0.71" +[[bin]] +name = "ufkbd-evfb" +path = "src/ufkbd_evfb.rs" + [[bin]] name = "ufkbd-gnome" path = "src/ufkbd_gnome.rs" diff --git a/build.rs b/build.rs index fa83d49..e89a3aa 100644 --- a/build.rs +++ b/build.rs @@ -22,4 +22,17 @@ fn main() println!("cargo::rustc-link-lib=expat"); bindings.write_to_file(out_file).expect("Writing failure"); + + let builder = bindgen::builder(); + + let bindings = builder.header("/usr/include/linux/fb.h") + .generate() + .expect("The Linux headers must be installed"); + + let out_dir: PathBuf = env::var("OUT_DIR") + .expect("Environment variable $OUT_DIR must be defined") + .into(); + let out_file = out_dir.join("linuxfb.rs"); + + bindings.write_to_file(out_file).expect("Writing failure"); } diff --git a/src/core/config.rs b/src/core/config.rs index f0b5561..2e04d17 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -60,6 +60,7 @@ pub struct Configuration { repeat: u64, layout: String, extra_keys: Vec, + evfb_height: f64, wayland_height: i32, wayland_im_enable: bool, @@ -67,6 +68,19 @@ pub struct Configuration { } impl Configuration { + fn load_evfb(&mut self, yaml: &Hash) + { + let height = yaml.get(&Yaml::String(String::from("height"))); + if let Some(height) = height { + let height = match height.as_f64() { + Some(h) => h.try_into().ok(), + None => None, + }; + let height = height.expect("Linux evdev-fbdev height should be a fraction from 0 to 1"); + self.evfb_height = height; + } + } + fn load_wayland(&mut self, yaml: &Hash) { let height = yaml.get(&Yaml::String(String::from("height"))); @@ -135,6 +149,7 @@ impl Configuration { extra_keys: vec![ String::from("alt"), String::from("meta") ], wayland_height: 185, wayland_im_enable: true, + evfb_height: 0.25, colors: KeyboardColors::default(), }; @@ -182,6 +197,12 @@ impl Configuration { cfg.load_wayland(wl); } + let evfb = yaml.get(&Yaml::String(String::from("evfb"))); + if let Some(evfb) = evfb { + let evfb = evfb.as_hash().expect("Linux evdev-fbdev configuration should be a YAML mapping"); + cfg.load_evfb(evfb); + } + let colors = yaml.get(&Yaml::String(String::from("colors"))); if let Some(colors) = colors { let colors = colors.as_hash().expect("Color configuration should be a YAML mapping"); @@ -228,6 +249,12 @@ impl Configuration { self.wayland_im_enable } + #[inline(always)] + pub fn evfb_height(&self) -> f64 + { + self.evfb_height + } + #[inline(always)] pub fn colors(&self) -> &KeyboardColors { diff --git a/src/evdev/mod.rs b/src/evdev/mod.rs new file mode 100644 index 0000000..2dd6335 --- /dev/null +++ b/src/evdev/mod.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Copyright (c) 2025, Richard Acayan. All rights reserved. + */ + +mod touch; +mod uinput; + +pub use self::touch::Touchscreen; +pub use self::uinput::VirtualKeyboard; diff --git a/src/evdev/touch.rs b/src/evdev/touch.rs new file mode 100644 index 0000000..c46eb34 --- /dev/null +++ b/src/evdev/touch.rs @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Copyright (c) 2024, Richard Acayan. All rights reserved. + */ + +use crate::core::Button; +use crate::core::Configuration; +use crate::core::Display; +use crate::core::Keyboard; +use evdev::AbsoluteAxisCode; +use evdev::Device; +use evdev::EventSummary; +use std::fs; +use std::io::Error; +use std::os::fd::AsFd; +use std::os::fd::AsRawFd; +use std::os::fd::BorrowedFd; +use std::os::fd::RawFd; + +#[derive(Debug)] +enum PressAction { + Pos, + Press, + Release, +} + +#[derive(Debug)] +struct TouchAction { + id: usize, + act: PressAction, +} + +pub struct Touchscreen { + dev: Device, + btn: Button, + actions: Vec, + pos: Vec<(f64, f64)>, + slot: usize, + y_off: f64, +} + +impl Touchscreen { + fn open_device_if_supported(dent: Result) -> Option + { + let path = dent.ok()?.path(); + let dev = Device::open(path).ok()?; + + let abs_props = dev.supported_absolute_axes()?; + + if abs_props.contains(AbsoluteAxisCode::ABS_MT_SLOT) + && abs_props.contains(AbsoluteAxisCode::ABS_MT_TRACKING_ID) + && abs_props.contains(AbsoluteAxisCode::ABS_MT_POSITION_X) + && abs_props.contains(AbsoluteAxisCode::ABS_MT_POSITION_Y) { + Some(dev) + } else { + None + } + } + + pub fn new(mut btn: Button, cfg: &Configuration) -> Result + { + let mut devs = fs::read_dir("/dev/input")?; + let dev = devs.find_map(Self::open_device_if_supported).unwrap(); + + dev.set_nonblocking(true)?; + + let props = dev.get_abs_state()?; + let max_slots = props[AbsoluteAxisCode::ABS_MT_SLOT.0 as usize].maximum as usize + 1; + + // Dimensions of the touchscreen device + let d_width = props[AbsoluteAxisCode::ABS_MT_POSITION_X.0 as usize].maximum; + let d_width = d_width as f64 + 1.0; + let d_height = props[AbsoluteAxisCode::ABS_MT_POSITION_Y.0 as usize].maximum; + let d_height = d_height as f64 + 1.0; + + // Dimensions of the keyboard + let c_height = d_height * cfg.evfb_height(); + + // Dimensions of the keyboard layout + let l_width = btn.layout().width(); + let l_height = btn.layout().height(); + + btn.set_scale(l_width / d_width, l_height / c_height); + + Ok(Self { + dev, btn, + actions: Vec::new(), + pos: vec![(0.0, 0.0); max_slots], + slot: 0, + y_off: d_height - c_height, + }) + } + + #[inline(always)] + pub fn button(&self) -> &Button + { + &self.btn + } + + pub fn dispatch_timers(&mut self) + { + self.btn.dispatch_timers() + } + + fn commit(&mut self) + { + for act in &self.actions { + match act.act { + PressAction::Pos => { + self.btn.pos(act.id, + self.pos[act.id].0, + self.pos[act.id].1 - self.y_off); + }, + PressAction::Press => { + if self.pos[act.id].1 >= self.y_off { + self.btn.press(act.id, + self.pos[act.id].0, + self.pos[act.id].1 - self.y_off); + } + }, + PressAction::Release => { + self.btn.release(act.id); + }, + } + } + + self.actions.clear(); + } + + pub fn handle_events(&mut self) + { + let evts: Vec<_> = self.dev.fetch_events().unwrap().collect(); + + for evt in evts { + match evt.destructure() { + EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_SLOT, val) => { + self.slot = val as usize; + }, + EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_POSITION_X, val) => { + self.pos[self.slot].0 = val as f64; + let action = TouchAction { + id: self.slot, + act: PressAction::Pos, + }; + self.actions.push(action); + }, + EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_POSITION_Y, val) => { + self.pos[self.slot].1 = val as f64; + let action = TouchAction { + id: self.slot, + act: PressAction::Pos, + }; + self.actions.push(action); + }, + EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_TRACKING_ID, -1) => { + let action = TouchAction { + id: self.slot, + act: PressAction::Release, + }; + self.actions.push(action); + }, + EventSummary::AbsoluteAxis(_, AbsoluteAxisCode::ABS_MT_TRACKING_ID, _) => { + let action = TouchAction { + id: self.slot, + act: PressAction::Press, + }; + self.actions.push(action); + }, + EventSummary::Synchronization(_, _, _) => { + self.commit(); + }, + _ => (), + } + } + } +} + +impl AsFd for Touchscreen { + fn as_fd(&self) -> BorrowedFd + { + self.dev.as_fd() + } +} + +impl AsRawFd for Touchscreen { + fn as_raw_fd(&self) -> RawFd + { + self.dev.as_raw_fd() + } +} diff --git a/src/evdev/uinput.rs b/src/evdev/uinput.rs new file mode 100644 index 0000000..a1b8878 --- /dev/null +++ b/src/evdev/uinput.rs @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Copyright (c) 2025, Richard Acayan. All rights reserved. + */ + +use crate::core::Keyboard; +use crate::core::Layout; +use crate::linux::Teletype; +use evdev::uinput::VirtualDevice; +use evdev::AttributeSet; +use evdev::EventType; +use evdev::InputEvent; +use evdev::KeyCode; +use std::collections::HashMap; +use std::io::Error; +use std::iter::FromIterator; +use xkeysym::Keysym; + +const STATIC_KEYCODES: [(Keysym, KeyCode); 77] = [ + (Keysym::space, KeyCode::KEY_SPACE), + (Keysym::apostrophe, KeyCode::KEY_APOSTROPHE), + (Keysym::comma, KeyCode::KEY_COMMA), + (Keysym::minus, KeyCode::KEY_MINUS), + (Keysym::period, KeyCode::KEY_DOT), + (Keysym::slash, KeyCode::KEY_SLASH), + (Keysym::_0, KeyCode::KEY_0), + (Keysym::_1, KeyCode::KEY_1), + (Keysym::_2, KeyCode::KEY_2), + (Keysym::_3, KeyCode::KEY_3), + (Keysym::_4, KeyCode::KEY_4), + (Keysym::_5, KeyCode::KEY_5), + (Keysym::_6, KeyCode::KEY_6), + (Keysym::_7, KeyCode::KEY_7), + (Keysym::_8, KeyCode::KEY_8), + (Keysym::_9, KeyCode::KEY_9), + (Keysym::semicolon, KeyCode::KEY_SEMICOLON), + (Keysym::equal, KeyCode::KEY_EQUAL), + (Keysym::bracketleft, KeyCode::KEY_LEFTBRACE), + (Keysym::backslash, KeyCode::KEY_BACKSLASH), + (Keysym::bracketright, KeyCode::KEY_RIGHTBRACE), + (Keysym::grave, KeyCode::KEY_GRAVE), + (Keysym::a, KeyCode::KEY_A), + (Keysym::b, KeyCode::KEY_B), + (Keysym::c, KeyCode::KEY_C), + (Keysym::d, KeyCode::KEY_D), + (Keysym::e, KeyCode::KEY_E), + (Keysym::f, KeyCode::KEY_F), + (Keysym::g, KeyCode::KEY_G), + (Keysym::h, KeyCode::KEY_H), + (Keysym::i, KeyCode::KEY_I), + (Keysym::j, KeyCode::KEY_J), + (Keysym::k, KeyCode::KEY_K), + (Keysym::l, KeyCode::KEY_L), + (Keysym::m, KeyCode::KEY_M), + (Keysym::n, KeyCode::KEY_N), + (Keysym::o, KeyCode::KEY_O), + (Keysym::p, KeyCode::KEY_P), + (Keysym::q, KeyCode::KEY_Q), + (Keysym::r, KeyCode::KEY_R), + (Keysym::s, KeyCode::KEY_S), + (Keysym::t, KeyCode::KEY_T), + (Keysym::u, KeyCode::KEY_U), + (Keysym::v, KeyCode::KEY_V), + (Keysym::w, KeyCode::KEY_W), + (Keysym::x, KeyCode::KEY_X), + (Keysym::y, KeyCode::KEY_Y), + (Keysym::z, KeyCode::KEY_Z), + (Keysym::BackSpace, KeyCode::KEY_BACKSPACE), + (Keysym::Tab, KeyCode::KEY_TAB), + (Keysym::Return, KeyCode::KEY_ENTER), + (Keysym::Escape, KeyCode::KEY_ESC), + (Keysym::Home, KeyCode::KEY_HOME), + (Keysym::Left, KeyCode::KEY_LEFT), + (Keysym::Up, KeyCode::KEY_UP), + (Keysym::Right, KeyCode::KEY_RIGHT), + (Keysym::Down, KeyCode::KEY_DOWN), + (Keysym::Prior, KeyCode::KEY_PAGEUP), + (Keysym::Next, KeyCode::KEY_PAGEDOWN), + (Keysym::End, KeyCode::KEY_END), + (Keysym::Insert, KeyCode::KEY_INSERT), + (Keysym::F1, KeyCode::KEY_F1), + (Keysym::F2, KeyCode::KEY_F2), + (Keysym::F3, KeyCode::KEY_F3), + (Keysym::F4, KeyCode::KEY_F4), + (Keysym::F5, KeyCode::KEY_F5), + (Keysym::F6, KeyCode::KEY_F6), + (Keysym::F7, KeyCode::KEY_F7), + (Keysym::F8, KeyCode::KEY_F8), + (Keysym::F9, KeyCode::KEY_F9), + (Keysym::F10, KeyCode::KEY_F10), + (Keysym::F11, KeyCode::KEY_F11), + (Keysym::F12, KeyCode::KEY_F12), + (Keysym::Shift_L, KeyCode::KEY_LEFTSHIFT), + (Keysym::Control_L, KeyCode::KEY_LEFTCTRL), + (Keysym::Alt_L, KeyCode::KEY_LEFTALT), + (Keysym::Delete, KeyCode::KEY_DELETE), +]; + +pub struct VirtualKeyboard { + dev: VirtualDevice, + tty: Teletype, + keycodes: HashMap, +} + +impl VirtualKeyboard { + pub fn new(tty: Teletype) -> Result + { + let keycodes = (1..=255).into_iter().map(|c| KeyCode(c)); + let keycodes = AttributeSet::from_iter(keycodes); + + let builder = VirtualDevice::builder()?; + let builder = builder.name("Unfettered Keyboard"); + let builder = builder.with_keys(&keycodes)?; + let dev = builder.build()?; + + Ok(Self { + dev, + tty, + keycodes: HashMap::with_capacity(256), + }) + } +} + +impl Keyboard for VirtualKeyboard { + fn key_supported(&self, sym: Keysym) -> bool + { + Teletype::keysym_value(sym).is_some() + } + + fn press(&mut self, sym: Keysym) + { + let code = *self.keycodes.get(&sym).unwrap(); + let evt = InputEvent::new_now(EventType::KEY.0, code as u16, 1); + self.dev.emit(&[evt]).unwrap(); + } + + fn release(&mut self, sym: Keysym) + { + let code = *self.keycodes.get(&sym).unwrap(); + let evt = InputEvent::new_now(EventType::KEY.0, code as u16, 0); + self.dev.emit(&[evt]).unwrap(); + } + + fn text(&mut self, text: &str) + { + self.tty.write_text(text); + self.press(Keysym::Shift_R); + self.release(Keysym::Shift_R); + } + + fn change_layout(&mut self, layout: &Layout) + { + let mut keycode = 1; + let mut used_codes = [false; 256]; + + self.keycodes.clear(); + for row in layout.rows() { + for key in row { + for part in &key.parts { + if !part.key_available() { + continue; + } + + if let Ok(idx) = STATIC_KEYCODES.binary_search_by_key(&part.sym(), |(s, _)| *s) { + let (sym, KeyCode(code)) = STATIC_KEYCODES[idx]; + self.keycodes.insert(sym, code as u8); + self.tty.write_key(part, code as u8); + used_codes[code as usize] = true; + } + } + } + } + + for row in layout.rows() { + for key in row { + for part in &key.parts { + if !part.key_available() { + continue; + } + + if self.keycodes.contains_key(&part.sym()) { + continue; + } + + while used_codes[keycode as usize] { + keycode += 1; + } + + self.keycodes.insert(part.sym(), keycode); + self.tty.write_key(part, keycode); + keycode += 1; + } + } + } + + while used_codes[keycode as usize] { + keycode += 1; + } + + self.keycodes.insert(Keysym::Shift_R, keycode); + self.tty.write_text_key(keycode); + } +} diff --git a/src/fbdev/framebuffer.rs b/src/fbdev/framebuffer.rs new file mode 100644 index 0000000..38a089d --- /dev/null +++ b/src/fbdev/framebuffer.rs @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Copyright (c) 2025, Richard Acayan. All rights reserved. + */ + +use crate::core::Configuration; +use crate::core::Display; +use crate::fbdev::linuxfb; +use imgref::ImgRefMut; +use rgb::FromSlice; +use rgb::alt::BGRA; +use std::fs::File; +use std::fs::OpenOptions; +use std::io::Error; +use std::io::Seek; +use std::io::SeekFrom; +use std::io::Write; +use std::os::fd::AsRawFd; + +pub struct Framebuffer { + f: File, + y: u32, + size: (u32, u32), + stride: u32, + img: Vec, +} + +impl Framebuffer { + pub fn new(cfg: &Configuration) -> Result + { + let mut open = OpenOptions::new(); + open.read(true); + open.write(true); + let f = open.open("/dev/fb0")?; + + let mut finfo = linuxfb::fb_fix_screeninfo { + id: [0; 16], + smem_start: 0, + smem_len: 0, + type_: 0, + type_aux: 0, + visual: 0, + xpanstep: 0, + ypanstep: 0, + ywrapstep: 0, + line_length: 0, + mmio_start: 0, + mmio_len: 0, + accel: 0, + capabilities: 0, + reserved: [0; 2], + }; + + unsafe { + libc::ioctl(f.as_raw_fd(), linuxfb::FBIOGET_FSCREENINFO as i32, &mut finfo as *mut _); + } + + let mut vinfo = linuxfb::fb_var_screeninfo { + xres: 0, + yres: 0, + xres_virtual: 0, + yres_virtual: 0, + xoffset: 0, + yoffset: 0, + bits_per_pixel: 0, + grayscale: 0, + red: linuxfb::fb_bitfield { offset: 0, length: 0, msb_right: 0 }, + green: linuxfb::fb_bitfield { offset: 0, length: 0, msb_right: 0 }, + blue: linuxfb::fb_bitfield { offset: 0, length: 0, msb_right: 0 }, + transp: linuxfb::fb_bitfield { offset: 0, length: 0, msb_right: 0 }, + nonstd: 0, + activate: 0, + height: 0, + width: 0, + accel_flags: 0, + pixclock: 0, + left_margin: 0, + right_margin: 0, + upper_margin: 0, + lower_margin: 0, + hsync_len: 0, + vsync_len: 0, + sync: 0, + vmode: 0, + rotate: 0, + colorspace: 0, + reserved: [0; 4], + }; + + unsafe { + libc::ioctl(f.as_raw_fd(), linuxfb::FBIOGET_VSCREENINFO as i32, &mut vinfo as *mut _); + } + + let height = (vinfo.yres_virtual as f64 * cfg.evfb_height()) as u32; + let y = vinfo.yres_virtual - height; + + let size = (vinfo.xres_virtual, height as u32); + + Ok(Self { + f, + y, + size, + stride: finfo.line_length, + img: vec![0; size.0 as usize * finfo.line_length as usize * 4], + }) + } +} + +impl Display for Framebuffer { + fn size(&self) -> (u32, u32) + { + self.size + } + + fn begin(&mut self) -> ImgRefMut> + { + // The ARGB format is in little endian. + let ptr = &mut self.img; + let ptr = ptr.as_bgra_mut(); + let img = ImgRefMut::new_stride(ptr, + self.size.0 as usize, + self.size.1 as usize, + (self.stride / 4) as usize); + + img + } + + fn resize(&mut self, _width: u32, _height: u32) + { + // Nothing to do here, we resize once on startup and everything is working. + } + + fn end(&mut self, _x: u32, y: u32, _width: u32, height: u32) + { + let kbd_top = self.y as u64 * self.stride as u64; + let target_top = y as usize * self.stride as usize; + let target_bottom = (y as usize + height as usize) * self.stride as usize; + let subimg = &self.img[target_top..target_bottom]; + + self.f.seek(SeekFrom::Start(kbd_top + target_top as u64)).unwrap(); + self.f.write(subimg).unwrap(); + } +} diff --git a/src/fbdev/linuxfb.rs b/src/fbdev/linuxfb.rs new file mode 100644 index 0000000..a46ff4a --- /dev/null +++ b/src/fbdev/linuxfb.rs @@ -0,0 +1,6 @@ +#![allow(dead_code, + improper_ctypes, + non_camel_case_types, + non_snake_case, + non_upper_case_globals)] +include!(concat!(env!("OUT_DIR"), "/linuxfb.rs")); diff --git a/src/fbdev/mod.rs b/src/fbdev/mod.rs new file mode 100644 index 0000000..8c4e54d --- /dev/null +++ b/src/fbdev/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Copyright (c) 2025, Richard Acayan. All rights reserved. + */ + +mod framebuffer; +mod linuxfb; + +pub use self::framebuffer::Framebuffer; diff --git a/src/linux/mod.rs b/src/linux/mod.rs new file mode 100644 index 0000000..1babfb3 --- /dev/null +++ b/src/linux/mod.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Copyright (c) 2025, Richard Acayan. All rights reserved. + */ + +mod tty; + +pub use self::tty::Teletype; diff --git a/src/linux/tty.rs b/src/linux/tty.rs new file mode 100644 index 0000000..402c61c --- /dev/null +++ b/src/linux/tty.rs @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Copyright (c) 2025, Richard Acayan. All rights reserved. + */ + +use crate::core::Configuration; +use crate::core::Part; +use linux_raw_sys::ioctl::KDSKBENT; +use linux_raw_sys::ioctl::KDSKBSENT; +use linux_raw_sys::ioctl::TIOCGWINSZ; +use linux_raw_sys::ioctl::TIOCSWINSZ; +use std::fs::File; +use std::io::Error; +use std::os::fd::AsRawFd; +use xkeysym::Keysym; + +#[repr(C)] +struct KbEntry { + table: u8, + index: u8, + value: u16, +} + +#[repr(C)] +struct KbSEntry { + func: u8, + string: [u8; 512], +} + +#[repr(C)] +#[derive(Clone)] +struct WinSize { + row: u16, + col: u16, + xpixel: u16, + ypixel: u16, +} + +pub struct Teletype { + f: File, + orig_ws: WinSize, +} + +const KT_LATIN: u8 = 0; +const KT_FN: u8 = 1; +const KT_SPEC: u8 = 2; +const KT_CUR: u8 = 6; +const KT_SHIFT: u8 = 6; + +fn key(kt: u8, kv: u8) -> u16 +{ + ((kt as u16) << 8) | (kv as u16) +} + +impl Teletype { + pub fn keysym_value(sym: Keysym) -> Option + { + if sym.raw() >= 0x20 && sym.raw() <= 0x7e { + return Some(sym.raw() as u16); + } + + match sym { + Keysym::BackSpace => Some(key(KT_LATIN, 0x7f)), + Keysym::Tab => Some(key(KT_LATIN, 0x09)), + Keysym::Return => Some(key(KT_SPEC, 0x01)), + Keysym::Escape => Some(key(KT_LATIN, 0x1b)), + Keysym::Home => Some(key(KT_FN, 0x14)), + Keysym::Left => Some(key(KT_CUR, 0x01)), + Keysym::Up => Some(key(KT_CUR, 0x03)), + Keysym::Right => Some(key(KT_CUR, 0x02)), + Keysym::Down => Some(key(KT_CUR, 0x00)), + Keysym::Prior => Some(key(KT_FN, 0x18)), + Keysym::Next => Some(key(KT_FN, 0x19)), + Keysym::End => Some(key(KT_FN, 0x18)), + Keysym::Insert => Some(key(KT_FN, 0x15)), + Keysym::F1 => Some(key(KT_FN, 0x00)), + Keysym::F2 => Some(key(KT_FN, 0x01)), + Keysym::F3 => Some(key(KT_FN, 0x02)), + Keysym::F4 => Some(key(KT_FN, 0x03)), + Keysym::F5 => Some(key(KT_FN, 0x04)), + Keysym::F6 => Some(key(KT_FN, 0x05)), + Keysym::F7 => Some(key(KT_FN, 0x06)), + Keysym::F8 => Some(key(KT_FN, 0x07)), + Keysym::F9 => Some(key(KT_FN, 0x08)), + Keysym::F10 => Some(key(KT_FN, 0x09)), + Keysym::F11 => Some(key(KT_FN, 0x0a)), + Keysym::F12 => Some(key(KT_FN, 0x0b)), + Keysym::Shift_L => Some(key(KT_SHIFT, 0x00)), + Keysym::Control_L => Some(key(KT_SHIFT, 0x02)), + Keysym::Alt_L => Some(key(KT_SHIFT, 0x03)), + Keysym::Delete => Some(key(KT_FN, 0x16)), + _ => None, + } + } + + pub fn new(cfg: &Configuration) -> Result + { + let f = File::open("/dev/tty0")?; + + let mut ws = WinSize { row: 0, col: 0, xpixel: 0, ypixel: 0 }; + unsafe { + libc::ioctl(f.as_raw_fd(), TIOCGWINSZ as i32, &mut ws as *mut _); + } + + let orig_ws = ws.clone(); + ws.row = (ws.row as f64 - cfg.evfb_height() * ws.row as f64) as u16; + + unsafe { + libc::ioctl(f.as_raw_fd(), TIOCSWINSZ as i32, &ws as *const _); + } + + Ok(Self { f, orig_ws }) + } + + pub fn write_key(&self, part: &Part, keycode: u8) + { + let lower = part.sym(); + let upper = Part::modify_shift(lower); + + let val = Self::keysym_value(lower).unwrap(); + let ent = KbEntry { table: 0, index: keycode, value: val }; + unsafe { + libc::ioctl(self.f.as_raw_fd(), KDSKBENT as i32, &ent as *const _); + } + + let val = Self::keysym_value(upper).unwrap(); + let ent = KbEntry { table: 1, index: keycode, value: val }; + unsafe { + libc::ioctl(self.f.as_raw_fd(), KDSKBENT as i32, &ent as *const _); + } + } + + pub fn write_text_key(&self, keycode: u8) + { + let ent = KbEntry { table: 0, index: keycode, value: 0x1fe }; + unsafe { + libc::ioctl(self.f.as_raw_fd(), KDSKBENT as i32, &ent as *const _); + } + + let ent = KbEntry { table: 1, index: keycode, value: 0x1fe }; + unsafe { + libc::ioctl(self.f.as_raw_fd(), KDSKBENT as i32, &ent as *const _); + } + } + + pub fn write_text(&self, text: &str) + { + let text = text.as_bytes(); + + let mut ent = KbSEntry { func: 254, string: [0; 512] }; + let dest = &mut ent.string[..text.len()]; + dest.copy_from_slice(text); + ent.string[511] = 0; + + unsafe { + libc::ioctl(self.f.as_raw_fd(), KDSKBSENT as i32, &ent as *const _); + } + } +} + +impl Drop for Teletype { + fn drop(&mut self) + { + unsafe { + libc::ioctl(self.f.as_raw_fd(), + TIOCSWINSZ as i32, + &self.orig_ws as *const _); + } + } +} diff --git a/src/ufkbd_evfb.rs b/src/ufkbd_evfb.rs new file mode 100644 index 0000000..7eed2b5 --- /dev/null +++ b/src/ufkbd_evfb.rs @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Copyright (c) 2025, Richard Acayan. All rights reserved. + */ + +mod core; +mod evdev; +mod fbdev; +mod linux; + +use crate::core::Button; +use crate::core::Configuration; +use crate::core::Display; +use crate::core::Graphics; +use crate::core::Layout; +use crate::evdev::Touchscreen; +use crate::evdev::VirtualKeyboard; +use crate::fbdev::Framebuffer; +use crate::linux::Teletype; +use polling::Event; +use polling::Events; +use polling::Poller; +use signal_hook::consts::signal; +use signal_hook::iterator::Signals; +use std::sync::Arc; +use std::sync::Mutex; +use std::time::Instant; + +fn main() +{ + let cfg = Configuration::load().unwrap(); + let disp = Framebuffer::new(&cfg).unwrap(); + + let gfx = Graphics::new(disp, &cfg); + let gfx = Mutex::new(gfx); + let gfx = Arc::new(gfx); + + let tty = Teletype::new(&cfg).unwrap(); + let kbd = VirtualKeyboard::new(tty).unwrap(); + let layout = Layout::load(&cfg).unwrap(); + let mut btn = Button::new(&cfg, layout, kbd, gfx.clone()); + btn.set_text_supported(true); + + { + let mut gfx = gfx.lock().unwrap(); + let (width, height) = gfx.display().size(); + gfx.resize(btn.layout(), btn.mod_state(), width, height); + } + + let mut touchscreen = Touchscreen::new(btn, &cfg).unwrap(); + + let mut events = Events::new(); + let poller = Poller::new().unwrap(); + let ts_evt = Event::readable(0); + + let mut sigs = Signals::new([ + signal::SIGHUP, + signal::SIGINT, + signal::SIGPIPE, + signal::SIGTERM, + ]).unwrap(); + + while sigs.pending().next().is_none() { + let timer = touchscreen.button().next_time().map(|t| t - Instant::now()); + + unsafe { + poller.add(&touchscreen, ts_evt).unwrap(); + } + + events.clear(); + poller.wait(&mut events, timer).unwrap(); + poller.delete(&touchscreen).unwrap(); + + if !events.is_empty() { + touchscreen.handle_events(); + } + + touchscreen.dispatch_timers(); + } +}