Add configurable session-scoped global shortcut portal
This commit is contained in:
1 parent
e71cd88ff8
commit
c8ed8e6c30
16 files changed
+1497
-26
No files matched your search
@@ -485,6 +485,64 @@ pub enum BindingAction {
|
||||
}
|
||||
|
||||
impl Binding {
|
||||
pub(crate) fn portal_trigger(chord: &str) -> Result<Self, ConfigError> {
|
||||
if chord.len() > 256 {
|
||||
return Err(ConfigError::InvalidBinding("Shortcut is too long".into()));
|
||||
}
|
||||
let parts = chord.split('+').collect::<Vec<_>>();
|
||||
if parts
|
||||
.iter()
|
||||
.filter(|part| part.trim().eq_ignore_ascii_case("num"))
|
||||
.count()
|
||||
> 1
|
||||
{
|
||||
return Err(ConfigError::InvalidBinding("Duplicate NUM modifier".into()));
|
||||
}
|
||||
let chord = parts
|
||||
.into_iter()
|
||||
.filter(|part| !part.trim().eq_ignore_ascii_case("num"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("+");
|
||||
let (modifiers, key) = parse_chord(&chord)?;
|
||||
let symbol = parse_keysym(&key)?;
|
||||
if (modifiers.ctrl
|
||||
&& modifiers.alt
|
||||
&& matches!(
|
||||
symbol,
|
||||
keysyms::KEY_Escape | keysyms::KEY_F1..=keysyms::KEY_F12
|
||||
))
|
||||
|| (keysyms::KEY_XF86Switch_VT_1..=keysyms::KEY_XF86Switch_VT_12).contains(&symbol)
|
||||
{
|
||||
return Err(ConfigError::InvalidBinding(
|
||||
"Reserved system shortcut".into(),
|
||||
));
|
||||
}
|
||||
Ok(Self {
|
||||
modifiers,
|
||||
trigger: BindingTrigger::Keysym(symbol),
|
||||
action: BindingAction::None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn conflicts(&self, other: &Self, map: &xkb::Keymap) -> bool {
|
||||
if self.modifiers != other.modifiers {
|
||||
return false;
|
||||
}
|
||||
match (&self.trigger, &other.trigger) {
|
||||
(BindingTrigger::Keysym(left), BindingTrigger::Keysym(right)) => left == right,
|
||||
(BindingTrigger::Keysym(symbol), BindingTrigger::Physical(code))
|
||||
| (BindingTrigger::Physical(code), BindingTrigger::Keysym(symbol)) => {
|
||||
(0..map.num_layouts_for_key(*code)).any(|layout| {
|
||||
map.key_get_syms_by_level(*code, layout, 0)
|
||||
.iter()
|
||||
.any(|key| key.raw() == *symbol)
|
||||
})
|
||||
}
|
||||
(BindingTrigger::Physical(left), BindingTrigger::Physical(right)) => left == right,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(
|
||||
&self,
|
||||
keycode: Keycode,
|
||||
@@ -1197,7 +1255,7 @@ fn validate_commands(commands: &HashMap<String, Vec<String>>) -> Result<(), Conf
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn physical_keymap(input: &InputSettings) -> Result<xkb::Keymap, ConfigError> {
|
||||
pub(crate) fn physical_keymap(input: &InputSettings) -> Result<xkb::Keymap, ConfigError> {
|
||||
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
|
||||
let options = (!input.xkb_options.is_empty()).then(|| input.xkb_options.join(","));
|
||||
|
||||
|
||||
+17
-4
@@ -150,13 +150,15 @@ impl Ferese {
|
||||
SERIAL_COUNTER.next_serial(),
|
||||
Event::time(&event) as u32,
|
||||
|data, modifiers, keysym| {
|
||||
if state == KeyState::Released {
|
||||
data.portal_shortcuts.release(keycode, Event::time(&event));
|
||||
if data.intercepted_keys.remove(&keycode) {
|
||||
return FilterResult::Intercept(());
|
||||
}
|
||||
}
|
||||
if data.session_lock.active {
|
||||
return FilterResult::Forward;
|
||||
}
|
||||
if state == KeyState::Released && data.intercepted_keys.remove(&keycode) {
|
||||
return FilterResult::Intercept(());
|
||||
}
|
||||
|
||||
let symbol = keysym.modified_sym().raw();
|
||||
if data.overview.is_active()
|
||||
&& matches!(symbol, keysyms::KEY_Return | keysyms::KEY_KP_Enter)
|
||||
@@ -225,6 +227,17 @@ impl Ferese {
|
||||
.then(|| binding.action.clone())
|
||||
});
|
||||
let Some(action) = action else {
|
||||
if state == KeyState::Pressed
|
||||
&& data.portal_shortcuts.press(
|
||||
keycode,
|
||||
&raw_symbols,
|
||||
modifiers,
|
||||
Event::time(&event),
|
||||
)
|
||||
{
|
||||
data.intercepted_keys.insert(keycode);
|
||||
return FilterResult::Intercept(());
|
||||
}
|
||||
return FilterResult::Forward;
|
||||
};
|
||||
|
||||
|
||||
+65
-13
@@ -5,7 +5,7 @@ use std::os::fd::AsRawFd;
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
mpsc::{SyncSender, sync_channel},
|
||||
@@ -47,10 +47,28 @@ const DEADLINE_IDLE: Duration = Duration::from_secs(1);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct IpcCall {
|
||||
owner: u64,
|
||||
request: Request,
|
||||
response: SyncSender<Response>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum IpcEvent {
|
||||
Call(IpcCall),
|
||||
Closed(u64),
|
||||
}
|
||||
|
||||
struct IpcConnection {
|
||||
owner: u64,
|
||||
sender: channel::SyncSender<IpcEvent>,
|
||||
}
|
||||
|
||||
impl Drop for IpcConnection {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.sender.send(IpcEvent::Closed(self.owner));
|
||||
}
|
||||
}
|
||||
|
||||
struct ConnectionPermit {
|
||||
active: Arc<AtomicUsize>,
|
||||
}
|
||||
@@ -94,18 +112,25 @@ pub(crate) fn init(
|
||||
inode: metadata.ino(),
|
||||
};
|
||||
|
||||
let (sender, receiver): (channel::SyncSender<IpcCall>, channel::Channel<IpcCall>) =
|
||||
let (sender, receiver): (channel::SyncSender<IpcEvent>, channel::Channel<IpcEvent>) =
|
||||
channel::sync_channel(REQUEST_QUEUE_CAPACITY);
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(receiver, |event, _, state| {
|
||||
if let channel::Event::Msg(call) = event {
|
||||
if let channel::Event::Msg(event) = event {
|
||||
let call = match event {
|
||||
IpcEvent::Call(call) => call,
|
||||
IpcEvent::Closed(owner) => {
|
||||
state.portal_shortcuts.remove(owner);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if call.request.command == "screenshot" {
|
||||
// Deferred: this path answers the caller itself, exactly
|
||||
// once, whenever the request finishes or is terminated.
|
||||
state.start_screenshot(call.request, call.response);
|
||||
} else {
|
||||
let response = state.handle_ipc_request(call.request);
|
||||
let response = state.handle_ipc_request(call.owner, call.request);
|
||||
let _ = call.response.send(response);
|
||||
}
|
||||
}
|
||||
@@ -187,10 +212,11 @@ pub(crate) struct ScreenshotInit {
|
||||
|
||||
fn accept_connections(
|
||||
listener: UnixListener,
|
||||
sender: channel::SyncSender<IpcCall>,
|
||||
sender: channel::SyncSender<IpcEvent>,
|
||||
signal: LoopSignal,
|
||||
) {
|
||||
let active_connections = Arc::new(AtomicUsize::new(0));
|
||||
let connection_ids = AtomicU64::new(1);
|
||||
|
||||
loop {
|
||||
let (stream, _) = match listener.accept() {
|
||||
@@ -222,12 +248,13 @@ fn accept_connections(
|
||||
continue;
|
||||
};
|
||||
let sender = sender.clone();
|
||||
let owner = connection_ids.fetch_add(1, Ordering::Relaxed);
|
||||
let signal = signal.clone();
|
||||
if let Err(error) = thread::Builder::new()
|
||||
.name("ferese-ipc-client".to_owned())
|
||||
.spawn(move || {
|
||||
let _permit = permit;
|
||||
serve_connection(stream, sender, signal);
|
||||
serve_connection(stream, sender, signal, owner);
|
||||
})
|
||||
{
|
||||
tracing::warn!(%error, "could not start IPC connection worker");
|
||||
@@ -254,9 +281,14 @@ fn try_acquire_connection(active: &Arc<AtomicUsize>) -> Option<ConnectionPermit>
|
||||
|
||||
fn serve_connection(
|
||||
mut stream: UnixStream,
|
||||
sender: channel::SyncSender<IpcCall>,
|
||||
sender: channel::SyncSender<IpcEvent>,
|
||||
signal: LoopSignal,
|
||||
owner: u64,
|
||||
) {
|
||||
let _connection = IpcConnection {
|
||||
owner,
|
||||
sender: sender.clone(),
|
||||
};
|
||||
loop {
|
||||
let request: Request = match read_frame(&mut stream) {
|
||||
Ok(request) => request,
|
||||
@@ -277,7 +309,11 @@ fn serve_connection(
|
||||
};
|
||||
let (response, receiver) = sync_channel(1);
|
||||
let exit_requested = request.command == "exit";
|
||||
let call = IpcCall { request, response };
|
||||
let call = IpcEvent::Call(IpcCall {
|
||||
owner,
|
||||
request,
|
||||
response,
|
||||
});
|
||||
if sender.try_send(call).is_err() {
|
||||
tracing::warn!("disconnecting IPC client because the request queue is full");
|
||||
return;
|
||||
@@ -300,8 +336,8 @@ fn serve_connection(
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
fn handle_ipc_request(&mut self, request: Request) -> Response {
|
||||
if self.session_lock.active {
|
||||
fn handle_ipc_request(&mut self, owner: u64, request: Request) -> Response {
|
||||
if self.session_lock.active && request.command != "portal-shortcuts-poll" {
|
||||
return Response::error(
|
||||
request.id,
|
||||
"session_locked",
|
||||
@@ -312,14 +348,30 @@ impl Ferese {
|
||||
return Response::error(request.id, error.code, error.message);
|
||||
}
|
||||
|
||||
match self.dispatch_ipc_command(&request.command, &request.args) {
|
||||
match self.dispatch_ipc_command(owner, &request.command, &request.args) {
|
||||
Ok(result) => Response::success(request.id, result),
|
||||
Err(error) => Response::error(request.id, error.code, error.message),
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_ipc_command(&mut self, command: &str, args: &Value) -> Result<Value, CommandError> {
|
||||
fn dispatch_ipc_command(
|
||||
&mut self,
|
||||
owner: u64,
|
||||
command: &str,
|
||||
args: &Value,
|
||||
) -> Result<Value, CommandError> {
|
||||
match command {
|
||||
"portal-shortcuts-register" => {
|
||||
let shortcuts = serde_json::from_value(args.clone())
|
||||
.map_err(|error| CommandError::new("invalid_argument", error.to_string()))?;
|
||||
self.portal_shortcuts
|
||||
.register(owner, shortcuts, &self.bindings, &self.input_settings)
|
||||
.map_err(|error| CommandError::new("shortcut_conflict", error))?;
|
||||
return Ok(json!({}));
|
||||
}
|
||||
"portal-shortcuts-poll" => {
|
||||
return Ok(self.portal_shortcuts.poll(owner, self.session_lock.active));
|
||||
}
|
||||
"exit" => {} // The IPC worker stops the loop after writing the response.
|
||||
"request-logout" => self.request_logout_confirmation(),
|
||||
"reload-config" => self
|
||||
@@ -961,7 +1013,7 @@ mod tests {
|
||||
let (sender, _receiver) = channel::sync_channel(1);
|
||||
let event_loop = EventLoop::<()>::try_new().unwrap();
|
||||
let signal = event_loop.get_signal();
|
||||
let worker = thread::spawn(move || serve_connection(server, sender, signal));
|
||||
let worker = thread::spawn(move || serve_connection(server, sender, signal, 1));
|
||||
let invalid = b"not-json";
|
||||
|
||||
client
|
||||
|
||||
@@ -13,6 +13,7 @@ mod input;
|
||||
mod ipc;
|
||||
mod metrics;
|
||||
mod overview;
|
||||
mod portal_shortcuts;
|
||||
mod presentation;
|
||||
mod private_client;
|
||||
mod reload;
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use smithay::input::keyboard::{Keycode, ModifiersState};
|
||||
|
||||
use crate::config::{Binding, InputSettings};
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct Shortcut {
|
||||
pub id: String,
|
||||
pub trigger: String,
|
||||
#[serde(skip)]
|
||||
num_required: bool,
|
||||
}
|
||||
|
||||
struct Registration {
|
||||
shortcuts: Vec<(Shortcut, Binding)>,
|
||||
held: HashMap<Keycode, String>,
|
||||
events: VecDeque<Value>,
|
||||
latest: (u64, Instant),
|
||||
closed: bool,
|
||||
}
|
||||
|
||||
impl Registration {
|
||||
fn timestamp(&self) -> u64 {
|
||||
self.latest
|
||||
.0
|
||||
.saturating_add(self.latest.1.elapsed().as_millis() as u64)
|
||||
}
|
||||
|
||||
fn release_all(&mut self) {
|
||||
let timestamp = self.timestamp();
|
||||
for id in self.held.drain().map(|(_, id)| id) {
|
||||
self.events
|
||||
.push_back(json!({"id":id,"active":false,"timestamp":timestamp}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct PortalShortcuts(HashMap<u64, Registration>);
|
||||
|
||||
impl PortalShortcuts {
|
||||
pub(crate) fn register(
|
||||
&mut self,
|
||||
owner: u64,
|
||||
shortcuts: Vec<Shortcut>,
|
||||
configured: &[Binding],
|
||||
input: &InputSettings,
|
||||
) -> Result<(), String> {
|
||||
if shortcuts.len() > 64 || (!self.0.contains_key(&owner) && self.0.len() >= 16) {
|
||||
return Err("Shortcut limit reached".into());
|
||||
}
|
||||
let keymap = crate::config::physical_keymap(input).map_err(|error| error.to_string())?;
|
||||
if self
|
||||
.0
|
||||
.get(&owner)
|
||||
.is_some_and(|registration| registration.closed)
|
||||
{
|
||||
return Err("Shortcut session has ended".into());
|
||||
}
|
||||
let mut parsed: Vec<(Shortcut, Binding)> = Vec::new();
|
||||
for mut shortcut in shortcuts {
|
||||
shortcut.num_required = shortcut
|
||||
.trigger
|
||||
.split('+')
|
||||
.any(|part| part.trim().eq_ignore_ascii_case("num"));
|
||||
if shortcut.id.is_empty()
|
||||
|| shortcut.id.len() > 256
|
||||
|| shortcut.id.chars().any(char::is_control)
|
||||
{
|
||||
return Err("Invalid shortcut identifier".into());
|
||||
}
|
||||
let binding =
|
||||
Binding::portal_trigger(&shortcut.trigger).map_err(|error| error.to_string())?;
|
||||
if configured
|
||||
.iter()
|
||||
.any(|other| binding.conflicts(other, &keymap))
|
||||
|| parsed
|
||||
.iter()
|
||||
.any(|(other, key)| other.id == shortcut.id || binding.conflicts(key, &keymap))
|
||||
|| self.0.iter().any(|(connection, registration)| {
|
||||
*connection != owner
|
||||
&& registration
|
||||
.shortcuts
|
||||
.iter()
|
||||
.any(|(_, other)| binding.conflicts(other, &keymap))
|
||||
})
|
||||
{
|
||||
return Err(format!("Shortcut {} is already in use", shortcut.trigger));
|
||||
}
|
||||
parsed.push((shortcut, binding));
|
||||
}
|
||||
let registration = self.0.entry(owner).or_insert_with(|| Registration {
|
||||
shortcuts: Vec::new(),
|
||||
held: HashMap::new(),
|
||||
events: VecDeque::new(),
|
||||
latest: (0, Instant::now()),
|
||||
closed: false,
|
||||
});
|
||||
registration.release_all();
|
||||
registration.shortcuts = parsed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, owner: u64) {
|
||||
self.0.remove(&owner);
|
||||
}
|
||||
|
||||
pub(crate) fn reconcile(&mut self, configured: &[Binding], input: &InputSettings) {
|
||||
let Ok(keymap) = crate::config::physical_keymap(input) else {
|
||||
for registration in self.0.values_mut() {
|
||||
registration.closed = true;
|
||||
}
|
||||
return;
|
||||
};
|
||||
for registration in self.0.values_mut() {
|
||||
registration.shortcuts.retain(|(_, binding)| {
|
||||
!configured.iter().any(|key| binding.conflicts(key, &keymap))
|
||||
});
|
||||
let active_ids = registration
|
||||
.shortcuts
|
||||
.iter()
|
||||
.map(|(shortcut, _)| shortcut.id.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
let timestamp = registration.timestamp();
|
||||
registration.held.retain(|_, id| {
|
||||
if active_ids.contains(&id.as_str()) {
|
||||
true
|
||||
} else {
|
||||
registration
|
||||
.events
|
||||
.push_back(json!({"id":id,"active":false,"timestamp":timestamp}));
|
||||
false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn poll(&mut self, owner: u64, locked: bool) -> Value {
|
||||
let Some(registration) = self.0.get_mut(&owner) else {
|
||||
return json!({"closed":true});
|
||||
};
|
||||
if registration.closed {
|
||||
return json!({"closed":true});
|
||||
}
|
||||
if locked {
|
||||
registration.release_all();
|
||||
}
|
||||
json!({
|
||||
"shortcuts": registration.shortcuts.iter().map(|(shortcut, _)| json!({"id":shortcut.id,"trigger":shortcut.trigger})).collect::<Vec<_>>(),
|
||||
"events": registration.events.drain(..).collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn press(
|
||||
&mut self,
|
||||
key: Keycode,
|
||||
symbols: &[u32],
|
||||
modifiers: &ModifiersState,
|
||||
timestamp: u64,
|
||||
) -> bool {
|
||||
for registration in self.0.values_mut() {
|
||||
let Some((shortcut, _)) = registration.shortcuts.iter().find(|(shortcut, binding)| {
|
||||
(!shortcut.num_required || modifiers.num_lock)
|
||||
&& binding.matches(
|
||||
key,
|
||||
symbols,
|
||||
modifiers.logo,
|
||||
modifiers.ctrl,
|
||||
modifiers.alt,
|
||||
modifiers.shift,
|
||||
)
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
if !registration.held.contains_key(&key) {
|
||||
if registration.events.len() >= 256 {
|
||||
registration.closed = true;
|
||||
registration.shortcuts.clear();
|
||||
registration.held.clear();
|
||||
registration.events.clear();
|
||||
return false;
|
||||
}
|
||||
registration.latest = (timestamp, Instant::now());
|
||||
registration.held.insert(key, shortcut.id.clone());
|
||||
registration
|
||||
.events
|
||||
.push_back(json!({"id":shortcut.id,"active":true,"timestamp":timestamp}));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn release(&mut self, key: Keycode, timestamp: u64) {
|
||||
for registration in self.0.values_mut() {
|
||||
if let Some(id) = registration.held.remove(&key) {
|
||||
registration.latest = (timestamp, Instant::now());
|
||||
registration
|
||||
.events
|
||||
.push_back(json!({"id":id,"active":false,"timestamp":timestamp}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smithay::input::keyboard::keysyms;
|
||||
|
||||
fn shortcut(id: &str, trigger: &str) -> Shortcut {
|
||||
Shortcut {
|
||||
id: id.into(),
|
||||
trigger: trigger.into(),
|
||||
num_required: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registrations_are_atomic_and_connection_scoped() {
|
||||
let input = crate::config::Config::parse_source("")
|
||||
.unwrap()
|
||||
.input_settings()
|
||||
.unwrap();
|
||||
let configured = vec![Binding::portal_trigger("Super+q").unwrap()];
|
||||
let mut shortcuts = PortalShortcuts::default();
|
||||
shortcuts
|
||||
.register(1, vec![shortcut("one", "Ctrl+Alt+a")], &configured, &input)
|
||||
.unwrap();
|
||||
assert!(
|
||||
shortcuts
|
||||
.register(2, vec![shortcut("two", "Ctrl+Alt+a")], &configured, &input)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
shortcuts
|
||||
.register(1, vec![shortcut("one", "Super+q")], &configured, &input)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
shortcuts.poll(1, false)["shortcuts"][0]["trigger"],
|
||||
"Ctrl+Alt+a"
|
||||
);
|
||||
shortcuts.remove(1);
|
||||
assert_eq!(shortcuts.poll(1, false)["closed"], true);
|
||||
shortcuts
|
||||
.register(2, vec![shortcut("two", "Ctrl+Alt+a")], &configured, &input)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn releases_follow_the_pressed_key_and_lock_releases_held_shortcuts() {
|
||||
let mut shortcuts = PortalShortcuts::default();
|
||||
shortcuts
|
||||
.register(
|
||||
1,
|
||||
vec![shortcut("one", "Ctrl+Alt+a")],
|
||||
&[],
|
||||
&crate::config::Config::parse_source("")
|
||||
.unwrap()
|
||||
.input_settings()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let key = Keycode::new(38);
|
||||
let modifiers = ModifiersState {
|
||||
ctrl: true,
|
||||
alt: true,
|
||||
..ModifiersState::default()
|
||||
};
|
||||
assert!(shortcuts.press(key, &[keysyms::KEY_a], &modifiers, 10));
|
||||
assert!(shortcuts.press(key, &[keysyms::KEY_a], &modifiers, 11));
|
||||
shortcuts.release(key, 12);
|
||||
let result = shortcuts.poll(1, false);
|
||||
assert_eq!(result["events"].as_array().unwrap().len(), 2);
|
||||
assert_eq!(result["events"][1]["active"], false);
|
||||
assert!(shortcuts.press(key, &[keysyms::KEY_a], &modifiers, 13));
|
||||
assert_eq!(shortcuts.poll(1, true)["events"][1]["active"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configuration_changes_revoke_conflicting_shortcuts() {
|
||||
let mut shortcuts = PortalShortcuts::default();
|
||||
let input = crate::config::Config::parse_source("")
|
||||
.unwrap()
|
||||
.input_settings()
|
||||
.unwrap();
|
||||
shortcuts
|
||||
.register(1, vec![shortcut("one", "Ctrl+Alt+a")], &[], &input)
|
||||
.unwrap();
|
||||
shortcuts.reconcile(&[Binding::portal_trigger("Ctrl+Alt+a").unwrap()], &input);
|
||||
assert!(
|
||||
shortcuts.poll(1, false)["shortcuts"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
assert!(Binding::portal_trigger("Ctrl+Alt+Escape").is_err());
|
||||
assert!(Binding::portal_trigger("Ctrl+Alt+F3").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_overflow_closes_the_session_instead_of_leaving_held_keys() {
|
||||
let mut shortcuts = PortalShortcuts::default();
|
||||
let input = crate::config::Config::parse_source("")
|
||||
.unwrap()
|
||||
.input_settings()
|
||||
.unwrap();
|
||||
shortcuts
|
||||
.register(1, vec![shortcut("one", "Ctrl+Alt+a")], &[], &input)
|
||||
.unwrap();
|
||||
let key = Keycode::new(38);
|
||||
let modifiers = ModifiersState {
|
||||
ctrl: true,
|
||||
alt: true,
|
||||
..ModifiersState::default()
|
||||
};
|
||||
for timestamp in 0..200 {
|
||||
shortcuts.press(key, &[keysyms::KEY_a], &modifiers, timestamp * 2);
|
||||
shortcuts.release(key, timestamp * 2 + 1);
|
||||
}
|
||||
assert_eq!(shortcuts.poll(1, false)["closed"], true);
|
||||
assert!(!shortcuts.press(key, &[keysyms::KEY_a], &modifiers, 500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn physical_conflicts_include_secondary_layouts_and_num_modifier_is_honored() {
|
||||
let config = crate::config::Config::parse_source(
|
||||
"binding keys=\"Ctrl+AD01\" match=\"physical\" action=\"none\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
let mut input = config.input_settings().unwrap();
|
||||
input.xkb_layout = "us,fr".into();
|
||||
let configured = config.bindings(&input).unwrap();
|
||||
let mut shortcuts = PortalShortcuts::default();
|
||||
assert!(
|
||||
shortcuts
|
||||
.register(1, vec![shortcut("one", "Ctrl+a")], &configured, &input)
|
||||
.is_err()
|
||||
);
|
||||
shortcuts
|
||||
.register(1, vec![shortcut("one", "NUM+Ctrl+z")], &[], &input)
|
||||
.unwrap();
|
||||
let key = Keycode::new(52);
|
||||
let mut modifiers = ModifiersState {
|
||||
ctrl: true,
|
||||
..ModifiersState::default()
|
||||
};
|
||||
assert!(!shortcuts.press(key, &[keysyms::KEY_z], &modifiers, 1));
|
||||
modifiers.num_lock = true;
|
||||
assert!(shortcuts.press(key, &[keysyms::KEY_z], &modifiers, 2));
|
||||
}
|
||||
}
|
||||
@@ -257,6 +257,7 @@ pub struct Ferese {
|
||||
pub(crate) overview_scrims: HashMap<OutputId, crate::winit::OverviewScrim>,
|
||||
pub(crate) material_programs: HashMap<ErasedContextId, crate::winit::MaterialProgram>,
|
||||
pub(crate) material_buffers: HashMap<WlSurface, crate::winit::MaterialBuffers>,
|
||||
pub(crate) portal_shortcuts: crate::portal_shortcuts::PortalShortcuts,
|
||||
pub(crate) pending_logout: Option<u32>,
|
||||
pub(crate) logout_owner: Option<smithay::reexports::wayland_server::backend::ObjectId>,
|
||||
pub(crate) blur_programs: HashMap<ErasedContextId, crate::winit::BlurProgram>,
|
||||
@@ -480,6 +481,7 @@ impl Ferese {
|
||||
overview_scrims: HashMap::new(),
|
||||
material_programs: HashMap::new(),
|
||||
material_buffers: HashMap::new(),
|
||||
portal_shortcuts: crate::portal_shortcuts::PortalShortcuts::default(),
|
||||
pending_logout: None,
|
||||
logout_owner: None,
|
||||
blur_programs: HashMap::new(),
|
||||
@@ -626,6 +628,8 @@ impl Ferese {
|
||||
self.gap_config = config.gap_config;
|
||||
self.input_settings = config.input_settings;
|
||||
self.bindings = config.bindings;
|
||||
self.portal_shortcuts
|
||||
.reconcile(&self.bindings, &self.input_settings);
|
||||
let old_rules = std::mem::replace(&mut self.window_rules, config.window_rules);
|
||||
self.theme_settings = config.theme_settings;
|
||||
self.column_width_presets = config.column_width_presets;
|
||||
|
||||
Reference in new issue
Block a user