use crate::{
model::{settings, state},
view::root::{self, Cydonia},
};
use bezel::gpui::{App, Global, Keystroke, SharedString, Task};
use futures::StreamExt;
use global_hotkey::{
GlobalHotKeyEvent, GlobalHotKeyManager, HotKeyState,
hotkey::{Code, HotKey, Modifiers},
};
use std::str::FromStr;
struct Held {
manager: Option<GlobalHotKeyManager>,
key: Option<HotKey>,
refused: Option<SharedString>,
_pump: Task<()>,
}
impl Global for Held {}
pub fn apply(chord: Option<&str>, cx: &mut App) {
if !cx.has_global::<Held>() {
install(cx);
}
let wanted = chord.and_then(translate);
let refused = match (chord, wanted.as_ref()) {
(Some(_), None) => Some(SharedString::from("Not a chord the system can hold.")),
_ => None,
};
let held = cx.global_mut::<Held>();
let Some(manager) = held.manager.as_ref() else {
held.refused = Some(SharedString::from("The system refused the shortcut."));
return;
};
if let Some(key) = held.key.take() {
let _ = manager.unregister(key);
}
held.refused = refused;
let Some(wanted) = wanted else {
return;
};
match manager.register(wanted) {
Ok(()) => held.key = Some(wanted),
Err(_) => held.refused = Some(SharedString::from("Another app is already holding it.")),
}
}
pub fn refused(cx: &App) -> Option<SharedString> {
cx.try_global::<Held>()?.refused.clone()
}
fn install(cx: &mut App) {
let (tx, mut rx) = futures::channel::mpsc::unbounded();
GlobalHotKeyEvent::set_event_handler(Some(move |event: GlobalHotKeyEvent| {
if event.state() == HotKeyState::Pressed {
let _ = tx.unbounded_send(());
}
}));
let pump = cx.spawn(async move |cx| {
while rx.next().await.is_some() {
cx.update(activate);
}
});
cx.set_global(Held {
manager: GlobalHotKeyManager::new().ok(),
key: None,
refused: None,
_pump: pump,
});
}
fn activate(cx: &mut App) {
cx.activate(true);
if let Some(handle) = cx
.windows()
.into_iter()
.find_map(|window| window.downcast::<Cydonia>())
{
let _ = handle.update(cx, |_, window, _| window.activate_window());
return;
}
let Ok(settings) = settings::load() else {
return;
};
let _ = root::open(settings, state::restore(), cx);
}
pub fn holdable(chord: &str) -> bool {
translate(chord).is_some()
}
fn translate(chord: &str) -> Option<HotKey> {
if chord.split_whitespace().count() != 1 {
return None;
}
let stroke = Keystroke::parse(chord.trim()).ok()?;
let mut modifiers = Modifiers::empty();
modifiers.set(Modifiers::CONTROL, stroke.modifiers.control);
modifiers.set(Modifiers::ALT, stroke.modifiers.alt);
modifiers.set(Modifiers::SHIFT, stroke.modifiers.shift);
modifiers.set(Modifiers::SUPER, stroke.modifiers.platform);
if modifiers.is_empty() {
return None;
}
Some(HotKey::new(Some(modifiers), code(&stroke.key)?))
}
fn code(key: &str) -> Option<Code> {
let named = match key {
"space" => "Space",
"enter" => "Enter",
"escape" => "Escape",
"tab" => "Tab",
"backspace" => "Backspace",
"delete" => "Delete",
"up" => "ArrowUp",
"down" => "ArrowDown",
"left" => "ArrowLeft",
"right" => "ArrowRight",
"home" => "Home",
"end" => "End",
"pageup" => "PageUp",
"pagedown" => "PageDown",
"insert" => "Insert",
"," => "Comma",
"." => "Period",
"/" => "Slash",
";" => "Semicolon",
"'" => "Quote",
"[" => "BracketLeft",
"]" => "BracketRight",
"\\" => "Backslash",
"-" => "Minus",
"=" => "Equal",
"`" => "Backquote",
key => return spelled(key),
};
Code::from_str(named).ok()
}
fn spelled(key: &str) -> Option<Code> {
let spelling = match key.as_bytes() {
[letter @ b'a'..=b'z'] => format!("Key{}", letter.to_ascii_uppercase() as char),
[digit @ b'0'..=b'9'] => format!("Digit{}", *digit as char),
[b'f', ..] if key[1..].parse::<u8>().is_ok() => key.to_uppercase(),
_ => return None,
};
Code::from_str(&spelling).ok()
}