1
0
Fork 0
mirror of https://github.com/Zedfrigg/ironbar.git synced 2025-08-17 14:51:04 +02:00

Merge pull request #846 from JakeStanger/refactor/keys/colpetto

refactor(keys): switch to `colpetto`
This commit is contained in:
Jake Stanger 2025-04-04 20:06:12 +01:00 committed by GitHub
commit 28a0e0fb99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1172 additions and 1016 deletions

1869
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -39,11 +39,11 @@ ipc = ["dep:serde_json"]
http = ["dep:reqwest"] http = ["dep:reqwest"]
"config+all" = [ "config+all" = [
"config+json", "config+json",
"config+yaml", "config+yaml",
"config+toml", "config+toml",
"config+corn", "config+corn",
"config+ron", "config+ron",
] ]
"config+json" = ["universal-config/json"] "config+json" = ["universal-config/json"]
"config+yaml" = ["universal-config/yaml"] "config+yaml" = ["universal-config/yaml"]
@ -53,7 +53,7 @@ http = ["dep:reqwest"]
cairo = ["lua-src", "mlua", "cairo-rs"] cairo = ["lua-src", "mlua", "cairo-rs"]
clipboard = ["dep:nix"] clipboard = ["dep:rustix"]
clock = ["chrono"] clock = ["chrono"]
@ -61,7 +61,7 @@ custom = []
focused = [] focused = []
keyboard = ["dep:input", "dep:evdev-rs", "dep:libc", "dep:nix"] keyboard = ["dep:colpetto", "dep:evdev-rs", "dep:rustix"]
"keyboard+all" = ["keyboard", "keyboard+sway", "keyboard+hyprland"] "keyboard+all" = ["keyboard", "keyboard+sway", "keyboard+hyprland"]
"keyboard+sway" = ["keyboard", "sway"] "keyboard+sway" = ["keyboard", "sway"]
"keyboard+hyprland" = ["keyboard", "hyprland"] "keyboard+hyprland" = ["keyboard", "hyprland"]
@ -105,17 +105,17 @@ gtk = "0.18.2"
gtk-layer-shell = "0.8.2" gtk-layer-shell = "0.8.2"
glib = "0.18.5" glib = "0.18.5"
tokio = { version = "1.44.1", features = [ tokio = { version = "1.44.1", features = [
"macros", "macros",
"rt-multi-thread", "rt-multi-thread",
"time", "time",
"process", "process",
"sync", "sync",
"io-util", "io-util",
"net", "net",
] } ] }
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
tracing-error = { version = "0.2.1" , default-features = false } tracing-error = { version = "0.2.1", default-features = false }
tracing-appender = "0.2.3" tracing-appender = "0.2.3"
strip-ansi-escapes = "0.2.0" strip-ansi-escapes = "0.2.0"
color-eyre = "0.6.3" color-eyre = "0.6.3"
@ -127,7 +127,7 @@ notify = { version = "8.0.0", default-features = false }
wayland-client = "0.31.1" wayland-client = "0.31.1"
wayland-protocols-wlr = { version = "0.2.0", features = ["client"] } wayland-protocols-wlr = { version = "0.2.0", features = ["client"] }
smithay-client-toolkit = { version = "0.18.1", default-features = false, features = [ smithay-client-toolkit = { version = "0.18.1", default-features = false, features = [
"calloop", "calloop",
] } ] }
universal-config = { version = "0.5.0", default-features = false } universal-config = { version = "0.5.0", default-features = false }
ctrlc = "3.4.5" ctrlc = "3.4.5"
@ -143,7 +143,7 @@ serde_json = { version = "1.0.140", optional = true }
reqwest = { version = "0.12.15", default-features = false, features = ["default-tls", "http2"], optional = true } reqwest = { version = "0.12.15", default-features = false, features = ["default-tls", "http2"], optional = true }
# cairo # cairo
lua-src = { version = "547.0.0", optional = true } lua-src = { version = "547.0.0", optional = true }
mlua = { version = "0.9.9", optional = true, features = ["luajit"] } mlua = { version = "0.9.9", optional = true, features = ["luajit"] }
cairo-rs = { version = "0.18.5", optional = true, features = ["png"] } cairo-rs = { version = "0.18.5", optional = true, features = ["png"] }
@ -151,9 +151,8 @@ cairo-rs = { version = "0.18.5", optional = true, features = ["png"] }
chrono = { version = "0.4.40", optional = true, default-features = false, features = ["clock", "unstable-locales"] } chrono = { version = "0.4.40", optional = true, default-features = false, features = ["clock", "unstable-locales"] }
# keyboard # keyboard
input = { version = "0.9.1", optional = true } colpetto = { version = "0.6.0", features = ["tokio", "tracing"], optional = true }
evdev-rs = { version = "0.6.1", optional = true } evdev-rs = { version = "0.6.1", optional = true }
libc = { version = "0.2.171", optional = true }
# music # music
mpd-utils = { version = "0.2.1", optional = true } mpd-utils = { version = "0.2.1", optional = true }
@ -176,10 +175,10 @@ libpulse-binding = { version = "2.29.0", optional = true }
# shared # shared
futures-lite = { version = "2.6.0", optional = true } # network_manager, upower, workspaces futures-lite = { version = "2.6.0", optional = true } # network_manager, upower, workspaces
nix = { version = "0.29.0", optional = true, features = ["event", "fs", "poll"] } # clipboard, input
zbus = { version = "5.5.0", default-features = false, features = ["tokio"], optional = true } # network_manager, notifications, upower zbus = { version = "5.5.0", default-features = false, features = ["tokio"], optional = true } # network_manager, notifications, upower
swayipc-async = { version = "2.0.4", optional = true } # workspaces, keyboard swayipc-async = { version = "2.0.4", optional = true } # workspaces, keyboard
hyprland = { version = "0.4.0-alpha.3", features = ["silent"], optional = true } # workspaces, keyboard hyprland = { version = "0.4.0-alpha.3", features = ["silent"], optional = true } # workspaces, keyboard
rustix = { version = "1.0.5", default-features = false, features = ["std", "fs", "pipe", "event"], optional = true } # clipboard, input
# schema # schema
schemars = { version = "0.8.22", optional = true } schemars = { version = "0.8.22", optional = true }

View file

@ -1,17 +1,20 @@
use crate::{arc_rw, read_lock, send, spawn, spawn_blocking, write_lock}; use crate::{Ironbar, arc_rw, read_lock, send, spawn, write_lock};
use color_eyre::{Report, Result}; use color_eyre::{Report, Result};
use colpetto::event::{AsRawEvent, DeviceEvent, KeyState, KeyboardEvent};
use colpetto::{DeviceCapability, Libinput};
use evdev_rs::DeviceWrapper; use evdev_rs::DeviceWrapper;
use evdev_rs::enums::{EV_KEY, EV_LED, EventCode, int_to_ev_key}; use evdev_rs::enums::{EV_KEY, EV_LED, EventCode, int_to_ev_key};
use input::event::keyboard::{KeyState, KeyboardEventTrait}; use futures_lite::StreamExt;
use input::event::{DeviceEvent, EventTrait, KeyboardEvent}; use rustix::fs::{Mode, OFlags, open};
use input::{DeviceCapability, Libinput, LibinputInterface}; use rustix::io::Errno;
use libc::{O_ACCMODE, O_RDONLY, O_RDWR}; use std::ffi::{CStr, CString, c_int};
use std::fs::{File, OpenOptions}; use std::os::fd::{FromRawFd, IntoRawFd, RawFd};
use std::os::unix::{fs::OpenOptionsExt, io::OwnedFd}; use std::os::unix::io::OwnedFd;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::time::Duration; use std::time::Duration;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio::task::LocalSet;
use tokio::time::sleep; use tokio::time::sleep;
use tracing::{debug, error}; use tracing::{debug, error};
@ -88,28 +91,6 @@ impl<P: AsRef<Path>> TryFrom<KeyData<P>> for Event {
} }
} }
pub struct Interface;
impl LibinputInterface for Interface {
fn open_restricted(&mut self, path: &Path, flags: i32) -> Result<OwnedFd, i32> {
// No idea what these flags do honestly, just copied them from the example.
let op = OpenOptions::new()
.custom_flags(flags)
.read((flags & O_ACCMODE == O_RDONLY) | (flags & O_ACCMODE == O_RDWR))
.open(path)
.map(OwnedFd::from);
if let Err(err) = &op {
error!("error opening {}: {err:?}", path.display());
}
op.map_err(|err| err.raw_os_error().unwrap_or(-1))
}
fn close_restricted(&mut self, fd: OwnedFd) {
drop(File::from(fd));
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct Client { pub struct Client {
tx: broadcast::Sender<Event>, tx: broadcast::Sender<Event>,
@ -122,14 +103,23 @@ pub struct Client {
impl Client { impl Client {
pub fn init(seat: String) -> Arc<Self> { pub fn init(seat: String) -> Arc<Self> {
let client = Arc::new(Self::new(seat)); let client = Arc::new(Self::new(seat));
{ {
let client = client.clone(); let client = client.clone();
spawn_blocking(move || {
if let Err(err) = client.run() { std::thread::spawn(move || {
error!("{err:?}"); let local = LocalSet::new();
}
local.spawn_local(async move {
if let Err(err) = client.run().await {
error!("{err:?}");
}
});
Ironbar::runtime().block_on(local);
}); });
} }
client client
} }
@ -144,83 +134,90 @@ impl Client {
} }
} }
fn run(&self) -> Result<()> { fn open_restricted(path: &CStr, flags: c_int) -> std::result::Result<RawFd, i32> {
let mut input = Libinput::new_with_udev(Interface); open(path, OFlags::from_bits_retain(flags as u32), Mode::empty())
input .map(IntoRawFd::into_raw_fd)
.udev_assign_seat(&self.seat) .map_err(Errno::raw_os_error)
.map_err(|()| Report::msg("failed to assign seat"))?; }
loop { fn close_restricted(fd: c_int) {
input.dispatch()?; drop(unsafe { OwnedFd::from_raw_fd(fd) });
}
for event in &mut input { async fn run(&self) -> Result<()> {
match event { let mut libinput = Libinput::with_tracing(Self::open_restricted, Self::close_restricted)?;
input::Event::Keyboard(KeyboardEvent::Key(event))
if event.key_state() == KeyState::Released =>
{
let Some(device) = (unsafe { event.device().udev_device() }) else {
continue;
};
let Some( libinput.udev_assign_seat(CString::new(&*self.seat)?.as_c_str())?;
key @ (EV_KEY::KEY_CAPSLOCK
| EV_KEY::KEY_NUMLOCK
| EV_KEY::KEY_SCROLLLOCK),
) = int_to_ev_key(event.key())
else {
continue;
};
if let Some(device_path) = device.devnode().map(PathBuf::from) { let mut stream = libinput.event_stream()?;
let tx = self.tx.clone(); while let Some(event) = stream.try_next().await? {
match event {
colpetto::Event::Device(DeviceEvent::Added(event)) => {
let device = event.device();
if !device.has_capability(DeviceCapability::Keyboard) {
continue;
}
// need to spawn a task to avoid blocking let name = device.name();
spawn(async move { let Some(device) = event.device().udev_device() else {
// wait for kb to change continue;
sleep(Duration::from_millis(50)).await; };
let data = KeyData { device_path, key }; if let Some(device_path) = device.devnode() {
// not all devices which report as keyboards actually are one -
// fire test event so we can figure out if it is
let caps_event: Result<Event> = KeyData {
device_path,
key: EV_KEY::KEY_CAPSLOCK,
}
.try_into();
if let Ok(event) = data.try_into() { if caps_event.is_ok() {
send!(tx, event); debug!(
} "new keyboard device: {} | {}",
}); name.to_string_lossy(),
device_path.display()
);
write_lock!(self.known_devices).push(device_path.to_path_buf());
send!(self.tx, Event::Device);
} }
} }
input::Event::Device(DeviceEvent::Added(event)) => {
let device = event.device();
if !device.has_capability(DeviceCapability::Keyboard) {
continue;
}
let name = device.name();
let Some(device) = (unsafe { event.device().udev_device() }) else {
continue;
};
if let Some(device_path) = device.devnode() {
// not all devices which report as keyboards actually are one -
// fire test event so we can figure out if it is
let caps_event: Result<Event> = KeyData {
device_path,
key: EV_KEY::KEY_CAPSLOCK,
}
.try_into();
if caps_event.is_ok() {
debug!("new keyboard device: {name} | {}", device_path.display());
write_lock!(self.known_devices).push(device_path.to_path_buf());
send!(self.tx, Event::Device);
}
}
}
_ => {}
} }
} colpetto::Event::Keyboard(KeyboardEvent::Key(event))
if event.key_state() == KeyState::Released =>
{
let Some(device) = event.device().udev_device() else {
continue;
};
// we need to sleep for a short period to avoid hogging cpu let Some(
std::thread::sleep(Duration::from_millis(20)); key @ (EV_KEY::KEY_CAPSLOCK | EV_KEY::KEY_NUMLOCK | EV_KEY::KEY_SCROLLLOCK),
) = int_to_ev_key(event.key())
else {
continue;
};
if let Some(device_path) = device.devnode().map(PathBuf::from) {
let tx = self.tx.clone();
// need to spawn a task to avoid blocking
spawn(async move {
// wait for kb to change
sleep(Duration::from_millis(50)).await;
let data = KeyData { device_path, key };
if let Ok(event) = data.try_into() {
send!(tx, event);
}
});
}
}
_ => {}
}
} }
Err(Report::msg("unexpected end of stream"))
} }
pub fn get_state(&self, key: Key) -> bool { pub fn get_state(&self, key: Key) -> bool {

View file

@ -148,10 +148,13 @@ impl Clients {
#[cfg(feature = "keyboard")] #[cfg(feature = "keyboard")]
pub fn libinput(&mut self, seat: &str) -> Arc<libinput::Client> { pub fn libinput(&mut self, seat: &str) -> Arc<libinput::Client> {
self.libinput if let Some(client) = self.libinput.get(seat) {
.entry(seat.into()) client.clone()
.or_insert_with(|| libinput::Client::init(seat.to_string())) } else {
.clone() let client = libinput::Client::init(seat.to_string());
self.libinput.insert(seat.into(), client.clone());
client
}
} }
#[cfg(feature = "music")] #[cfg(feature = "music")]

View file

@ -8,18 +8,22 @@ use self::offer::{DataControlDeviceOffer, DataControlOfferHandler};
use self::source::DataControlSourceHandler; use self::source::DataControlSourceHandler;
use super::{Client, Environment, Event, Request, Response}; use super::{Client, Environment, Event, Request, Response};
use crate::{Ironbar, lock, spawn, try_send}; use crate::{Ironbar, lock, spawn, try_send};
use color_eyre::Result;
use device::DataControlDevice; use device::DataControlDevice;
use glib::Bytes; use glib::Bytes;
use nix::fcntl::{F_GETPIPE_SZ, F_SETPIPE_SZ, fcntl}; use rustix::buffer::spare_capacity;
use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags, EpollTimeout}; use rustix::event::epoll;
use rustix::event::epoll::CreateFlags;
use rustix::fs::Timespec;
use rustix::pipe::{fcntl_getpipe_size, fcntl_setpipe_size};
use smithay_client_toolkit::data_device_manager::WritePipe; use smithay_client_toolkit::data_device_manager::WritePipe;
use std::cmp::min; use std::cmp::min;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use std::fs::File; use std::fs::File;
use std::io::{ErrorKind, Write}; use std::io::{ErrorKind, Write};
use std::os::fd::RawFd; use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
use std::os::fd::{AsRawFd, OwnedFd};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use std::{fs, io}; use std::{fs, io};
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::sync::broadcast; use tokio::sync::broadcast;
@ -154,7 +158,7 @@ impl Environment {
let source = self let source = self
.data_control_device_manager_state .data_control_device_manager_state
.create_copy_paste_source(&self.queue_handle, [INTERNAL_MIME_TYPE, &item.mime_type]); .create_copy_paste_source(&self.queue_handle, [&item.mime_type, INTERNAL_MIME_TYPE]);
source.set_selection(&device.device); source.set_selection(&device.device);
self.copy_paste_sources.push(source); self.copy_paste_sources.push(source);
@ -273,7 +277,7 @@ impl DataControlSourceHandler for Environment {
source: &ZwlrDataControlSourceV1, source: &ZwlrDataControlSourceV1,
mime: String, mime: String,
write_pipe: WritePipe, write_pipe: WritePipe,
) { ) -> Result<()> {
debug!("Handler received source send request event ({mime})"); debug!("Handler received source send request event ({mime})");
if let Some(item) = lock!(self.clipboard).clone() { if let Some(item) = lock!(self.clipboard).clone() {
@ -294,28 +298,30 @@ impl DataControlSourceHandler for Environment {
), ),
}; };
let pipe_size = set_pipe_size(fd.as_raw_fd(), bytes.len()) let pipe_size =
.expect("Failed to increase pipe size"); set_pipe_size(fd.as_fd(), bytes.len()).expect("Failed to increase pipe size");
let mut file = File::from(fd.try_clone().expect("to be able to clone")); let mut file = File::from(fd.try_clone().expect("to be able to clone"));
debug!("Writing {} bytes", bytes.len()); debug!("Writing {} bytes", bytes.len());
let mut events = (0..16).map(|_| EpollEvent::empty()).collect::<Vec<_>>(); let epoll = epoll::create(CreateFlags::CLOEXEC)?;
let epoll_event = EpollEvent::new(EpollFlags::EPOLLOUT, 0); epoll::add(
&epoll,
fd,
epoll::EventData::new_u64(0),
epoll::EventFlags::OUT,
)?;
let epoll_fd = let mut events = Vec::with_capacity(16);
Epoll::new(EpollCreateFlags::empty()).expect("to get valid file descriptor");
epoll_fd
.add(fd, epoll_event)
.expect("to send valid epoll operation");
let timeout = EpollTimeout::from(100u16);
while !bytes.is_empty() { while !bytes.is_empty() {
let chunk = &bytes[..min(pipe_size as usize, bytes.len())]; let chunk = &bytes[..min(pipe_size, bytes.len())];
epoll_fd epoll::wait(
.wait(&mut events, timeout) &epoll,
.expect("Failed to wait to epoll"); spare_capacity(&mut events),
Some(&Timespec::try_from(Duration::from_millis(100))?),
)?;
match file.write(chunk) { match file.write(chunk) {
Ok(written) => { Ok(written) => {
@ -331,9 +337,11 @@ impl DataControlSourceHandler for Environment {
debug!("Done writing"); debug!("Done writing");
} else { } else {
error!("Failed to find source"); error!("Failed to find source (mime: '{mime}')");
} }
} }
Ok(())
} }
fn cancelled( fn cancelled(
@ -358,7 +366,7 @@ impl DataControlSourceHandler for Environment {
/// it will be clamped at this. /// it will be clamped at this.
/// ///
/// Returns the new size if succeeded. /// Returns the new size if succeeded.
fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> { fn set_pipe_size(fd: BorrowedFd, size: usize) -> io::Result<usize> {
// clamp size at kernel max // clamp size at kernel max
let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size") let max_pipe_size = fs::read_to_string("/proc/sys/fs/pipe-max-size")
.expect("Failed to find pipe-max-size virtual kernel file") .expect("Failed to find pipe-max-size virtual kernel file")
@ -368,23 +376,24 @@ fn set_pipe_size(fd: RawFd, size: usize) -> io::Result<i32> {
let size = min(size, max_pipe_size); let size = min(size, max_pipe_size);
let curr_size = fcntl(fd, F_GETPIPE_SZ)? as usize; let curr_size = fcntl_getpipe_size(fd)?;
trace!("Current pipe size: {curr_size}"); trace!("Current pipe size: {curr_size}");
let new_size = if size > curr_size { let new_size = if size > curr_size {
trace!("Requesting pipe size increase to (at least): {size}"); trace!("Requesting pipe size increase to (at least): {size}");
let res = fcntl(fd, F_SETPIPE_SZ(size as i32))?; fcntl_setpipe_size(fd, size)?;
let res = fcntl_getpipe_size(fd)?;
trace!("New pipe size: {res}"); trace!("New pipe size: {res}");
if res < size as i32 { if res < size {
return Err(io::Error::last_os_error()); return Err(io::Error::last_os_error());
} }
res res
} else { } else {
size as i32 size
}; };
Ok(new_size) Ok(new_size)

View file

@ -1,7 +1,6 @@
use super::manager::DataControlDeviceManagerState; use super::manager::DataControlDeviceManagerState;
use crate::lock; use crate::lock;
use nix::fcntl::OFlag; use rustix::pipe::{PipeFlags, pipe_with};
use nix::unistd::pipe2;
use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError; use smithay_client_toolkit::data_device_manager::data_offer::DataOfferError;
use std::ops::DerefMut; use std::ops::DerefMut;
use std::os::fd::AsFd; use std::os::fd::AsFd;
@ -171,7 +170,7 @@ where
/// could not be created. /// could not be created.
pub fn receive(offer: &ZwlrDataControlOfferV1, mime_type: String) -> std::io::Result<Receiver> { pub fn receive(offer: &ZwlrDataControlOfferV1, mime_type: String) -> std::io::Result<Receiver> {
// create a pipe // create a pipe
let (readfd, writefd) = pipe2(OFlag::O_CLOEXEC | OFlag::O_NONBLOCK)?; let (readfd, writefd) = pipe_with(PipeFlags::CLOEXEC)?;
offer.receive(mime_type, writefd.as_fd()); offer.receive(mime_type, writefd.as_fd());

View file

@ -1,6 +1,8 @@
use super::device::DataControlDevice; use super::device::DataControlDevice;
use super::manager::DataControlDeviceManagerState; use super::manager::DataControlDeviceManagerState;
use color_eyre::Result;
use smithay_client_toolkit::data_device_manager::WritePipe; use smithay_client_toolkit::data_device_manager::WritePipe;
use tracing::error;
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle}; use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::{ use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::{
Event, ZwlrDataControlSourceV1, Event, ZwlrDataControlSourceV1,
@ -41,7 +43,7 @@ pub trait DataControlSourceHandler: Sized {
source: &ZwlrDataControlSourceV1, source: &ZwlrDataControlSourceV1,
mime: String, mime: String,
fd: WritePipe, fd: WritePipe,
); ) -> Result<()>;
/// The data source is no longer valid /// The data source is no longer valid
/// Cleanup & destroy this resource /// Cleanup & destroy this resource
@ -68,7 +70,9 @@ where
) { ) {
match event { match event {
Event::Send { mime_type, fd } => { Event::Send { mime_type, fd } => {
state.send_request(conn, qh, source, mime_type, fd.into()); if let Err(err) = state.send_request(conn, qh, source, mime_type, fd.into()) {
error!("{err:#}");
}
} }
Event::Cancelled => { Event::Cancelled => {
state.cancelled(conn, qh, source); state.cancelled(conn, qh, source);