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:
parent
9b893e474d
commit
7b38edb656
12 changed files with 866 additions and 0 deletions
|
|
@ -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"
|
||||
|
|
|
|||
13
build.rs
13
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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
10
src/evdev/mod.rs
Normal 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
190
src/evdev/touch.rs
Normal 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
203
src/evdev/uinput.rs
Normal 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
143
src/fbdev/framebuffer.rs
Normal 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
6
src/fbdev/linuxfb.rs
Normal 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
9
src/fbdev/mod.rs
Normal 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
8
src/linux/mod.rs
Normal 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
170
src/linux/tty.rs
Normal 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
80
src/ufkbd_evfb.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue