logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
[package]
name = "gpui_linux"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/gpui_linux.rs"
[features]
default = ["wayland", "x11"]
test-support = ["gpui/test-support"]
wayland = [
"bitflags",
"gpui_wgpu",
"ashpd/wayland",
"calloop-wayland-source",
"wayland-backend",
"wayland-client",
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-plasma",
"wayland-protocols-wlr",
"filedescriptor",
"xkbcommon",
"open",
"gpui/wayland",
]
x11 = [
"gpui_wgpu",
"ashpd",
"as-raw-xcb-connection",
"x11rb",
"xkbcommon",
"xim",
"x11-clipboard",
"filedescriptor",
"open",
"scap?/x11",
]
screen-capture = [
"gpui/screen-capture",
"scap",
]
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
anyhow.workspace = true
bytemuck = "1"
collections.workspace = true
image.workspace = true
futures.workspace = true
gpui.workspace = true
gpui_wgpu = { workspace = true, optional = true, features = ["font-kit"] }
http_client.workspace = true
itertools.workspace = true
libc.workspace = true
log.workspace = true
parking_lot.workspace = true
pathfinder_geometry = "0.5"
pollster.workspace = true
profiling.workspace = true
smallvec.workspace = true
smol.workspace = true
strum.workspace = true
url.workspace = true
util.workspace = true
uuid.workspace = true
# Always used
oo7 = { version = "0.6", default-features = false, features = [
"async-std",
"native_crypto",
] }
calloop = "0.14.3"
raw-window-handle = "0.6"
# Used in both windowing options
ashpd = { workspace = true, optional = true }
swash = { version = "0.2.6" }
bitflags = { workspace = true, optional = true }
filedescriptor = { version = "0.8.2", optional = true }
open = { version = "5.2.0", optional = true }
xkbcommon = { version = "0.8.0", features = ["wayland", "x11"], optional = true }
# Screen capture
scap = { workspace = true, optional = true }
# Wayland
calloop-wayland-source = { version = "0.4.1", optional = true }
wayland-backend = { version = "0.3.3", features = [
"client_system",
"dlopen",
], optional = true }
wayland-client = { version = "0.31.11", optional = true }
wayland-cursor = { version = "0.31.11", optional = true }
wayland-protocols = { version = "0.32.9", features = [
"client",
"staging",
"unstable",
], optional = true }
wayland-protocols-plasma = { version = "0.3.9", features = [
"client",
], optional = true }
wayland-protocols-wlr = { version = "0.3.9", features = [
"client",
], optional = true }
# X11
as-raw-xcb-connection = { version = "1", optional = true }
x11rb = { version = "0.13.1", features = [
"allow-unsafe-code",
"xkb",
"randr",
"xinput",
"cursor",
"resource_manager",
"sync",
"dri3",
], optional = true }
# WARNING: If you change this, you must also publish a new version of zed-xim to crates.io
xim = { git = "https://github.com/zed-industries/xim-rs.git", rev = "16f35a2c881b815a2b6cdfd6687988e84f8447d8", features = [
"x11rb-xcb",
"x11rb-client",
], package = "zed-xim", version = "0.4.0-zed", optional = true }
x11-clipboard = { version = "0.9.3", optional = true }

View File

@@ -0,0 +1 @@
../../LICENSE-APACHE

View File

@@ -0,0 +1,4 @@
#![cfg(any(target_os = "linux", target_os = "freebsd"))]
mod linux;
pub use linux::current_platform;

View File

@@ -0,0 +1,57 @@
mod dispatcher;
mod headless;
mod keyboard;
mod platform;
#[cfg(any(feature = "wayland", feature = "x11"))]
mod text_system;
#[cfg(feature = "wayland")]
mod wayland;
#[cfg(feature = "x11")]
mod x11;
#[cfg(any(feature = "wayland", feature = "x11"))]
mod xdg_desktop_portal;
pub use dispatcher::*;
pub(crate) use headless::*;
pub(crate) use keyboard::*;
pub(crate) use platform::*;
#[cfg(any(feature = "wayland", feature = "x11"))]
pub(crate) use text_system::*;
#[cfg(feature = "wayland")]
pub(crate) use wayland::*;
#[cfg(feature = "x11")]
pub(crate) use x11::*;
use std::rc::Rc;
/// Returns the default platform implementation for the current OS.
pub fn current_platform(headless: bool) -> Rc<dyn gpui::Platform> {
#[cfg(feature = "x11")]
use anyhow::Context as _;
if headless {
return Rc::new(LinuxPlatform {
inner: HeadlessClient::new(),
});
}
match gpui::guess_compositor() {
#[cfg(feature = "wayland")]
"Wayland" => Rc::new(LinuxPlatform {
inner: WaylandClient::new(),
}),
#[cfg(feature = "x11")]
"X11" => Rc::new(LinuxPlatform {
inner: X11Client::new()
.context("Failed to initialize X11 client.")
.unwrap(),
}),
"Headless" => Rc::new(LinuxPlatform {
inner: HeadlessClient::new(),
}),
_ => unreachable!(),
}
}

View File

@@ -0,0 +1,417 @@
use calloop::{
EventLoop, PostAction,
channel::{self, Sender},
timer::TimeoutAction,
};
use util::ResultExt;
use std::{
mem::MaybeUninit,
thread,
time::{Duration, Instant},
};
use gpui::{
GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, PriorityQueueReceiver,
PriorityQueueSender, RunnableVariant, TaskTiming, ThreadTaskTimings, profiler,
};
struct TimerAfter {
duration: Duration,
runnable: RunnableVariant,
}
pub(crate) struct LinuxDispatcher {
main_sender: PriorityQueueCalloopSender<RunnableVariant>,
timer_sender: Sender<TimerAfter>,
background_sender: PriorityQueueSender<RunnableVariant>,
_background_threads: Vec<thread::JoinHandle<()>>,
main_thread_id: thread::ThreadId,
}
const MIN_THREADS: usize = 2;
impl LinuxDispatcher {
pub fn new(main_sender: PriorityQueueCalloopSender<RunnableVariant>) -> Self {
let (background_sender, background_receiver) = PriorityQueueReceiver::new();
let thread_count =
std::thread::available_parallelism().map_or(MIN_THREADS, |i| i.get().max(MIN_THREADS));
let mut background_threads = (0..thread_count)
.map(|i| {
let receiver: PriorityQueueReceiver<RunnableVariant> = background_receiver.clone();
std::thread::Builder::new()
.name(format!("Worker-{i}"))
.spawn(move || {
for runnable in receiver.iter() {
let start = Instant::now();
let location = runnable.metadata().location;
let mut timing = TaskTiming {
location,
start,
end: None,
};
profiler::add_task_timing(timing);
runnable.run();
let end = Instant::now();
timing.end = Some(end);
profiler::add_task_timing(timing);
log::trace!(
"background thread {}: ran runnable. took: {:?}",
i,
start.elapsed()
);
}
})
.unwrap()
})
.collect::<Vec<_>>();
let (timer_sender, timer_channel) = calloop::channel::channel::<TimerAfter>();
let timer_thread = std::thread::Builder::new()
.name("Timer".to_owned())
.spawn(move || {
let mut event_loop: EventLoop<()> =
EventLoop::try_new().expect("Failed to initialize timer loop!");
let handle = event_loop.handle();
let timer_handle = event_loop.handle();
handle
.insert_source(timer_channel, move |e, _, _| {
if let channel::Event::Msg(timer) = e {
let mut runnable = Some(timer.runnable);
timer_handle
.insert_source(
calloop::timer::Timer::from_duration(timer.duration),
move |_, _, _| {
if let Some(runnable) = runnable.take() {
let start = Instant::now();
let location = runnable.metadata().location;
let mut timing = TaskTiming {
location,
start,
end: None,
};
profiler::add_task_timing(timing);
runnable.run();
let end = Instant::now();
timing.end = Some(end);
profiler::add_task_timing(timing);
}
TimeoutAction::Drop
},
)
.expect("Failed to start timer");
}
})
.expect("Failed to start timer thread");
event_loop.run(None, &mut (), |_| {}).log_err();
})
.unwrap();
background_threads.push(timer_thread);
Self {
main_sender,
timer_sender,
background_sender,
_background_threads: background_threads,
main_thread_id: thread::current().id(),
}
}
}
impl PlatformDispatcher for LinuxDispatcher {
fn get_all_timings(&self) -> Vec<gpui::ThreadTaskTimings> {
let global_timings = GLOBAL_THREAD_TIMINGS.lock();
ThreadTaskTimings::convert(&global_timings)
}
fn get_current_thread_timings(&self) -> gpui::ThreadTaskTimings {
gpui::profiler::get_current_thread_task_timings()
}
fn is_main_thread(&self) -> bool {
thread::current().id() == self.main_thread_id
}
fn dispatch(&self, runnable: RunnableVariant, priority: Priority) {
self.background_sender
.send(priority, runnable)
.unwrap_or_else(|_| panic!("blocking sender returned without value"));
}
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) {
self.main_sender
.send(priority, runnable)
.unwrap_or_else(|runnable| {
// NOTE: Runnable may wrap a Future that is !Send.
//
// This is usually safe because we only poll it on the main thread.
// However if the send fails, we know that:
// 1. main_receiver has been dropped (which implies the app is shutting down)
// 2. we are on a background thread.
// It is not safe to drop something !Send on the wrong thread, and
// the app will exit soon anyway, so we must forget the runnable.
std::mem::forget(runnable);
});
}
fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant) {
self.timer_sender
.send(TimerAfter { duration, runnable })
.ok();
}
fn spawn_realtime(&self, f: Box<dyn FnOnce() + Send>) {
std::thread::spawn(move || {
// SAFETY: always safe to call
let thread_id = unsafe { libc::pthread_self() };
let policy = libc::SCHED_FIFO;
let sched_priority = 65;
// SAFETY: all sched_param members are valid when initialized to zero.
let mut sched_param =
unsafe { MaybeUninit::<libc::sched_param>::zeroed().assume_init() };
sched_param.sched_priority = sched_priority;
// SAFETY: sched_param is a valid initialized structure
let result = unsafe { libc::pthread_setschedparam(thread_id, policy, &sched_param) };
if result != 0 {
log::warn!("failed to set realtime thread priority");
}
f();
});
}
}
pub struct PriorityQueueCalloopSender<T> {
sender: PriorityQueueSender<T>,
ping: calloop::ping::Ping,
}
impl<T> PriorityQueueCalloopSender<T> {
fn new(tx: PriorityQueueSender<T>, ping: calloop::ping::Ping) -> Self {
Self { sender: tx, ping }
}
fn send(&self, priority: Priority, item: T) -> Result<(), gpui::queue::SendError<T>> {
let res = self.sender.send(priority, item);
if res.is_ok() {
self.ping.ping();
}
res
}
}
impl<T> Drop for PriorityQueueCalloopSender<T> {
fn drop(&mut self) {
self.ping.ping();
}
}
pub struct PriorityQueueCalloopReceiver<T> {
receiver: PriorityQueueReceiver<T>,
source: calloop::ping::PingSource,
ping: calloop::ping::Ping,
}
impl<T> PriorityQueueCalloopReceiver<T> {
pub fn new() -> (PriorityQueueCalloopSender<T>, Self) {
let (ping, source) = calloop::ping::make_ping().expect("Failed to create a Ping.");
let (tx, rx) = PriorityQueueReceiver::new();
(
PriorityQueueCalloopSender::new(tx, ping.clone()),
Self {
receiver: rx,
source,
ping,
},
)
}
}
use calloop::channel::Event;
#[derive(Debug)]
pub struct ChannelError(calloop::ping::PingError);
impl std::fmt::Display for ChannelError {
#[cfg_attr(feature = "nightly_coverage", coverage(off))]
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.0, f)
}
}
impl std::error::Error for ChannelError {
#[cfg_attr(feature = "nightly_coverage", coverage(off))]
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.0)
}
}
impl<T> calloop::EventSource for PriorityQueueCalloopReceiver<T> {
type Event = Event<T>;
type Metadata = ();
type Ret = ();
type Error = ChannelError;
fn process_events<F>(
&mut self,
readiness: calloop::Readiness,
token: calloop::Token,
mut callback: F,
) -> Result<calloop::PostAction, Self::Error>
where
F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret,
{
let mut clear_readiness = false;
let mut disconnected = false;
let action = self
.source
.process_events(readiness, token, |(), &mut ()| {
let mut is_empty = true;
let receiver = self.receiver.clone();
for runnable in receiver.try_iter() {
match runnable {
Ok(r) => {
callback(Event::Msg(r), &mut ());
is_empty = false;
}
Err(_) => {
disconnected = true;
}
}
}
if disconnected {
callback(Event::Closed, &mut ());
}
if is_empty {
clear_readiness = true;
}
})
.map_err(ChannelError)?;
if disconnected {
Ok(PostAction::Remove)
} else if clear_readiness {
Ok(action)
} else {
// Re-notify the ping source so we can try again.
self.ping.ping();
Ok(PostAction::Continue)
}
}
fn register(
&mut self,
poll: &mut calloop::Poll,
token_factory: &mut calloop::TokenFactory,
) -> calloop::Result<()> {
self.source.register(poll, token_factory)
}
fn reregister(
&mut self,
poll: &mut calloop::Poll,
token_factory: &mut calloop::TokenFactory,
) -> calloop::Result<()> {
self.source.reregister(poll, token_factory)
}
fn unregister(&mut self, poll: &mut calloop::Poll) -> calloop::Result<()> {
self.source.unregister(poll)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn calloop_works() {
let mut event_loop = calloop::EventLoop::try_new().unwrap();
let handle = event_loop.handle();
let (tx, rx) = PriorityQueueCalloopReceiver::new();
struct Data {
got_msg: bool,
got_closed: bool,
}
let mut data = Data {
got_msg: false,
got_closed: false,
};
let _channel_token = handle
.insert_source(rx, move |evt, &mut (), data: &mut Data| match evt {
Event::Msg(()) => {
data.got_msg = true;
}
Event::Closed => {
data.got_closed = true;
}
})
.unwrap();
// nothing is sent, nothing is received
event_loop
.dispatch(Some(::std::time::Duration::ZERO), &mut data)
.unwrap();
assert!(!data.got_msg);
assert!(!data.got_closed);
// a message is send
tx.send(Priority::Medium, ()).unwrap();
event_loop
.dispatch(Some(::std::time::Duration::ZERO), &mut data)
.unwrap();
assert!(data.got_msg);
assert!(!data.got_closed);
// the sender is dropped
drop(tx);
event_loop
.dispatch(Some(::std::time::Duration::ZERO), &mut data)
.unwrap();
assert!(data.got_msg);
assert!(data.got_closed);
}
}
// running 1 test
// test linux::dispatcher::tests::tomato ... FAILED
// failures:
// ---- linux::dispatcher::tests::tomato stdout ----
// [crates/gpui/src/platform/linux/dispatcher.rs:262:9]
// returning 1 tasks to process
// [crates/gpui/src/platform/linux/dispatcher.rs:480:75] evt = Msg(
// (),
// )
// returning 0 tasks to process
// thread 'linux::dispatcher::tests::tomato' (478301) panicked at crates/gpui/src/platform/linux/dispatcher.rs:515:9:
// assertion failed: data.got_closed
// note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

View File

@@ -0,0 +1,3 @@
mod client;
pub(crate) use client::*;

View File

@@ -0,0 +1,128 @@
use std::cell::RefCell;
use std::rc::Rc;
use calloop::{EventLoop, LoopHandle};
use util::ResultExt;
use crate::linux::{LinuxClient, LinuxCommon, LinuxKeyboardLayout};
use gpui::{
AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, PlatformKeyboardLayout,
PlatformWindow, WindowParams,
};
pub struct HeadlessClientState {
pub(crate) _loop_handle: LoopHandle<'static, HeadlessClient>,
pub(crate) event_loop: Option<calloop::EventLoop<'static, HeadlessClient>>,
pub(crate) common: LinuxCommon,
}
#[derive(Clone)]
pub(crate) struct HeadlessClient(Rc<RefCell<HeadlessClientState>>);
impl HeadlessClient {
pub(crate) fn new() -> Self {
let event_loop = EventLoop::try_new().unwrap();
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
let handle = event_loop.handle();
handle
.insert_source(main_receiver, |event, _, _: &mut HeadlessClient| {
if let calloop::channel::Event::Msg(runnable) = event {
runnable.run();
}
})
.ok();
HeadlessClient(Rc::new(RefCell::new(HeadlessClientState {
event_loop: Some(event_loop),
_loop_handle: handle,
common,
})))
}
}
impl LinuxClient for HeadlessClient {
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R {
f(&mut self.0.borrow_mut().common)
}
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
Box::new(LinuxKeyboardLayout::new("unknown".into()))
}
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
vec![]
}
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>> {
None
}
fn display(&self, _id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
None
}
#[cfg(feature = "screen-capture")]
fn screen_capture_sources(
&self,
) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn gpui::ScreenCaptureSource>>>>
{
let (tx, rx) = futures::channel::oneshot::channel();
tx.send(Err(anyhow::anyhow!(
"Headless mode does not support screen capture."
)))
.ok();
rx
}
fn active_window(&self) -> Option<AnyWindowHandle> {
None
}
fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
None
}
fn open_window(
&self,
_handle: AnyWindowHandle,
_params: WindowParams,
) -> anyhow::Result<Box<dyn PlatformWindow>> {
anyhow::bail!("neither DISPLAY nor WAYLAND_DISPLAY is set. You can run in headless mode");
}
fn compositor_name(&self) -> &'static str {
"headless"
}
fn set_cursor_style(&self, _style: CursorStyle) {}
fn open_uri(&self, _uri: &str) {}
fn reveal_path(&self, _path: std::path::PathBuf) {}
fn write_to_primary(&self, _item: gpui::ClipboardItem) {}
fn write_to_clipboard(&self, _item: gpui::ClipboardItem) {}
fn read_from_primary(&self) -> Option<gpui::ClipboardItem> {
None
}
fn read_from_clipboard(&self) -> Option<gpui::ClipboardItem> {
None
}
fn run(&self) {
let mut event_loop = self
.0
.borrow_mut()
.event_loop
.take()
.expect("App is already running");
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
}
}

View File

@@ -0,0 +1,22 @@
use gpui::{PlatformKeyboardLayout, SharedString};
#[derive(Clone)]
pub(crate) struct LinuxKeyboardLayout {
name: SharedString,
}
impl PlatformKeyboardLayout for LinuxKeyboardLayout {
fn id(&self) -> &str {
&self.name
}
fn name(&self) -> &str {
&self.name
}
}
impl LinuxKeyboardLayout {
pub(crate) fn new(name: SharedString) -> Self {
Self { name }
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
pub(crate) use gpui_wgpu::CosmicTextSystem;

View File

@@ -0,0 +1,41 @@
mod client;
mod clipboard;
mod cursor;
mod display;
mod serial;
mod window;
/// Contains Types for configuring layer_shell surfaces.
pub mod layer_shell;
pub(crate) use client::*;
use wayland_protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape;
use gpui::CursorStyle;
pub(super) fn to_shape(style: CursorStyle) -> Shape {
match style {
CursorStyle::Arrow => Shape::Default,
CursorStyle::IBeam => Shape::Text,
CursorStyle::Crosshair => Shape::Crosshair,
CursorStyle::ClosedHand => Shape::Grabbing,
CursorStyle::OpenHand => Shape::Grab,
CursorStyle::PointingHand => Shape::Pointer,
CursorStyle::ResizeLeft => Shape::WResize,
CursorStyle::ResizeRight => Shape::EResize,
CursorStyle::ResizeLeftRight => Shape::EwResize,
CursorStyle::ResizeUp => Shape::NResize,
CursorStyle::ResizeDown => Shape::SResize,
CursorStyle::ResizeUpDown => Shape::NsResize,
CursorStyle::ResizeUpLeftDownRight => Shape::NwseResize,
CursorStyle::ResizeUpRightDownLeft => Shape::NeswResize,
CursorStyle::ResizeColumn => Shape::ColResize,
CursorStyle::ResizeRow => Shape::RowResize,
CursorStyle::IBeamCursorForVerticalLayout => Shape::VerticalText,
CursorStyle::OperationNotAllowed => Shape::NotAllowed,
CursorStyle::DragLink => Shape::Alias,
CursorStyle::DragCopy => Shape::Copy,
CursorStyle::ContextualMenu => Shape::ContextMenu,
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,260 @@
use std::{
fs::File,
io::{ErrorKind, Write},
os::fd::{AsRawFd, BorrowedFd, OwnedFd},
};
use calloop::{LoopHandle, PostAction};
use filedescriptor::Pipe;
use strum::IntoEnumIterator;
use wayland_client::{Connection, protocol::wl_data_offer::WlDataOffer};
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1;
use crate::linux::{WaylandClientStatePtr, platform::read_fd};
use gpui::{ClipboardEntry, ClipboardItem, Image, ImageFormat, hash};
/// Text mime types that we'll offer to other programs.
pub(crate) const TEXT_MIME_TYPES: [&str; 3] =
["text/plain;charset=utf-8", "UTF8_STRING", "text/plain"];
pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list";
/// Text mime types that we'll accept from other programs.
pub(crate) const ALLOWED_TEXT_MIME_TYPES: [&str; 2] = ["text/plain;charset=utf-8", "UTF8_STRING"];
pub(crate) struct Clipboard {
connection: Connection,
loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
self_mime: String,
// Internal clipboard
contents: Option<ClipboardItem>,
primary_contents: Option<ClipboardItem>,
// External clipboard
cached_read: Option<ClipboardItem>,
current_offer: Option<DataOffer<WlDataOffer>>,
cached_primary_read: Option<ClipboardItem>,
current_primary_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
}
pub(crate) trait ReceiveData {
fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>);
}
impl ReceiveData for WlDataOffer {
fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) {
self.receive(mime_type, fd);
}
}
impl ReceiveData for ZwpPrimarySelectionOfferV1 {
fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) {
self.receive(mime_type, fd);
}
}
#[derive(Clone, Debug)]
/// Wrapper for `WlDataOffer` and `ZwpPrimarySelectionOfferV1`, used to help track mime types.
pub(crate) struct DataOffer<T: ReceiveData> {
pub inner: T,
mime_types: Vec<String>,
}
impl<T: ReceiveData> DataOffer<T> {
pub fn new(offer: T) -> Self {
Self {
inner: offer,
mime_types: Vec::new(),
}
}
pub fn add_mime_type(&mut self, mime_type: String) {
self.mime_types.push(mime_type)
}
fn has_mime_type(&self, mime_type: &str) -> bool {
self.mime_types.iter().any(|t| t == mime_type)
}
fn read_bytes(&self, connection: &Connection, mime_type: &str) -> Option<Vec<u8>> {
let pipe = Pipe::new().unwrap();
self.inner.receive_data(mime_type.to_string(), unsafe {
BorrowedFd::borrow_raw(pipe.write.as_raw_fd())
});
let fd = pipe.read;
drop(pipe.write);
connection.flush().unwrap();
match unsafe { read_fd(fd) } {
Ok(bytes) => Some(bytes),
Err(err) => {
log::error!("error reading clipboard pipe: {err:?}");
None
}
}
}
fn read_text(&self, connection: &Connection) -> Option<ClipboardItem> {
let mime_type = self.mime_types.iter().find(|&mime_type| {
ALLOWED_TEXT_MIME_TYPES
.iter()
.any(|&allowed| allowed == mime_type)
})?;
let bytes = self.read_bytes(connection, mime_type)?;
let text_content = match String::from_utf8(bytes) {
Ok(content) => content,
Err(e) => {
log::error!("Failed to convert clipboard content to UTF-8: {}", e);
return None;
}
};
// Normalize the text to unix line endings, otherwise
// copying from eg: firefox inserts a lot of blank
// lines, and that is super annoying.
let result = text_content.replace("\r\n", "\n");
Some(ClipboardItem::new_string(result))
}
fn read_image(&self, connection: &Connection) -> Option<ClipboardItem> {
for format in ImageFormat::iter() {
let mime_type = format.mime_type();
if !self.has_mime_type(mime_type) {
continue;
}
if let Some(bytes) = self.read_bytes(connection, mime_type) {
let id = hash(&bytes);
return Some(ClipboardItem {
entries: vec![ClipboardEntry::Image(Image { format, bytes, id })],
});
}
}
None
}
}
impl Clipboard {
pub fn new(
connection: Connection,
loop_handle: LoopHandle<'static, WaylandClientStatePtr>,
) -> Self {
Self {
connection,
loop_handle,
self_mime: format!("pid/{}", std::process::id()),
contents: None,
primary_contents: None,
cached_read: None,
current_offer: None,
cached_primary_read: None,
current_primary_offer: None,
}
}
pub fn set(&mut self, item: ClipboardItem) {
self.contents = Some(item);
}
pub fn set_primary(&mut self, item: ClipboardItem) {
self.primary_contents = Some(item);
}
pub fn set_offer(&mut self, data_offer: Option<DataOffer<WlDataOffer>>) {
self.cached_read = None;
self.current_offer = data_offer;
}
pub fn set_primary_offer(&mut self, data_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>) {
self.cached_primary_read = None;
self.current_primary_offer = data_offer;
}
pub fn self_mime(&self) -> String {
self.self_mime.clone()
}
pub fn send(&self, _mime_type: String, fd: OwnedFd) {
if let Some(text) = self.contents.as_ref().and_then(|contents| contents.text()) {
self.send_internal(fd, text.as_bytes().to_owned());
}
}
pub fn send_primary(&self, _mime_type: String, fd: OwnedFd) {
if let Some(text) = self
.primary_contents
.as_ref()
.and_then(|contents| contents.text())
{
self.send_internal(fd, text.as_bytes().to_owned());
}
}
pub fn read(&mut self) -> Option<ClipboardItem> {
let offer = self.current_offer.as_ref()?;
if let Some(cached) = self.cached_read.clone() {
return Some(cached);
}
if offer.has_mime_type(&self.self_mime) {
return self.contents.clone();
}
let item = offer
.read_text(&self.connection)
.or_else(|| offer.read_image(&self.connection))?;
self.cached_read = Some(item.clone());
Some(item)
}
pub fn read_primary(&mut self) -> Option<ClipboardItem> {
let offer = self.current_primary_offer.as_ref()?;
if let Some(cached) = self.cached_primary_read.clone() {
return Some(cached);
}
if offer.has_mime_type(&self.self_mime) {
return self.primary_contents.clone();
}
let item = offer
.read_text(&self.connection)
.or_else(|| offer.read_image(&self.connection))?;
self.cached_primary_read = Some(item.clone());
Some(item)
}
fn send_internal(&self, fd: OwnedFd, bytes: Vec<u8>) {
let mut written = 0;
self.loop_handle
.insert_source(
calloop::generic::Generic::new(
File::from(fd),
calloop::Interest::WRITE,
calloop::Mode::Level,
),
move |_, file, _| {
let file = unsafe { file.get_mut() };
loop {
match file.write(&bytes[written..]) {
Ok(n) if written + n == bytes.len() => {
written += n;
break Ok(PostAction::Remove);
}
Ok(n) => written += n,
Err(err) if err.kind() == ErrorKind::WouldBlock => {
break Ok(PostAction::Continue);
}
Err(_) => break Ok(PostAction::Remove),
}
}
},
)
.unwrap();
}
}

View File

@@ -0,0 +1,152 @@
use crate::linux::Globals;
use crate::linux::{DEFAULT_CURSOR_ICON_NAME, log_cursor_icon_warning};
use anyhow::{Context as _, anyhow};
use util::ResultExt;
use wayland_client::Connection;
use wayland_client::protocol::wl_surface::WlSurface;
use wayland_client::protocol::{wl_pointer::WlPointer, wl_shm::WlShm};
use wayland_cursor::{CursorImageBuffer, CursorTheme};
pub(crate) struct Cursor {
loaded_theme: Option<LoadedTheme>,
size: u32,
scaled_size: u32,
surface: WlSurface,
shm: WlShm,
connection: Connection,
}
pub(crate) struct LoadedTheme {
theme: CursorTheme,
name: Option<String>,
scaled_size: u32,
}
impl Drop for Cursor {
fn drop(&mut self) {
self.loaded_theme.take();
self.surface.destroy();
}
}
impl Cursor {
pub fn new(connection: &Connection, globals: &Globals, size: u32) -> Self {
let mut this = Self {
loaded_theme: None,
size,
scaled_size: size,
surface: globals.compositor.create_surface(&globals.qh, ()),
shm: globals.shm.clone(),
connection: connection.clone(),
};
this.set_theme_internal(None);
this
}
fn set_theme_internal(&mut self, theme_name: Option<String>) {
if let Some(loaded_theme) = self.loaded_theme.as_ref()
&& loaded_theme.name == theme_name
&& loaded_theme.scaled_size == self.scaled_size
{
return;
}
let result = if let Some(theme_name) = theme_name.as_ref() {
CursorTheme::load_from_name(
&self.connection,
self.shm.clone(),
theme_name,
self.scaled_size,
)
} else {
CursorTheme::load(&self.connection, self.shm.clone(), self.scaled_size)
};
if let Some(theme) = result
.context("Wayland: Failed to load cursor theme")
.log_err()
{
self.loaded_theme = Some(LoadedTheme {
theme,
name: theme_name,
scaled_size: self.scaled_size,
});
}
}
pub fn set_theme(&mut self, theme_name: String) {
self.set_theme_internal(Some(theme_name));
}
fn set_scaled_size(&mut self, scaled_size: u32) {
self.scaled_size = scaled_size;
let theme_name = self
.loaded_theme
.as_ref()
.and_then(|loaded_theme| loaded_theme.name.clone());
self.set_theme_internal(theme_name);
}
pub fn set_size(&mut self, size: u32) {
self.size = size;
self.set_scaled_size(size);
}
pub fn set_icon(
&mut self,
wl_pointer: &WlPointer,
serial_id: u32,
cursor_icon_names: &[&str],
scale: i32,
) {
self.set_scaled_size(self.size * scale as u32);
let Some(loaded_theme) = &mut self.loaded_theme else {
log::warn!("Wayland: Unable to load cursor themes");
return;
};
let theme = &mut loaded_theme.theme;
let buffer: &CursorImageBuffer;
'outer: {
for cursor_icon_name in cursor_icon_names {
if let Some(cursor) = theme.get_cursor(cursor_icon_name) {
buffer = &cursor[0];
break 'outer;
}
}
if let Some(cursor) = theme.get_cursor(DEFAULT_CURSOR_ICON_NAME) {
buffer = &cursor[0];
log_cursor_icon_warning(anyhow!(
"wayland: Unable to get cursor icon {:?}. \
Using default cursor icon: '{}'",
cursor_icon_names,
DEFAULT_CURSOR_ICON_NAME
));
} else {
log_cursor_icon_warning(anyhow!(
"wayland: Unable to fallback on default cursor icon '{}' for theme '{}'",
DEFAULT_CURSOR_ICON_NAME,
loaded_theme.name.as_deref().unwrap_or("default")
));
return;
}
}
let (width, height) = buffer.dimensions();
let (hot_x, hot_y) = buffer.hotspot();
self.surface.set_buffer_scale(scale);
wl_pointer.set_cursor(
serial_id,
Some(&self.surface),
hot_x as i32 / scale,
hot_y as i32 / scale,
);
self.surface.attach(Some(buffer), 0, 0);
self.surface.damage(0, 0, width as i32, height as i32);
self.surface.commit();
}
}

View File

@@ -0,0 +1,42 @@
use std::{
fmt::Debug,
hash::{Hash, Hasher},
};
use anyhow::Context as _;
use uuid::Uuid;
use wayland_backend::client::ObjectId;
use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay};
#[derive(Debug, Clone)]
pub(crate) struct WaylandDisplay {
/// The ID of the wl_output object
pub id: ObjectId,
pub name: Option<String>,
pub bounds: Bounds<Pixels>,
}
impl Hash for WaylandDisplay {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl PlatformDisplay for WaylandDisplay {
fn id(&self) -> DisplayId {
DisplayId::new(self.id.protocol_id() as u64)
}
fn uuid(&self) -> anyhow::Result<Uuid> {
let name = self
.name
.as_ref()
.context("Wayland display does not have a name")?;
Ok(Uuid::new_v5(&Uuid::NAMESPACE_DNS, name.as_bytes()))
}
fn bounds(&self) -> Bounds<Pixels> {
self.bounds
}
}

View File

@@ -0,0 +1,26 @@
pub use gpui::layer_shell::*;
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
pub(crate) fn wayland_layer(layer: Layer) -> zwlr_layer_shell_v1::Layer {
match layer {
Layer::Background => zwlr_layer_shell_v1::Layer::Background,
Layer::Bottom => zwlr_layer_shell_v1::Layer::Bottom,
Layer::Top => zwlr_layer_shell_v1::Layer::Top,
Layer::Overlay => zwlr_layer_shell_v1::Layer::Overlay,
}
}
pub(crate) fn wayland_anchor(anchor: Anchor) -> zwlr_layer_surface_v1::Anchor {
zwlr_layer_surface_v1::Anchor::from_bits_truncate(anchor.bits())
}
pub(crate) fn wayland_keyboard_interactivity(
value: KeyboardInteractivity,
) -> zwlr_layer_surface_v1::KeyboardInteractivity {
match value {
KeyboardInteractivity::None => zwlr_layer_surface_v1::KeyboardInteractivity::None,
KeyboardInteractivity::Exclusive => zwlr_layer_surface_v1::KeyboardInteractivity::Exclusive,
KeyboardInteractivity::OnDemand => zwlr_layer_surface_v1::KeyboardInteractivity::OnDemand,
}
}

View File

@@ -0,0 +1,67 @@
use collections::HashMap;
#[derive(Debug, Hash, PartialEq, Eq)]
pub(crate) enum SerialKind {
DataDevice,
InputMethod,
MouseEnter,
MousePress,
KeyPress,
/// Serial carried by the most recent `wl_keyboard.enter` event (the
/// compositor's keyboard-focus serial). Mutter's
/// `meta_wayland_keyboard_can_grab_surface` accepts exactly this serial
/// when activating a focused window via xdg-activation-v1, so it must be
/// attached to activation tokens — a stale mouse-press serial is rejected.
KeyboardEnter,
}
#[derive(Debug)]
struct SerialData {
serial: u32,
}
impl SerialData {
fn new(value: u32) -> Self {
Self { serial: value }
}
}
#[derive(Debug)]
/// Helper for tracking of different serial kinds.
pub(crate) struct SerialTracker {
serials: HashMap<SerialKind, SerialData>,
}
impl SerialTracker {
pub fn new() -> Self {
Self {
serials: HashMap::default(),
}
}
pub fn update(&mut self, kind: SerialKind, value: u32) {
self.serials.insert(kind, SerialData::new(value));
}
/// Returns the latest tracked serial of the provided [`SerialKind`]
///
/// Will return 0 if not tracked.
pub fn get(&self, kind: SerialKind) -> u32 {
self.serials
.get(&kind)
.map(|serial_data| serial_data.serial)
.unwrap_or(0)
}
/// Returns the latest tracked serial of the provided [`SerialKind`]s
///
/// Will return 0 if not tracked.
pub fn latest_of(&self, kinds: &[SerialKind]) -> u32 {
kinds
.iter()
.filter_map(|kind| self.serials.get(kind))
.max_by_key(|serial_data| serial_data.serial)
.map(|serial_data| serial_data.serial)
.unwrap_or(0)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
mod client;
mod clipboard;
mod display;
mod event;
mod window;
mod xim_handler;
pub(crate) use client::*;
pub(crate) use display::*;
pub(crate) use event::*;
pub(crate) use window::*;
pub(crate) use xim_handler::*;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
use anyhow::Context as _;
use uuid::Uuid;
use x11rb::{connection::Connection as _, xcb_ffi::XCBConnection};
use gpui::{Bounds, DisplayId, Pixels, PlatformDisplay, Size, px};
#[derive(Debug)]
pub(crate) struct X11Display {
x_screen_index: usize,
bounds: Bounds<Pixels>,
uuid: Uuid,
}
impl X11Display {
pub(crate) fn new(
xcb: &XCBConnection,
scale_factor: f32,
x_screen_index: usize,
) -> anyhow::Result<Self> {
let screen = xcb
.setup()
.roots
.get(x_screen_index)
.with_context(|| format!("No screen found with index {x_screen_index}"))?;
Ok(Self {
x_screen_index,
bounds: Bounds {
origin: Default::default(),
size: Size {
width: px(screen.width_in_pixels as f32 / scale_factor),
height: px(screen.height_in_pixels as f32 / scale_factor),
},
},
uuid: Uuid::from_bytes([0; 16]),
})
}
}
impl PlatformDisplay for X11Display {
fn id(&self) -> DisplayId {
DisplayId::new(self.x_screen_index as u64)
}
fn uuid(&self) -> anyhow::Result<Uuid> {
Ok(self.uuid)
}
fn bounds(&self) -> Bounds<Pixels> {
self.bounds
}
}

View File

@@ -0,0 +1,154 @@
use x11rb::protocol::{
xinput,
xproto::{self, ModMask},
};
use gpui::{Modifiers, MouseButton, NavigationDirection};
pub(crate) enum ButtonOrScroll {
Button(MouseButton),
Scroll(ScrollDirection),
}
pub(crate) enum ScrollDirection {
Up,
Down,
Left,
Right,
}
pub(crate) fn button_or_scroll_from_event_detail(detail: u32) -> Option<ButtonOrScroll> {
Some(match detail {
1 => ButtonOrScroll::Button(MouseButton::Left),
2 => ButtonOrScroll::Button(MouseButton::Middle),
3 => ButtonOrScroll::Button(MouseButton::Right),
4 => ButtonOrScroll::Scroll(ScrollDirection::Up),
5 => ButtonOrScroll::Scroll(ScrollDirection::Down),
6 => ButtonOrScroll::Scroll(ScrollDirection::Left),
7 => ButtonOrScroll::Scroll(ScrollDirection::Right),
8 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Back)),
9 => ButtonOrScroll::Button(MouseButton::Navigate(NavigationDirection::Forward)),
_ => return None,
})
}
pub(crate) fn modifiers_from_state(state: xproto::KeyButMask) -> Modifiers {
Modifiers {
control: state.contains(xproto::KeyButMask::CONTROL),
alt: state.contains(xproto::KeyButMask::MOD1),
shift: state.contains(xproto::KeyButMask::SHIFT),
platform: state.contains(xproto::KeyButMask::MOD4),
function: false,
}
}
pub(crate) fn modifiers_from_xinput_info(modifier_info: xinput::ModifierInfo) -> Modifiers {
Modifiers {
control: modifier_info.effective as u16 & ModMask::CONTROL.bits()
== ModMask::CONTROL.bits(),
alt: modifier_info.effective as u16 & ModMask::M1.bits() == ModMask::M1.bits(),
shift: modifier_info.effective as u16 & ModMask::SHIFT.bits() == ModMask::SHIFT.bits(),
platform: modifier_info.effective as u16 & ModMask::M4.bits() == ModMask::M4.bits(),
function: false,
}
}
pub(crate) fn pressed_button_from_mask(button_mask: u32) -> Option<MouseButton> {
Some(if button_mask & 2 == 2 {
MouseButton::Left
} else if button_mask & 4 == 4 {
MouseButton::Middle
} else if button_mask & 8 == 8 {
MouseButton::Right
} else {
return None;
})
}
pub(crate) fn get_valuator_axis_index(
valuator_mask: &Vec<u32>,
valuator_number: u16,
) -> Option<usize> {
// XInput valuator masks have a 1 at the bit indexes corresponding to each
// valuator present in this event's axisvalues. Axisvalues is ordered from
// lowest valuator number to highest, so counting bits before the 1 bit for
// this valuator yields the index in axisvalues.
if bit_is_set_in_vec(valuator_mask, valuator_number) {
Some(popcount_upto_bit_index(valuator_mask, valuator_number) as usize)
} else {
None
}
}
/// Returns the number of 1 bits in `bit_vec` for all bits where `i < bit_index`.
fn popcount_upto_bit_index(bit_vec: &Vec<u32>, bit_index: u16) -> u32 {
let array_index = bit_index as usize / 32;
let popcount: u32 = bit_vec
.get(array_index)
.map_or(0, |bits| keep_bits_upto(*bits, bit_index % 32).count_ones());
if array_index == 0 {
popcount
} else {
// Valuator numbers over 32 probably never occur for scroll position, but may as well
// support it.
let leading_popcount: u32 = bit_vec
.iter()
.take(array_index)
.map(|bits| bits.count_ones())
.sum();
popcount + leading_popcount
}
}
fn bit_is_set_in_vec(bit_vec: &Vec<u32>, bit_index: u16) -> bool {
let array_index = bit_index as usize / 32;
bit_vec
.get(array_index)
.is_some_and(|bits| bit_is_set(*bits, bit_index % 32))
}
fn bit_is_set(bits: u32, bit_index: u16) -> bool {
bits & (1 << bit_index) != 0
}
/// Sets every bit with `i >= bit_index` to 0.
fn keep_bits_upto(bits: u32, bit_index: u16) -> u32 {
if bit_index == 0 {
0
} else if bit_index >= 32 {
u32::MAX
} else {
bits & ((1 << bit_index) - 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_valuator_axis_index() {
assert!(get_valuator_axis_index(&vec![0b11], 0) == Some(0));
assert!(get_valuator_axis_index(&vec![0b11], 1) == Some(1));
assert!(get_valuator_axis_index(&vec![0b11], 2) == None);
assert!(get_valuator_axis_index(&vec![0b100], 0) == None);
assert!(get_valuator_axis_index(&vec![0b100], 1) == None);
assert!(get_valuator_axis_index(&vec![0b100], 2) == Some(0));
assert!(get_valuator_axis_index(&vec![0b100], 3) == None);
assert!(get_valuator_axis_index(&vec![0b1010, 0], 0) == None);
assert!(get_valuator_axis_index(&vec![0b1010, 0], 1) == Some(0));
assert!(get_valuator_axis_index(&vec![0b1010, 0], 2) == None);
assert!(get_valuator_axis_index(&vec![0b1010, 0], 3) == Some(1));
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 0) == None);
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 1) == Some(0));
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 2) == None);
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 3) == Some(1));
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 32) == Some(2));
assert!(get_valuator_axis_index(&vec![0b1010, 0b1], 33) == None);
assert!(get_valuator_axis_index(&vec![0b1010, 0b101], 34) == Some(3));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,133 @@
use std::default::Default;
use x11rb::protocol::{Event, xproto};
use xim::{AHashMap, AttributeName, Client, ClientError, ClientHandler, InputStyle};
pub enum XimCallbackEvent {
XimXEvent(x11rb::protocol::Event),
XimPreeditEvent(xproto::Window, String),
XimCommitEvent(xproto::Window, String),
}
pub struct XimHandler {
pub im_id: u16,
pub ic_id: u16,
pub connected: bool,
pub window: xproto::Window,
pub last_callback_event: Option<XimCallbackEvent>,
}
impl XimHandler {
pub fn new() -> Self {
Self {
im_id: Default::default(),
ic_id: Default::default(),
connected: false,
window: Default::default(),
last_callback_event: None,
}
}
}
impl<C: Client<XEvent = xproto::KeyPressEvent>> ClientHandler<C> for XimHandler {
fn handle_connect(&mut self, client: &mut C) -> Result<(), ClientError> {
client.open("C")
}
fn handle_open(&mut self, client: &mut C, input_method_id: u16) -> Result<(), ClientError> {
self.im_id = input_method_id;
client.get_im_values(input_method_id, &[AttributeName::QueryInputStyle])
}
fn handle_get_im_values(
&mut self,
client: &mut C,
input_method_id: u16,
_attributes: AHashMap<AttributeName, Vec<u8>>,
) -> Result<(), ClientError> {
let ic_attributes = client
.build_ic_attributes()
.push(AttributeName::InputStyle, InputStyle::PREEDIT_CALLBACKS)
.push(AttributeName::ClientWindow, self.window)
.push(AttributeName::FocusWindow, self.window)
.build();
client.create_ic(input_method_id, ic_attributes)
}
fn handle_create_ic(
&mut self,
_client: &mut C,
_input_method_id: u16,
input_context_id: u16,
) -> Result<(), ClientError> {
self.connected = true;
self.ic_id = input_context_id;
Ok(())
}
fn handle_commit(
&mut self,
_client: &mut C,
_input_method_id: u16,
_input_context_id: u16,
text: &str,
) -> Result<(), ClientError> {
self.last_callback_event = Some(XimCallbackEvent::XimCommitEvent(
self.window,
String::from(text),
));
Ok(())
}
fn handle_forward_event(
&mut self,
_client: &mut C,
_input_method_id: u16,
_input_context_id: u16,
_flag: xim::ForwardEventFlag,
xev: C::XEvent,
) -> Result<(), ClientError> {
match xev.response_type {
x11rb::protocol::xproto::KEY_PRESS_EVENT => {
self.last_callback_event = Some(XimCallbackEvent::XimXEvent(Event::KeyPress(xev)));
}
x11rb::protocol::xproto::KEY_RELEASE_EVENT => {
self.last_callback_event =
Some(XimCallbackEvent::XimXEvent(Event::KeyRelease(xev)));
}
_ => {}
}
Ok(())
}
fn handle_close(&mut self, client: &mut C, _input_method_id: u16) -> Result<(), ClientError> {
client.disconnect()
}
fn handle_preedit_draw(
&mut self,
_client: &mut C,
_input_method_id: u16,
_input_context_id: u16,
_caret: i32,
_chg_first: i32,
_chg_len: i32,
_status: xim::PreeditDrawStatus,
preedit_string: &str,
_feedbacks: Vec<xim::Feedback>,
) -> Result<(), ClientError> {
// XIMReverse: 1, XIMPrimary: 8, XIMTertiary: 32: selected text
// XIMUnderline: 2, XIMSecondary: 16: underlined text
// XIMHighlight: 4: normal text
// XIMVisibleToForward: 64, XIMVisibleToBackward: 128, XIMVisibleCenter: 256: text align position
// XIMPrimary, XIMHighlight, XIMSecondary, XIMTertiary are not specified,
// but interchangeable as above
// Currently there's no way to support these.
self.last_callback_event = Some(XimCallbackEvent::XimPreeditEvent(
self.window,
String::from(preedit_string),
));
Ok(())
}
}

View File

@@ -0,0 +1,191 @@
//! Provides a [calloop] event source from [XDG Desktop Portal] events
//!
//! This module uses the [ashpd] crate
use ashpd::desktop::settings::{ColorScheme, Settings};
use calloop::channel::Channel;
use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
use smol::stream::StreamExt;
use gpui::{BackgroundExecutor, WindowAppearance};
pub enum Event {
WindowAppearance(WindowAppearance),
#[cfg_attr(feature = "x11", allow(dead_code))]
CursorTheme(String),
#[cfg_attr(feature = "x11", allow(dead_code))]
CursorSize(u32),
ButtonLayout(String),
}
pub struct XDPEventSource {
channel: Channel<Event>,
}
impl XDPEventSource {
pub fn new(executor: &BackgroundExecutor) -> Self {
let (sender, channel) = calloop::channel::channel();
let background = executor.clone();
executor
.spawn(async move {
let settings = Settings::new().await?;
if let Ok(initial_appearance) = settings.color_scheme().await {
sender.send(Event::WindowAppearance(
window_appearance_from_color_scheme(initial_appearance),
))?;
}
if let Ok(initial_theme) = settings
.read::<String>("org.gnome.desktop.interface", "cursor-theme")
.await
{
sender.send(Event::CursorTheme(initial_theme))?;
}
// If u32 is used here, it throws invalid type error
if let Ok(initial_size) = settings
.read::<i32>("org.gnome.desktop.interface", "cursor-size")
.await
{
sender.send(Event::CursorSize(initial_size as u32))?;
}
if let Ok(initial_layout) = settings
.read::<String>("org.gnome.desktop.wm.preferences", "button-layout")
.await
{
sender.send(Event::ButtonLayout(initial_layout))?;
}
if let Ok(mut cursor_theme_changed) = settings
.receive_setting_changed_with_args(
"org.gnome.desktop.interface",
"cursor-theme",
)
.await
{
let sender = sender.clone();
background
.spawn(async move {
while let Some(theme) = cursor_theme_changed.next().await {
let theme = theme?;
sender.send(Event::CursorTheme(theme))?;
}
anyhow::Ok(())
})
.detach();
}
if let Ok(mut cursor_size_changed) = settings
.receive_setting_changed_with_args::<i32>(
"org.gnome.desktop.interface",
"cursor-size",
)
.await
{
let sender = sender.clone();
background
.spawn(async move {
while let Some(size) = cursor_size_changed.next().await {
let size = size?;
sender.send(Event::CursorSize(size as u32))?;
}
anyhow::Ok(())
})
.detach();
}
if let Ok(mut button_layout_changed) = settings
.receive_setting_changed_with_args(
"org.gnome.desktop.wm.preferences",
"button-layout",
)
.await
{
let sender = sender.clone();
background
.spawn(async move {
while let Some(layout) = button_layout_changed.next().await {
let layout = layout?;
sender.send(Event::ButtonLayout(layout))?;
}
anyhow::Ok(())
})
.detach();
}
let mut appearance_changed = settings.receive_color_scheme_changed().await?;
while let Some(scheme) = appearance_changed.next().await {
sender.send(Event::WindowAppearance(
window_appearance_from_color_scheme(scheme),
))?;
}
anyhow::Ok(())
})
.detach();
Self { channel }
}
}
impl EventSource for XDPEventSource {
type Event = Event;
type Metadata = ();
type Ret = ();
type Error = anyhow::Error;
fn process_events<F>(
&mut self,
readiness: Readiness,
token: Token,
mut callback: F,
) -> Result<PostAction, Self::Error>
where
F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret,
{
self.channel.process_events(readiness, token, |evt, _| {
if let calloop::channel::Event::Msg(msg) = evt {
(callback)(msg, &mut ())
}
})?;
Ok(PostAction::Continue)
}
fn register(
&mut self,
poll: &mut Poll,
token_factory: &mut TokenFactory,
) -> calloop::Result<()> {
self.channel.register(poll, token_factory)?;
Ok(())
}
fn reregister(
&mut self,
poll: &mut Poll,
token_factory: &mut TokenFactory,
) -> calloop::Result<()> {
self.channel.reregister(poll, token_factory)?;
Ok(())
}
fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> {
self.channel.unregister(poll)?;
Ok(())
}
}
fn window_appearance_from_color_scheme(cs: ColorScheme) -> WindowAppearance {
match cs {
ColorScheme::PreferDark => WindowAppearance::Dark,
ColorScheme::PreferLight => WindowAppearance::Light,
ColorScheme::NoPreference => WindowAppearance::Light,
}
}