use crate::gui::message::Message;
use crate::gui::state::{AppState, Focus, GuiApp, SidebarMode};
use iced::{Subscription, event, keyboard, window};
use std::sync::atomic::{AtomicBool, Ordering};
pub static LAST_MOUSE_POS: std::sync::LazyLock<std::sync::RwLock<iced::Point>> =
std::sync::LazyLock::new(|| std::sync::RwLock::new(iced::Point::new(0.0, 0.0)));
static CMD_HELD: AtomicBool = AtomicBool::new(false);
pub fn cmd_held() -> bool {
CMD_HELD.load(Ordering::Relaxed)
}
pub static ACTIVE_FOCUS: std::sync::LazyLock<std::sync::RwLock<Focus>> =
std::sync::LazyLock::new(|| std::sync::RwLock::new(Focus::default()));
fn digit_sidebar_message(modified_key: &keyboard::Key) -> Option<Message> {
let s = match modified_key.as_ref() {
keyboard::Key::Character(s) => s,
_ => return None,
};
let mode = match s {
"1" => SidebarMode::Calendars,
"2" => SidebarMode::Categories,
"3" => SidebarMode::Locations,
"4" => SidebarMode::Goals,
"5" => SidebarMode::Journal,
_ => return None,
};
Some(Message::SidebarModeChanged(mode))
}
pub fn subscription(app: &GuiApp) -> Subscription<Message> {
let mut subs = Vec::new();
subs.push(crate::gui::async_ops::worker_subscription(app.ctx.clone()));
match app.state {
AppState::Active => {
subs.push(event::listen_with(handle_hotkey));
}
AppState::Help(_, _) => {
subs.push(event::listen_with(handle_help_hotkey));
}
AppState::Settings => {
subs.push(event::listen_with(handle_settings_hotkey));
}
AppState::Onboarding => {
subs.push(event::listen_with(handle_onboarding_hotkey));
}
_ => {}
}
subs.push(event::listen_with(|evt, _status, _window_id| match evt {
iced::Event::Window(window::Event::Resized(size)) => Some(Message::WindowResized(size)),
iced::Event::Window(window::Event::Focused) => Some(Message::WindowFocused(true)),
iced::Event::Window(window::Event::Unfocused) => Some(Message::WindowFocused(false)),
_ => None,
}));
if app.auto_refresh_interval_mins > 0 {
subs.push(
iced::time::every(std::time::Duration::from_secs(
app.auto_refresh_interval_mins as u64 * 60,
))
.map(|_| Message::Refresh),
);
}
let has_running_tasks = app.tasks.iter().any(|item| {
if let crate::store::TaskListItem::Task(t) = item {
t.last_started_at.is_some()
} else {
false
}
});
if has_running_tasks {
subs.push(iced::time::every(std::time::Duration::from_secs(60)).map(|_| Message::Tick));
}
Subscription::batch(subs)
}
fn handle_settings_hotkey(
evt: iced::Event,
_status: iced::event::Status,
_id: iced::window::Id,
) -> Option<Message> {
if let iced::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = evt {
match key.as_ref() {
keyboard::Key::Named(keyboard::key::Named::Tab) => {
Some(Message::CycleFocus(!modifiers.shift()))
}
keyboard::Key::Named(keyboard::key::Named::Escape) => Some(Message::CancelSettings),
_ => None,
}
} else {
None
}
}
fn handle_onboarding_hotkey(
evt: iced::Event,
_status: iced::event::Status,
_id: iced::window::Id,
) -> Option<Message> {
if let iced::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = evt {
match key.as_ref() {
keyboard::Key::Named(keyboard::key::Named::Tab) => {
Some(Message::CycleFocus(!modifiers.shift()))
}
_ => None,
}
} else {
None
}
}
fn handle_help_hotkey(
evt: iced::Event,
status: iced::event::Status,
_id: iced::window::Id,
) -> Option<Message> {
if status == iced::event::Status::Captured {
return None;
}
if let iced::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = evt {
match key.as_ref() {
keyboard::Key::Named(keyboard::key::Named::Escape) => Some(Message::CloseHelp),
keyboard::Key::Named(keyboard::key::Named::Tab) => {
Some(Message::SwitchHelpTab(!modifiers.shift()))
}
keyboard::Key::Named(keyboard::key::Named::ArrowRight) => {
Some(Message::SwitchHelpTab(true))
}
keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => {
Some(Message::SwitchHelpTab(false))
}
keyboard::Key::Character(s) if s == "q" || s == "?" || s == "/" => {
Some(Message::CloseHelp)
}
keyboard::Key::Character("l") => Some(Message::SwitchHelpTab(true)),
keyboard::Key::Character("h") => Some(Message::SwitchHelpTab(false)),
_ => None,
}
} else {
None
}
}
fn handle_hotkey(
evt: iced::Event,
status: iced::event::Status,
_id: iced::window::Id,
) -> Option<Message> {
use iced::keyboard::key::Named;
if let iced::Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = &evt {
CMD_HELD.store(
modifiers.control() || modifiers.command(),
Ordering::Relaxed,
);
}
if let iced::Event::Mouse(iced::mouse::Event::CursorMoved { position }) = &evt
&& let Ok(mut pos) = LAST_MOUSE_POS.write()
{
*pos = *position;
}
if let iced::Event::Mouse(iced::mouse::Event::WheelScrolled { delta }) = &evt
&& CMD_HELD.load(Ordering::Relaxed)
{
match delta {
iced::mouse::ScrollDelta::Lines { y, .. }
| iced::mouse::ScrollDelta::Pixels { y, .. } => {
if *y > 0.0 {
return Some(Message::ZoomIn);
} else if *y < 0.0 {
return Some(Message::ZoomOut);
}
}
}
}
if let iced::Event::Mouse(iced::mouse::Event::ButtonPressed(iced::mouse::Button::Middle)) = &evt
&& CMD_HELD.load(Ordering::Relaxed)
{
return Some(Message::ZoomReset);
}
if status == iced::event::Status::Captured {
if let iced::Event::Keyboard(keyboard::Event::KeyPressed {
key,
modifiers,
modified_key,
..
}) = &evt
{
if *key == keyboard::Key::Named(Named::Escape) {
return Some(Message::EscCaptured);
}
let is_cmd = modifiers.control() || modifiers.command();
if is_cmd && let keyboard::Key::Character(s) = key.as_ref() {
match s.to_lowercase().as_str() {
"s" => {
if modifiers.shift() {
return Some(Message::SaveTaskKeepEditing);
} else {
return Some(Message::SubmitTask);
}
}
"n" => return Some(Message::StartCreateWithDescription),
"e" => return Some(Message::KeyboardEditTree),
"," => return Some(Message::OpenSettings),
"z" => {
if modifiers.shift() {
return Some(Message::Redo);
} else {
return Some(Message::Undo);
}
}
"y" => return Some(Message::Redo),
_ => {}
}
}
if is_cmd && let Some(msg) = digit_sidebar_message(modified_key) {
return Some(msg);
}
if let Ok(focus) = ACTIVE_FOCUS.read()
&& (*focus == Focus::MainList || *focus == Focus::Sidebar)
{
match key.as_ref() {
keyboard::Key::Named(Named::ArrowDown) => return Some(Message::SelectNext),
keyboard::Key::Named(Named::ArrowUp) => return Some(Message::SelectPrev),
keyboard::Key::Named(Named::ArrowRight) => return Some(Message::ArrowRight),
keyboard::Key::Named(Named::ArrowLeft) => return Some(Message::ArrowLeft),
keyboard::Key::Named(Named::Enter) => return Some(Message::EnterPressed),
keyboard::Key::Named(Named::Space) => {
if modifiers.shift() {
return Some(Message::ShiftSpaceSelected);
} else {
return Some(Message::ToggleSelected);
}
}
_ => {}
}
}
}
return None;
}
if let iced::Event::Keyboard(keyboard::Event::KeyPressed {
key,
modifiers,
modified_key,
..
}) = evt
{
let is_cmd = modifiers.command() || modifiers.control();
if is_cmd {
if let keyboard::Key::Character(s) = key.as_ref() {
match s.to_lowercase().as_str() {
"+" | "=" => return Some(Message::ZoomIn),
"-" => return Some(Message::ZoomOut),
"0" => return Some(Message::ZoomReset),
"b" => return Some(Message::ToggleSidebar),
"d" => return Some(Message::KeyboardDuplicateTask),
"s" => {
if modifiers.shift() {
return Some(Message::SaveTaskKeepEditing);
} else {
return Some(Message::SubmitTask);
}
}
"n" => return Some(Message::StartCreateWithDescription),
"e" => {
if let Ok(focus) = ACTIVE_FOCUS.read()
&& *focus == Focus::AddTaskInput
{
return Some(Message::StartCreateWithDescription);
}
return Some(Message::KeyboardEditTree);
}
"m" => return Some(Message::ToggleEditorMaximize),
"," => return Some(Message::OpenSettings),
"p" => return Some(Message::ToggleSortStandardByPriorityToggle),
"z" => {
if modifiers.shift() {
return Some(Message::Redo);
} else {
return Some(Message::Undo);
}
}
"y" => return Some(Message::Redo),
_ => {}
}
} else if let keyboard::Key::Named(Named::Delete) = key.as_ref() {
return Some(Message::KeyboardDeleteTaskTree);
}
if let Some(msg) = digit_sidebar_message(&modified_key) {
return Some(msg);
}
}
if modifiers.command() || modifiers.control() || modifiers.alt() {
return None;
}
if let Some(msg) = digit_sidebar_message(&modified_key) {
return Some(msg);
}
match key.as_ref() {
keyboard::Key::Character(s) => {
let s_lower = s.to_lowercase();
match (s_lower.as_ref(), modifiers.shift()) {
("j", false) => Some(Message::SelectNext),
("k", false) => Some(Message::SelectPrev),
("e", false) => Some(Message::EditSelected),
("e", true) => Some(Message::EditSelectedDescription),
("s", false) => Some(Message::ToggleActiveSelected),
("s", true) => Some(Message::StopSelected),
("x", false) => Some(Message::CancelSelected),
("y", false) => Some(Message::YankSelected),
("y", true) => Some(Message::ToggleYankLock),
("f", false) => Some(Message::FocusSelected),
("c", false) => Some(Message::KeyboardLinkChild),
("c", true) => Some(Message::KeyboardCreateChild),
("t", false) => Some(Message::KeyboardAddSession),
("t", true) => Some(Message::KeyboardToggleSessions),
("b", false) => Some(Message::KeyboardAddDependency),
("l", false) => Some(Message::KeyboardAddRelation),
("l", true) => Some(Message::KeyboardToggleDetails),
("g", false) => Some(Message::KeyboardOpenLocations),
("o", false) => Some(Message::KeyboardOpenUrl),
("a", false) => Some(Message::FocusInput),
("h", true) => Some(Message::ToggleHideCompletedToggle),
("m", false) => Some(Message::CategoryMatchModeToggle),
("m", true) => Some(Message::MoveSelected),
("z", false) => Some(Message::KeyboardToggleTreeCollapse),
("q", false) => Some(Message::CloseWindow),
("w", false) => Some(Message::ToggleQuickFilter),
("r", false) => Some(Message::Refresh),
("r", true) => Some(Message::JumpToRandomTask),
("/", false) => Some(Message::FocusSearch),
("/", true) => Some(Message::OpenHelp(crate::help::HelpTab::Shortcuts)),
("?", _) => Some(Message::OpenHelp(crate::help::HelpTab::Shortcuts)),
_ => match s {
"*" => Some(Message::ClearAllFilters),
"+" | "=" => Some(Message::ChangePrioritySelected(1)),
"-" => Some(Message::ChangePrioritySelected(-1)),
"." | ">" => Some(Message::DemoteSelected),
"," | "<" => Some(Message::PromoteSelected),
_ => None,
},
}
}
keyboard::Key::Named(Named::ArrowDown) => Some(Message::SelectNext),
keyboard::Key::Named(Named::ArrowUp) => Some(Message::SelectPrev),
keyboard::Key::Named(Named::ArrowRight) => Some(Message::ArrowRight),
keyboard::Key::Named(Named::ArrowLeft) => Some(Message::ArrowLeft),
keyboard::Key::Named(Named::PageDown) => Some(Message::SelectNextPage),
keyboard::Key::Named(Named::PageUp) => Some(Message::SelectPrevPage),
keyboard::Key::Named(Named::Enter) => Some(Message::EnterPressed),
keyboard::Key::Named(Named::Space) => {
if modifiers.shift() {
Some(Message::ShiftSpaceSelected)
} else {
Some(Message::ToggleSelected)
}
}
keyboard::Key::Named(Named::Escape) => Some(Message::EscapePressed),
keyboard::Key::Named(Named::Delete) => {
Some(Message::DeleteSelected)
}
keyboard::Key::Named(Named::Tab) => Some(Message::CycleFocus(!modifiers.shift())),
_ => None,
}
} else {
None
}
}