add evfb (evdev + fbdev) backend

Outside of a terminal emulator or web browser, this typing experience
can be useful outside the DE, such as in the TTY or when entering a
password for full-disk encryption. Add a target that uses the bare
hardware to allow the keyboard to be deployed in more flexible
environments.
This commit is contained in:
Richard Acayan 2025-07-21 19:01:23 -04:00
parent 9b893e474d
commit 7b38edb656
12 changed files with 866 additions and 0 deletions

View file

@ -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"

View file

@ -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");
}

View file

@ -60,6 +60,7 @@ pub struct Configuration {
repeat: u64,
layout: String,
extra_keys: Vec<String>,
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
{

10
src/evdev/mod.rs Normal file
View file

@ -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;

190
src/evdev/touch.rs Normal file
View file

@ -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<D: Display, K: Keyboard> {
dev: Device,
btn: Button<D, K>,
actions: Vec<TouchAction>,
pos: Vec<(f64, f64)>,
slot: usize,
y_off: f64,
}
impl<D: Display, K: Keyboard> Touchscreen<D, K> {
fn open_device_if_supported(dent: Result<fs::DirEntry, Error>) -> Option<Device>
{
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<D, K>, cfg: &Configuration) -> Result<Self, Error>
{
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<D, K>
{
&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<D: Display, K: Keyboard> AsFd for Touchscreen<D, K> {
fn as_fd(&self) -> BorrowedFd
{
self.dev.as_fd()
}
}
impl<D: Display, K: Keyboard> AsRawFd for Touchscreen<D, K> {
fn as_raw_fd(&self) -> RawFd
{
self.dev.as_raw_fd()
}
}

203
src/evdev/uinput.rs Normal file
View file

@ -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<Keysym, u8>,
}
impl VirtualKeyboard {
pub fn new(tty: Teletype) -> Result<Self, Error>
{
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);
}
}

143
src/fbdev/framebuffer.rs Normal file
View file

@ -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<u8>,
}
impl Framebuffer {
pub fn new(cfg: &Configuration) -> Result<Self, Error>
{
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<BGRA<u8>>
{
// 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();
}
}

6
src/fbdev/linuxfb.rs Normal file
View file

@ -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"));

9
src/fbdev/mod.rs Normal file
View file

@ -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;

8
src/linux/mod.rs Normal file
View file

@ -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;

170
src/linux/tty.rs Normal file
View file

@ -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<u16>
{
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<Self, Error>
{
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 _);
}
}
}

80
src/ufkbd_evfb.rs Normal file
View file

@ -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();
}
}