pub(crate) mod bindings;
pub(crate) mod modkeys;
pub(crate) mod spawn;
use crate::compositor::exit_confirm::exit_confirm_controller;
use crate::compositor::root::Halley;
use crate::compositor::screenshot::screenshot_controller;
use crate::input::ctx::InputCtx;
use std::time::Instant;
use smithay::input::keyboard::{FilterResult, KeysymHandle};
use smithay::utils::SERIAL_COUNTER;
use self::bindings::{
apply_bound_key, apply_compositor_action_release, compositor_action_allows_repeat,
compositor_binding_action, key_is_compositor_binding,
modifiers_keep_focus_cycle_session_active,
};
use self::modkeys::{is_modifier_keycode, update_mod_state};
use halley_config::CompositorBindingAction;
use halley_config::keybinds::key_name_to_evdev;
use smithay::backend::input::KeyState;
use crate::compositor::interaction::state::ClusterNamePromptRepeatAction;
#[inline]
fn now_millis_u32() -> u32 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| (d.as_millis() & 0xffff_ffff) as u32)
.unwrap_or(0)
}
fn flush_trapped_modal_release(st: &mut Halley, code: u32) {
let Some(keyboard) = st.platform.seat.get_keyboard() else {
st.input.interaction_state.modal_release_keys.remove(&code);
return;
};
let (_, mods_changed) =
keyboard.input_intercept::<(), _>(st, code.into(), KeyState::Released, |_, _, _| ());
keyboard.input_forward(
st,
code.into(),
KeyState::Released,
SERIAL_COUNTER.next_serial(),
now_millis_u32(),
mods_changed,
);
st.input.interaction_state.modal_release_keys.remove(&code);
}
#[inline]
fn cluster_mode_allows_keyboard_action(action: &CompositorBindingAction) -> bool {
matches!(
action,
CompositorBindingAction::ZoomIn
| CompositorBindingAction::ZoomOut
| CompositorBindingAction::ZoomReset
)
}
fn handle_focus_cycle_session_input<B: crate::backend::interface::BackendView>(
st: &mut Halley,
ctx: &InputCtx<'_, B>,
code: u32,
pressed: bool,
) {
let mods = ctx.mod_state.borrow().clone();
let matched_action = if pressed && !is_modifier_keycode(code) {
compositor_binding_action(st, code, &mods)
} else {
None
};
if let Some(keyboard) = st.platform.seat.get_keyboard() {
let serial = SERIAL_COUNTER.next_serial();
keyboard.input::<(), _>(
st,
code.into(),
if pressed {
KeyState::Pressed
} else {
KeyState::Released
},
serial,
now_millis_u32(),
|_, _, _| FilterResult::Intercept(()),
);
}
if pressed {
let escape = key_name_to_evdev("escape").map(|value| value + 8);
if Some(code) == escape {
crate::compositor::interaction::state::trap_modal_key_release(st, code);
if st.cancel_focus_cycle() {
ctx.backend.request_redraw();
}
return;
}
if let Some(CompositorBindingAction::FocusCycle(direction)) = matched_action
&& st.start_or_step_focus_cycle(direction, Instant::now())
{
ctx.backend.request_redraw();
}
return;
}
if !modifiers_keep_focus_cycle_session_active(st, &mods)
&& st.commit_focus_cycle(Instant::now())
{
ctx.backend.request_redraw();
}
}
fn cluster_prompt_input_char(keysym: &KeysymHandle<'_>) -> Option<char> {
let ch = keysym.modified_sym().key_char()?;
(!ch.is_control()).then_some(ch)
}
fn log_keyboard_binding_resolution(
st: &Halley,
code: u32,
pressed: bool,
mods: &crate::compositor::interaction::ModState,
matched_action: Option<&CompositorBindingAction>,
matched_launch: Option<&str>,
matched_binding: bool,
intercept: bool,
) {
let _ = (
st,
code,
pressed,
mods,
matched_action,
matched_launch,
matched_binding,
intercept,
);
}
fn latch_hover_keyboard_spawn_monitor(st: &mut Halley, pointer_screen: (f32, f32)) {
if st.runtime.tuning.input.focus_mode != halley_config::InputFocusMode::Hover {
return;
}
let Some(monitor) = st.monitor_for_screen(pointer_screen.0, pointer_screen.1) else {
return;
};
st.input.interaction_state.last_pointer_screen_global = Some(pointer_screen);
st.set_interaction_monitor(monitor.as_str());
st.model.spawn_state.pending_spawn_monitor = Some(monitor);
}
fn keyboard_has_client_focus(st: &Halley) -> bool {
st.platform
.seat
.get_keyboard()
.and_then(|keyboard| keyboard.current_focus())
.is_some()
}
pub(crate) fn handle_keyboard_input<B: crate::backend::interface::BackendView>(
st: &mut Halley,
ctx: &InputCtx<'_, B>,
code: u32,
pressed: bool,
) {
let exit_confirm_active = exit_confirm_controller(&*st).active();
update_mod_state(&mut ctx.mod_state.borrow_mut(), code, pressed);
if !pressed
&& st
.input
.interaction_state
.modal_release_keys
.contains(&code)
{
flush_trapped_modal_release(st, code);
return;
}
let exit_escape = key_name_to_evdev("escape").map(|code| code + 8);
let exit_return = key_name_to_evdev("return").map(|code| code + 8);
if exit_confirm_active {
if let Some(keyboard) = st.platform.seat.get_keyboard() {
let serial = SERIAL_COUNTER.next_serial();
keyboard.input::<(), _>(
st,
code.into(),
if pressed {
KeyState::Pressed
} else {
KeyState::Released
},
serial,
now_millis_u32(),
|_, _, _| FilterResult::Intercept(()),
);
}
if pressed {
if Some(code) == exit_escape {
crate::compositor::interaction::state::trap_modal_key_release(st, code);
exit_confirm_controller(&mut *st).clear();
ctx.backend.request_redraw();
} else if Some(code) == exit_return {
crate::compositor::interaction::state::trap_modal_key_release(st, code);
exit_confirm_controller(&mut *st).clear();
st.request_exit();
ctx.backend.request_redraw();
}
}
return;
}
if pressed {
let now_ms = st.now_ms(Instant::now());
if crate::compositor::interaction::state::note_typing_activity(st, now_ms) {
ctx.backend.request_redraw();
}
}
if screenshot_controller(&mut *st).screenshot_session_active() {
if let Some(keyboard) = st.platform.seat.get_keyboard() {
let serial = SERIAL_COUNTER.next_serial();
keyboard.input::<(), _>(
st,
code.into(),
if pressed {
KeyState::Pressed
} else {
KeyState::Released
},
serial,
now_millis_u32(),
|_, _, _| FilterResult::Intercept(()),
);
}
if pressed {
let escape = key_name_to_evdev("escape").map(|value| value + 8);
let enter = key_name_to_evdev("return").map(|value| value + 8);
let left = key_name_to_evdev("left").map(|value| value + 8);
let right = key_name_to_evdev("right").map(|value| value + 8);
let menu_mode = st
.input
.interaction_state
.screenshot_session
.as_ref()
.is_some_and(|session| session.mode == halley_api::CaptureMode::Menu);
if Some(code) == escape {
crate::compositor::interaction::state::trap_modal_key_release(st, code);
if menu_mode {
let _ = screenshot_controller(&mut *st).cancel_screenshot_session();
} else {
let _ = screenshot_controller(&mut *st).return_screenshot_session_to_menu();
}
ctx.backend.request_redraw();
} else if Some(code) == enter {
crate::compositor::interaction::state::trap_modal_key_release(st, code);
let _ = screenshot_controller(&mut *st).confirm_screenshot_session(Instant::now());
ctx.backend.request_redraw();
} else if Some(code) == left {
if menu_mode {
crate::compositor::interaction::state::trap_modal_key_release(st, code);
}
let _ = screenshot_controller(&mut *st).move_screenshot_menu_selection(-1);
ctx.backend.request_redraw();
} else if Some(code) == right {
if menu_mode {
crate::compositor::interaction::state::trap_modal_key_release(st, code);
}
let _ = screenshot_controller(&mut *st).move_screenshot_menu_selection(1);
ctx.backend.request_redraw();
}
}
return;
}
let prompt_monitor = st.model.monitor_state.current_monitor.clone();
if crate::compositor::clusters::system::cluster_system_controller(&*st)
.cluster_name_prompt_active_for_monitor(prompt_monitor.as_str())
{
let mut repeated_char = None;
if let Some(keyboard) = st.platform.seat.get_keyboard() {
let _ = keyboard.input::<(), _>(
st,
code.into(),
if pressed {
KeyState::Pressed
} else {
KeyState::Released
},
SERIAL_COUNTER.next_serial(),
now_millis_u32(),
|_, _, keysym| {
if pressed {
repeated_char = cluster_prompt_input_char(&keysym);
}
FilterResult::Intercept(())
},
);
}
if pressed {
let first_press = ctx.mod_state.borrow_mut().intercepted_keys.insert(code);
let left = key_name_to_evdev("left").map(|value| value + 8);
let right = key_name_to_evdev("right").map(|value| value + 8);
let delete = key_name_to_evdev("delete").map(|value| value + 8);
let backspace = key_name_to_evdev("backspace").map(|value| value + 8);
let escape = key_name_to_evdev("escape").map(|value| value + 8);
let enter = key_name_to_evdev("return").map(|value| value + 8);
let repeat_action = if Some(code) == left {
Some(ClusterNamePromptRepeatAction::MoveLeft)
} else if Some(code) == right {
Some(ClusterNamePromptRepeatAction::MoveRight)
} else if Some(code) == delete {
Some(ClusterNamePromptRepeatAction::Delete)
} else if Some(code) == backspace {
Some(ClusterNamePromptRepeatAction::Backspace)
} else {
repeated_char.map(ClusterNamePromptRepeatAction::Insert)
};
let handled = if Some(code) == escape {
crate::compositor::interaction::state::trap_modal_key_release(st, code);
crate::compositor::clusters::system::cluster_system_controller(&mut *st)
.cancel_cluster_name_prompt_for_monitor(prompt_monitor.as_str())
} else if Some(code) == enter {
crate::compositor::interaction::state::trap_modal_key_release(st, code);
crate::compositor::clusters::system::cluster_system_controller(&mut *st)
.confirm_cluster_name_prompt_for_monitor(
prompt_monitor.as_str(),
Instant::now(),
)
} else if !first_press && repeat_action.is_none() {
false
} else if Some(code) == left {
crate::compositor::clusters::system::cluster_system_controller(&mut *st)
.cluster_name_prompt_move_left_for_monitor(prompt_monitor.as_str())
} else if Some(code) == right {
crate::compositor::clusters::system::cluster_system_controller(&mut *st)
.cluster_name_prompt_move_right_for_monitor(prompt_monitor.as_str())
} else if Some(code) == backspace {
crate::compositor::clusters::system::cluster_system_controller(&mut *st)
.cluster_name_prompt_backspace_for_monitor(prompt_monitor.as_str())
} else if Some(code) == delete {
crate::compositor::clusters::system::cluster_system_controller(&mut *st)
.cluster_name_prompt_delete_for_monitor(prompt_monitor.as_str())
} else if let Some(ch) = repeated_char {
crate::compositor::clusters::system::cluster_system_controller(&mut *st)
.insert_cluster_name_prompt_char_for_monitor(prompt_monitor.as_str(), ch)
} else {
false
};
if handled && let Some(action) = repeat_action {
let now_ms = st.now_ms(Instant::now());
crate::compositor::clusters::system::cluster_system_controller(&mut *st)
.start_cluster_name_prompt_repeat_for_monitor(
prompt_monitor.as_str(),
code,
action,
now_ms,
);
}
if handled {
ctx.backend.request_redraw();
}
} else {
ctx.mod_state.borrow_mut().intercepted_keys.remove(&code);
crate::compositor::clusters::system::cluster_system_controller(&mut *st)
.stop_cluster_name_prompt_repeat_for_code(code);
}
return;
}
if st.focus_cycle_session_active() {
handle_focus_cycle_session_input(st, ctx, code, pressed);
return;
}
let cluster_escape = key_name_to_evdev("escape").map(|code| code + 8);
let cluster_return = key_name_to_evdev("return").map(|code| code + 8);
if st.cluster_mode_active() && (Some(code) == cluster_escape || Some(code) == cluster_return) {
if let Some(keyboard) = st.platform.seat.get_keyboard() {
let serial = SERIAL_COUNTER.next_serial();
keyboard.input::<(), _>(
st,
code.into(),
if pressed {
KeyState::Pressed
} else {
KeyState::Released
},
serial,
now_millis_u32(),
|_, _, _| FilterResult::Intercept(()),
);
}
if pressed {
let handled = if Some(code) == cluster_escape {
crate::compositor::interaction::state::trap_modal_key_release(st, code);
st.exit_cluster_mode()
} else {
crate::compositor::interaction::state::trap_modal_key_release(st, code);
st.confirm_cluster_mode(Instant::now())
};
if handled || Some(code) == cluster_return || Some(code) == cluster_escape {
ctx.backend.request_redraw();
}
}
return;
}
let mods = ctx.mod_state.borrow().clone();
let is_mod_key = is_modifier_keycode(code);
let layer_shell_keyboard_focus =
crate::compositor::monitor::layer_shell::keyboard_focus_is_layer_surface(st);
let session_lock_active = crate::protocol::wayland::session_lock::session_lock_active(st);
let compositor_shortcuts_blocked = layer_shell_keyboard_focus || session_lock_active;
let matched_action = if pressed && !is_mod_key && !compositor_shortcuts_blocked {
compositor_binding_action(st, code, &mods)
} else {
None
};
let matched_launch = if pressed && !is_mod_key && !compositor_shortcuts_blocked {
st.runtime
.tuning
.launch_bindings
.iter()
.find(|binding| {
bindings::input_matches_binding(code, binding.key)
&& self::modkeys::modifier_exact(&mods, binding.modifiers)
})
.map(|binding| binding.command.clone())
} else {
None
};
let matched_binding = matched_action.is_some()
|| matched_launch.is_some()
|| (pressed && !is_mod_key && key_is_compositor_binding(st, code, &mods));
let cluster_blocks_key = st.cluster_mode_active() && !is_mod_key;
let cluster_allowed_action = matched_action
.as_ref()
.is_some_and(cluster_mode_allows_keyboard_action);
if pressed
&& !is_mod_key
&& !matched_binding
&& !cluster_blocks_key
&& !compositor_shortcuts_blocked
&& !keyboard_has_client_focus(st)
&& let Some(fid) = st.last_input_surface_node_for_monitor(st.focused_monitor())
{
let open_monitors = st
.model
.cluster_state
.cluster_bloom_open
.keys()
.cloned()
.collect::<Vec<_>>();
for monitor in open_monitors {
let open_core = st
.cluster_bloom_for_monitor(monitor.as_str())
.and_then(|cid| st.model.field.cluster(cid).and_then(|cluster| cluster.core));
if open_core != Some(fid) {
let _ = st.close_cluster_bloom_for_monitor(monitor.as_str());
}
}
st.set_interaction_focus(Some(fid), 30_000, Instant::now());
}
let mut first_binding_press = false;
let mut repeat_binding_press = false;
let intercept = if is_mod_key {
false
} else if cluster_blocks_key {
if pressed && cluster_allowed_action {
let mut ms = ctx.mod_state.borrow_mut();
first_binding_press = ms.intercepted_keys.insert(code);
repeat_binding_press = !first_binding_press;
if first_binding_press && let Some(action) = matched_action.clone() {
ms.intercepted_compositor_actions.insert(code, action);
}
} else if !pressed {
let mut ms = ctx.mod_state.borrow_mut();
let intercepted = ms.intercepted_keys.remove(&code);
if intercepted {
if let Some(action) = ms.intercepted_compositor_actions.remove(&code)
&& apply_compositor_action_release(st, action)
{
ctx.backend.request_redraw();
}
} else {
ms.intercepted_compositor_actions.remove(&code);
}
}
true
} else if pressed {
if matched_binding {
let mut ms = ctx.mod_state.borrow_mut();
first_binding_press = ms.intercepted_keys.insert(code);
repeat_binding_press = !first_binding_press;
if first_binding_press && let Some(action) = matched_action.clone() {
ms.intercepted_compositor_actions.insert(code, action);
}
true
} else {
false
}
} else {
let mut ms = ctx.mod_state.borrow_mut();
let intercepted = ms.intercepted_keys.remove(&code);
if intercepted {
if let Some(action) = ms.intercepted_compositor_actions.remove(&code)
&& apply_compositor_action_release(st, action)
{
ctx.backend.request_redraw();
}
}
intercepted
};
if let Some(keyboard) = st.platform.seat.get_keyboard() {
let now = Instant::now();
let serial = SERIAL_COUNTER.next_serial();
crate::protocol::wayland::activation::note_input_serial(st, serial, st.now_ms(now));
let key_state = if pressed {
KeyState::Pressed
} else {
KeyState::Released
};
keyboard.input::<(), _>(
st,
code.into(),
key_state,
serial,
now_millis_u32(),
|_, _, _| {
if intercept {
FilterResult::Intercept(())
} else {
FilterResult::Forward
}
},
);
}
log_keyboard_binding_resolution(
st,
code,
pressed,
&mods,
matched_action.as_ref(),
matched_launch.as_deref(),
matched_binding,
intercept,
);
if pressed && matched_binding {
let should_apply = first_binding_press
|| (repeat_binding_press
&& matched_action
.as_ref()
.is_some_and(|action: &CompositorBindingAction| {
compositor_action_allows_repeat(action.clone())
}));
if should_apply {
let launch_like = matched_launch.is_some()
|| matches!(matched_action, Some(CompositorBindingAction::OpenTerminal));
if launch_like {
let pointer_screen = ctx.pointer_state.borrow().screen;
latch_hover_keyboard_spawn_monitor(st, pointer_screen);
}
if apply_bound_key(st, code, &mods, ctx.config_path, ctx.wayland_display) {
if matched_action.as_ref().is_some_and(|action| {
matches!(
action,
CompositorBindingAction::ZoomIn
| CompositorBindingAction::ZoomOut
| CompositorBindingAction::ZoomReset
)
}) {
ctx.backend
.request_output_redraw(st.model.monitor_state.current_monitor.as_str());
} else {
ctx.backend.request_redraw();
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hover_keyboard_launch_latches_pointer_monitor_over_stale_pending_monitor() {
let mut tuning = halley_config::RuntimeTuning::default();
tuning.input.focus_mode = halley_config::InputFocusMode::Hover;
tuning.tty_viewports = vec![
halley_config::ViewportOutputConfig {
connector: "left".to_string(),
enabled: true,
offset_x: 0,
offset_y: 0,
width: 800,
height: 600,
refresh_rate: None,
transform_degrees: 0,
vrr: halley_config::ViewportVrrMode::Off,
focus_ring: None,
},
halley_config::ViewportOutputConfig {
connector: "right".to_string(),
enabled: true,
offset_x: 800,
offset_y: 0,
width: 800,
height: 600,
refresh_rate: None,
transform_degrees: 0,
vrr: halley_config::ViewportVrrMode::Off,
focus_ring: None,
},
];
let dh = smithay::reexports::wayland_server::Display::<Halley>::new()
.expect("display")
.handle();
let mut state = Halley::new_for_test(&dh, tuning);
state.model.spawn_state.pending_spawn_monitor = Some("left".to_string());
latch_hover_keyboard_spawn_monitor(&mut state, (900.0, 120.0));
assert_eq!(
state.model.spawn_state.pending_spawn_monitor.as_deref(),
Some("right")
);
assert_eq!(
state.input.interaction_state.last_pointer_screen_global,
Some((900.0, 120.0))
);
assert_eq!(state.interaction_monitor(), "right");
}
}