From 9acfd8d8c18a5f0e9e32c5136c305edf2e440058 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 19 Jun 2026 20:21:57 -0700 Subject: [PATCH] input-sources: Use `cosmic-keyboard-layout-unstable-v1` https://github.com/pop-os/cosmic-protocols/pull/28 Continues to use `xkg_config` to get list of layouts, but the group is now changed by modifying the group while leaving the config unchanged. And changes to the group are now reflected in the applet. --- Cargo.lock | 2 + cosmic-applet-input-sources/Cargo.toml | 2 + ....system76.CosmicAppletInputSources.desktop | 1 + cosmic-applet-input-sources/src/lib.rs | 128 +++++++++----- cosmic-applet-input-sources/src/wayland.rs | 157 ++++++++++++++++++ 5 files changed, 247 insertions(+), 43 deletions(-) create mode 100644 cosmic-applet-input-sources/src/wayland.rs diff --git a/Cargo.lock b/Cargo.lock index ef0a8ba1..b3e84558 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1074,7 +1074,9 @@ dependencies = [ name = "cosmic-applet-input-sources" version = "1.0.15" dependencies = [ + "cosmic-client-toolkit", "cosmic-comp-config", + "futures", "i18n-embed", "i18n-embed-fl", "libcosmic", diff --git a/cosmic-applet-input-sources/Cargo.toml b/cosmic-applet-input-sources/Cargo.toml index 8fdb1dd9..94af4bb6 100644 --- a/cosmic-applet-input-sources/Cargo.toml +++ b/cosmic-applet-input-sources/Cargo.toml @@ -5,7 +5,9 @@ edition = "2024" license = "GPL-3.0-only" [dependencies] +cctk.workspace = true cosmic-comp-config = { git = "https://github.com/pop-os/cosmic-comp.git", rev = "5eb5af4" } +futures.workspace = true i18n-embed-fl.workspace = true i18n-embed.workspace = true libcosmic.workspace = true diff --git a/cosmic-applet-input-sources/data/com.system76.CosmicAppletInputSources.desktop b/cosmic-applet-input-sources/data/com.system76.CosmicAppletInputSources.desktop index ea0f667b..73aeccf0 100644 --- a/cosmic-applet-input-sources/data/com.system76.CosmicAppletInputSources.desktop +++ b/cosmic-applet-input-sources/data/com.system76.CosmicAppletInputSources.desktop @@ -12,4 +12,5 @@ NoDisplay=true X-CosmicApplet=true X-CosmicShrinkable=true X-CosmicHoverPopup=Auto +X-HostWaylandDisplay=true X-OverflowPriority=10 diff --git a/cosmic-applet-input-sources/src/lib.rs b/cosmic-applet-input-sources/src/lib.rs index fc7dcc32..4f7f1265 100644 --- a/cosmic-applet-input-sources/src/lib.rs +++ b/cosmic-applet-input-sources/src/lib.rs @@ -3,37 +3,60 @@ mod localize; -use cosmic::iced::{Alignment, Length}; +use cctk::{ + cosmic_protocols::keyboard_layout::v1::client::zcosmic_keyboard_layout_v1::ZcosmicKeyboardLayoutV1, + wayland_client::{Connection, Proxy, backend::Backend}, +}; use cosmic::{ app, app::Core, applet::{self}, - cosmic_config::{self, ConfigSet, CosmicConfigEntry}, + cosmic_config::{self, CosmicConfigEntry}, cosmic_theme::Spacing, iced::Subscription, + iced::core::window, iced::{ - Rectangle, Task, + self, Rectangle, Task, + event::wayland::{Event as WaylandEvent, OutputEvent}, platform_specific::shell::commands::popup::{destroy_popup, get_popup}, - widget::{column, row}, window::Id, }, - iced::{core::window, runtime::Appearance}, prelude::*, surface, theme, widget::{ self, autosize, rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription}, - space, }, }; use cosmic_comp_config::CosmicCompConfig; -use std::sync::LazyLock; +use std::{ + os::unix::{ + io::{FromRawFd, RawFd}, + net::UnixStream, + }, + sync::LazyLock, +}; use xkb_data::KeyboardLayout; +mod wayland; + static AUTOSIZE_MAIN_ID: LazyLock = LazyLock::new(|| widget::Id::new("autosize-main")); pub const ID: &str = "com.system76.CosmicAppletInputSources"; pub fn run() -> cosmic::iced::Result { + let socket = std::env::var("X_PRIVILEGED_WAYLAND_SOCKET") + .ok() + .and_then(|fd| { + fd.parse::() + .ok() + .map(|fd| unsafe { UnixStream::from_raw_fd(fd) }) + }); + let wayland_connection = if let Some(socket) = socket { + Some(Connection::from_socket(socket).unwrap()) + } else { + None + }; + localize::localize(); let layouts = match xkb_data::all_keyboard_layouts() { @@ -44,7 +67,7 @@ pub fn run() -> cosmic::iced::Result { } }; - let (comp_config_handler, comp_config) = + let comp_config = match cosmic_config::Config::new("com.system76.CosmicComp", CosmicCompConfig::VERSION) { Ok(config_handler) => { let config = match CosmicCompConfig::get_entry(&config_handler) { @@ -54,17 +77,17 @@ pub fn run() -> cosmic::iced::Result { config } }; - (Some(config_handler), config) + config } Err(err) => { tracing::error!("failed to create config handler: {}", err); - (None, CosmicCompConfig::default()) + CosmicCompConfig::default() } }; cosmic::applet::run::(Flags { + wayland_connection, comp_config, - comp_config_handler, layouts: layouts.layout_list.layout, }) } @@ -80,11 +103,13 @@ pub struct Window { core: Core, popup: Option, comp_config: CosmicCompConfig, - comp_config_handler: Option, layouts: Vec, active_layouts: Vec, rectangle_tracker: Option>, rectangle: Rectangle, + wayland_connection: Option, + keyboard_layout: Option, + current_layout: usize, } #[derive(Clone, Debug)] @@ -96,12 +121,14 @@ pub enum Message { KeyboardSettings, Surface(surface::Action), Rectangle(RectangleUpdate), + WaylandConnection(Backend), + Wayland(wayland::Event), } #[derive(Debug)] pub struct Flags { + wayland_connection: Option, pub comp_config: CosmicCompConfig, - pub comp_config_handler: Option, pub layouts: Vec, } @@ -122,7 +149,6 @@ impl cosmic::Application for Window { fn init(core: Core, flags: Self::Flags) -> (Self, app::Task) { let window = Window { - comp_config_handler: flags.comp_config_handler, layouts: flags.layouts, core, popup: None, @@ -130,6 +156,9 @@ impl cosmic::Application for Window { active_layouts: Vec::new(), rectangle_tracker: None, rectangle: Rectangle::default(), + wayland_connection: flags.wayland_connection, + keyboard_layout: None, + current_layout: 0, }; (window, Task::none()) } @@ -177,31 +206,10 @@ impl cosmic::Application for Window { tokio::spawn(cosmic::process::spawn(cmd)); } Message::SetActiveLayout(pos) => { - if pos == 0 { - return Task::none(); - } - - self.active_layouts.swap(0, pos); - let mut new_layout = String::new(); - let mut new_variant = String::new(); - - for layout in &self.active_layouts { - new_layout.push_str(&layout.layout); - new_layout.push(','); - new_variant.push_str(&layout.variant); - new_variant.push(','); - } - - let _excess_comma = new_layout.pop(); - let _excess_comma = new_variant.pop(); - - self.comp_config.xkb_config.layout = new_layout; - self.comp_config.xkb_config.variant = new_variant; - if let Some(comp_config_handler) = &self.comp_config_handler { - if let Err(err) = - comp_config_handler.set("xkb_config", &self.comp_config.xkb_config) - { - tracing::error!("Failed to set config 'xkb_config' {err}"); + if let Some(keyboard_layout) = &self.keyboard_layout { + keyboard_layout.set_group(pos as u32); + if let Some(backend) = keyboard_layout.backend().upgrade() { + let _ = backend.flush(); } } } @@ -218,13 +226,26 @@ impl cosmic::Application for Window { self.rectangle_tracker = Some(tracker); } }, + Message::WaylandConnection(backend) => { + if self.wayland_connection.is_none() { + self.wayland_connection = Some(Connection::from_backend(backend)); + } + } + Message::Wayland(w) => match w { + wayland::Event::KeyboardLayout(keyboard_layout) => { + self.keyboard_layout = Some(keyboard_layout); + } + wayland::Event::Group(group) => { + self.current_layout = group as usize; + } + }, } Task::none() } fn view(&self) -> Element<'_, Self::Message> { - let applet_text = if let Some(l) = self.active_layouts.first() { + let applet_text = if let Some(l) = self.active_layouts.get(self.current_layout) { if !l.variant.is_empty() { format!("{} ({})", l.layout, l.variant) } else { @@ -257,8 +278,13 @@ impl cosmic::Application for Window { let mut content_list = widget::column::with_capacity(4 + self.active_layouts.len()).padding([8, 0]); for (id, layout) in self.active_layouts.iter().enumerate() { + let font = if id == self.current_layout { + cosmic::font::bold() + } else { + cosmic::font::default() + }; let group = widget::column::with_capacity(2) - .push(widget::text::body(layout.description.as_str())) + .push(widget::text::body(layout.description.as_str()).font(font)) .push(widget::text::caption(layout.layout.as_str())); content_list = content_list .push(applet::menu_button(group).on_press(Message::SetActiveLayout(id))); @@ -279,7 +305,7 @@ impl cosmic::Application for Window { } fn subscription(&self) -> Subscription { - Subscription::batch(vec![ + let mut subscriptions = vec![ rectangle_tracker_subscription(0).map(|e| Message::Rectangle(e.1)), self.core .watch_config("com.system76.CosmicComp") @@ -293,7 +319,23 @@ impl cosmic::Application for Window { } Message::CompConfig(Box::new(update.config)) }), - ]) + ]; + subscriptions.push(if let Some(connection) = &self.wayland_connection { + wayland::subscription(connection.clone()).map(Message::Wayland) + } else { + iced::event::listen_with(|evt, _, _| match evt { + iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(evt)) => { + if let WaylandEvent::Output(OutputEvent::Created(_), output) = evt { + if let Some(backend) = output.backend().upgrade() { + return Some(Message::WaylandConnection(backend)); + } + } + None + } + _ => None, + }) + }); + Subscription::batch(subscriptions) } fn style(&self) -> Option { diff --git a/cosmic-applet-input-sources/src/wayland.rs b/cosmic-applet-input-sources/src/wayland.rs new file mode 100644 index 00000000..7bee2245 --- /dev/null +++ b/cosmic-applet-input-sources/src/wayland.rs @@ -0,0 +1,157 @@ +use cctk::{ + cosmic_protocols::keyboard_layout::v1::client::zcosmic_keyboard_layout_v1::ZcosmicKeyboardLayoutV1, + keyboard_layout::{KeyboardLayoutHandler, KeyboardLayoutState}, + sctk::{ + self, + registry::{ProvidesRegistryState, RegistryState}, + seat::{Capability, SeatHandler, SeatState}, + }, + wayland_client::{ + Connection, QueueHandle, delegate_noop, + globals::registry_queue_init, + protocol::{wl_keyboard, wl_seat}, + }, +}; +use cosmic::iced; +use futures::{SinkExt, channel::mpsc, executor::block_on}; +use std::{hash::Hash, thread}; + +#[derive(Clone, Debug)] +pub enum Event { + KeyboardLayout(ZcosmicKeyboardLayoutV1), + Group(usize), +} + +pub fn subscription(connection: Connection) -> iced::Subscription { + #[derive(Clone)] + struct WaylandSubscription(Connection); + impl Hash for WaylandSubscription { + fn hash(&self, state: &mut H) { + self.0.backend().display_id().hash(state); + } + } + iced::Subscription::run_with( + WaylandSubscription(connection), + |WaylandSubscription(connection)| { + let connection = connection.clone(); + iced::stream::channel(8, move |sender| async move { + thread::spawn(move || thread(connection, sender)); + }) + }, + ) +} + +struct Keyboard { + seat: wl_seat::WlSeat, + keyboard_layout: ZcosmicKeyboardLayoutV1, +} + +impl Drop for Keyboard { + fn drop(&mut self) { + self.keyboard_layout.destroy(); + } +} + +struct AppData { + seat_state: SeatState, + registry_state: RegistryState, + keyboard_layout_state: KeyboardLayoutState, + keyboard: Option, + sender: mpsc::Sender, +} + +impl KeyboardLayoutHandler for AppData { + fn group( + &mut self, + _conn: &Connection, + _qh: &QueueHandle, + _keyboard: &wl_keyboard::WlKeyboard, + _keyboard_layout: &ZcosmicKeyboardLayoutV1, + group: u32, + ) { + let _ = block_on(self.sender.send(Event::Group(group as usize))); + } +} + +impl ProvidesRegistryState for AppData { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + + sctk::registry_handlers![SeatState,]; +} + +impl SeatHandler for AppData { + fn seat_state(&mut self) -> &mut SeatState { + &mut self.seat_state + } + + fn new_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} + + fn new_capability( + &mut self, + _conn: &Connection, + qh: &QueueHandle, + seat: wl_seat::WlSeat, + capability: Capability, + ) { + if capability == Capability::Keyboard && self.keyboard.is_none() { + let keyboard = seat.get_keyboard(qh, ()); + let keyboard_layout = self + .keyboard_layout_state + .get_keyboard_layout(&keyboard, qh); + keyboard.release(); + + if let Some(keyboard_layout) = keyboard_layout { + let _ = block_on( + self.sender + .send(Event::KeyboardLayout(keyboard_layout.clone())), + ); + self.keyboard = Some(Keyboard { + seat, + keyboard_layout, + }); + } else { + keyboard.release(); + } + } + } + + fn remove_capability( + &mut self, + _conn: &Connection, + _: &QueueHandle, + seat: wl_seat::WlSeat, + _capability: Capability, + ) { + self.keyboard.take_if(|x| x.seat == seat); + } + + fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, seat: wl_seat::WlSeat) { + self.keyboard.take_if(|x| x.seat == seat); + } +} + +fn thread(conn: Connection, sender: mpsc::Sender) { + let (globals, mut event_queue) = registry_queue_init(&conn).unwrap(); + let qh = event_queue.handle(); + let registry_state = RegistryState::new(&globals); + let seat_state = SeatState::new(&globals, &qh); + let keyboard_layout_state = KeyboardLayoutState::new(®istry_state, &qh); + + let mut app_data = AppData { + seat_state, + registry_state, + keyboard_layout_state, + keyboard: None, + sender, + }; + loop { + event_queue.blocking_dispatch(&mut app_data).unwrap(); + } +} + +sctk::delegate_registry!(AppData); +sctk::delegate_seat!(AppData); +cctk::delegate_keyboard_layout!(AppData); +delegate_noop!(AppData: ignore wl_keyboard::WlKeyboard);