Skip to content
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ rust-version = "1.93"
[workspace]
members = ["cosmic-comp-config"]

[patch.'https://github.com/pop-os/cosmic-settings-daemon']
cosmic-settings-config = { git = "https://github.com/krakotay/cosmic-settings-daemon", branch = "caps-lock-bindings"}
cosmic-settings-daemon-config = { git = "https://github.com/krakotay/cosmic-settings-daemon", branch = "caps-lock-bindings"}

[dependencies]
anyhow = { version = "1.0.102", features = ["backtrace"] }
bitflags = "2.11.0"
Expand Down
21 changes: 10 additions & 11 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use crate::{
state::{BackendData, State},
utils::prelude::OutputExt,
wayland::protocols::{
output_configuration::OutputConfigurationState, workspace::WorkspaceUpdateGuard,
keyboard_layout::KeyboardLayoutState, output_configuration::OutputConfigurationState,
workspace::WorkspaceUpdateGuard,
},
};
use anyhow::Context;
Expand Down Expand Up @@ -807,19 +808,17 @@ fn config_changed(config: cosmic_config::Config, keys: Vec<String>, state: &mut
if let Err(err) = keyboard.set_xkb_config(state, xkb_config_to_wl(&value)) {
error!(?err, "Failed to load provided xkb config");
// TODO Revert to default?
}

// Press and release the numlock key to update modifiers.
if old_modifier_state.num_lock != keyboard.modifier_state().num_lock {
const NUMLOCK_SCANCODE: u32 = 69;
change_modifier_state(&keyboard, NUMLOCK_SCANCODE, state);
}
if old_modifier_state.caps_lock != keyboard.modifier_state().caps_lock {
const CAPSLOCK_SCANCODE: u32 = 58;
change_modifier_state(&keyboard, CAPSLOCK_SCANCODE, state);
} else if keyboard.set_modifier_state(old_modifier_state) != 0 {
keyboard.advertise_modifier_state(state);
<State as smithay::input::SeatHandler>::led_state_changed(
state,
&seat,
keyboard.led_state(),
);
}
}
}
KeyboardLayoutState::refresh(state);
state.common.config.cosmic_conf.xkb_config = value;
}
"keyboard_config" => {
Expand Down
87 changes: 86 additions & 1 deletion src/input/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use crate::{
},
utils::prelude::*,
wayland::{
handlers::xdg_activation::ActivationContext, protocols::workspace::WorkspaceUpdateGuard,
handlers::xdg_activation::ActivationContext,
protocols::{keyboard_layout::KeyboardLayoutState, workspace::WorkspaceUpdateGuard},
},
};
use cosmic_comp_config::{TileBehavior, workspace::WorkspaceLayout};
Expand All @@ -23,6 +24,7 @@ use smithay::{
#[cfg(not(feature = "debug"))]
use tracing::info;
use tracing::{error, warn};
use xkbcommon::xkb::Keysym;

use std::{os::unix::process::CommandExt, thread};

Expand All @@ -35,6 +37,24 @@ fn propagate_by_default(action: &shortcuts::Action) -> bool {
)
}

fn rotate_layouts(layouts: &mut String, variants: &mut String, active_layout: usize) {
let mut layout_entries = layouts.split(',').collect::<Vec<_>>();
let mut variant_entries = variants
.split(',')
.chain(std::iter::repeat(""))
.take(layout_entries.len())
.collect::<Vec<_>>();
if !layout_entries.is_empty() {
let offset = active_layout % layout_entries.len();
layout_entries.rotate_left(offset);
variant_entries.rotate_left(offset);
}
let new_layouts = layout_entries.join(",");
let new_variants = variant_entries.join(",");
*layouts = new_layouts;
*variants = new_variants;
}

impl State {
pub fn handle_action(
&mut self,
Expand All @@ -45,6 +65,30 @@ impl State {
pattern: shortcuts::Binding,
direction: Option<Direction>,
) {
if let Some(keyboard) = seat.get_keyboard() {
let mut modifiers = keyboard.modifier_state();
let clear_lock = match pattern.key {
Some(Keysym::Caps_Lock) if modifiers.caps_lock => {
modifiers.caps_lock = false;
true
}
Some(Keysym::Num_Lock) if modifiers.num_lock => {
modifiers.num_lock = false;
true
}
_ => false,
};

if clear_lock && keyboard.set_modifier_state(modifiers) != 0 {
keyboard.advertise_modifier_state(self);
<State as smithay::input::SeatHandler>::led_state_changed(
self,
seat,
keyboard.led_state(),
);
}
}

// TODO: Detect if started from login manager or tty, and only allow
// `Terminate` if it will return to login manager.
if self.common.shell.read().session_lock.is_some()
Expand Down Expand Up @@ -1013,6 +1057,31 @@ impl State {
self.common.shell.write().toggle_sticky_current(seat);
}

Action::System(shortcuts::action::System::InputSourceSwitch) => {
if let Some(keyboard) = seat.get_keyboard() {
let active_layout = keyboard.with_xkb_state(self, |mut context| {
context.cycle_next_layout();
context.xkb().lock().unwrap().active_layout().0 as usize
});
KeyboardLayoutState::refresh(self);

let mut xkb_config = self.common.config.cosmic_conf.xkb_config.clone();
rotate_layouts(
&mut xkb_config.layout,
&mut xkb_config.variant,
active_layout,
);
if let Err(err) = self
.common
.config
.cosmic_helper
.set("xkb_config", &xkb_config)
{
error!(?err, "Failed to persist active input source");
}
}
}

// Gets the configured command for a given system action.
Action::System(system) => {
if let Some(command) = self.common.config.system_actions.get(&system) {
Expand Down Expand Up @@ -1178,3 +1247,19 @@ fn to_previous_workspace(
workspace_state,
)
}

#[cfg(test)]
mod tests {
use super::rotate_layouts;

#[test]
fn rotating_layouts_keeps_variants_aligned() {
let mut layouts = "us,ru,de".to_string();
let mut variants = ",phonetic".to_string();

rotate_layouts(&mut layouts, &mut variants, 1);

assert_eq!(layouts, "ru,de,us");
assert_eq!(variants, "phonetic,,");
}
}
15 changes: 13 additions & 2 deletions src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use crate::{
image_copy_capture::{SessionHolder, cursor_capture_constraints},
xwayland_keyboard_grab::XWaylandGrabSeat,
},
wayland::protocols::keyboard_layout::KeyboardLayoutState,
};
use calloop::{
RegistrationToken,
Expand Down Expand Up @@ -304,6 +305,12 @@ impl State {
keyboard.modifier_state().num_lock;
}
}

if previous_modifiers.serialized.layout_effective
!= keyboard.modifier_state().serialized.layout_effective
{
KeyboardLayoutState::refresh(self);
}
}
}

Expand Down Expand Up @@ -1633,6 +1640,8 @@ impl State {
let key_matches = |binding_key: Keysym| -> bool {
raw_syms.contains(&binding_key) || latin_sym.is_some_and(|sym| sym == binding_key)
};
let keycode_matches =
|binding_keycode: u32| -> bool { event.key_code().raw() == binding_keycode };

let mut shell = self.common.shell.write();

Expand Down Expand Up @@ -1919,6 +1928,7 @@ impl State {

// is this a released (triggered) modifier-only binding?
if binding.key.is_none()
&& binding.keycode.is_none()
&& event.state() == KeyState::Released
&& !cosmic_modifiers_eq_smithay(&binding.modifiers, modifiers)
&& modifiers_queue.take(binding)
Expand All @@ -1932,6 +1942,7 @@ impl State {

// could this potentially become a modifier-only binding?
if binding.key.is_none()
&& binding.keycode.is_none()
&& event.state() == KeyState::Pressed
&& cosmic_modifiers_eq_smithay(&binding.modifiers, modifiers)
{
Expand All @@ -1940,9 +1951,9 @@ impl State {
}

// is this a normal binding?
if binding.key.is_some()
if (binding.key.is_some_and(key_matches)
|| binding.keycode.is_some_and(keycode_matches))
&& event.state() == KeyState::Pressed
&& key_matches(binding.key.unwrap())
&& cosmic_modifiers_eq_smithay(&binding.modifiers, modifiers)
{
modifiers_queue.clear();
Expand Down
1 change: 1 addition & 0 deletions src/wayland/protocols/keyboard_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ where
handle.with_xkb_state(state, |mut context| {
context.set_layout(Layout(group));
});
KeyboardLayoutState::refresh(state);
}
}
zcosmic_keyboard_layout_v1::Request::Destroy => {}
Expand Down