diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-22 17:01:25 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-04-22 17:01:25 +0300 |
| commit | 4b482f37a1e233b9aca6bf99b12e8b4da3e5c05e (patch) | |
| tree | 69d1a56ad6335547eec9727691310cf96ebe9623 /src/app/input | |
| parent | 5bd939fbfc92a8073d0eac11dfb02a31ea638b15 (diff) | |
feat: add terminal-delivered client notifications
Diffstat (limited to 'src/app/input')
| -rw-r--r-- | src/app/input/mod.rs | 37 | ||||
| -rw-r--r-- | src/app/input/modal.rs | 12 | ||||
| -rw-r--r-- | src/app/input/overlays.rs | 81 | ||||
| -rw-r--r-- | src/app/input/settings.rs | 68 |
4 files changed, 79 insertions, 119 deletions
diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs index 2f069fea..9676da62 100644 --- a/src/app/input/mod.rs +++ b/src/app/input/mod.rs @@ -37,11 +37,11 @@ pub(crate) use self::{ handle_keybind_help_key, handle_rename_key, handle_resize_key, }, navigate::{handle_navigate_key, terminal_direct_navigation_action}, + settings::open_settings, }; use self::{ modal::{ - modal_action_from_key, ModalAction, ONBOARDING_NOTIFICATION_ACTIONS, - ONBOARDING_WELCOME_ACTIONS, RELEASE_NOTES_ACTIONS, + modal_action_from_key, ModalAction, ONBOARDING_WELCOME_ACTIONS, RELEASE_NOTES_ACTIONS, }, settings::SettingsAction, }; @@ -89,32 +89,11 @@ impl App { } pub(crate) fn handle_onboarding_key(&mut self, key: KeyEvent) { - match self.state.onboarding_step { - 0 => match key.code { - KeyCode::Right | KeyCode::Char('l') => { - self.state.onboarding_step = 1; - } - _ => match modal_action_from_key(&key, ONBOARDING_WELCOME_ACTIONS) { - Some(ModalAction::Continue) => self.state.onboarding_step = 1, - _ => {} - }, - }, - _ => match key.code { - KeyCode::Up | KeyCode::Char('k') => self.state.onboarding_list.move_prev(), - KeyCode::Down | KeyCode::Char('j') => self.state.onboarding_list.move_next(4), - KeyCode::Left | KeyCode::Char('h') => { - self.state.onboarding_step = 0; - } - KeyCode::Char(c) if ('1'..='4').contains(&c) => { - self.state - .onboarding_list - .select((c as usize) - ('1' as usize)); - } - _ => match modal_action_from_key(&key, ONBOARDING_NOTIFICATION_ACTIONS) { - Some(ModalAction::Back) => self.state.onboarding_step = 0, - Some(ModalAction::Save) => self.complete_onboarding(), - _ => {} - }, + match key.code { + KeyCode::Right | KeyCode::Char('l') => self.open_settings_from_onboarding(), + _ => match modal_action_from_key(&key, ONBOARDING_WELCOME_ACTIONS) { + Some(ModalAction::Continue) => self.open_settings_from_onboarding(), + _ => {} }, } } @@ -170,7 +149,7 @@ impl App { match action { SettingsAction::SaveTheme(name) => self.save_theme(&name), SettingsAction::SaveSound(enabled) => self.save_sound(enabled), - SettingsAction::SaveToast(enabled) => self.save_toast(enabled), + SettingsAction::SaveToastDelivery(delivery) => self.save_toast_delivery(delivery), } } diff --git a/src/app/input/modal.rs b/src/app/input/modal.rs index fe909326..5c3108f2 100644 --- a/src/app/input/modal.rs +++ b/src/app/input/modal.rs @@ -9,7 +9,6 @@ use crate::{ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum ModalAction { Continue, - Back, Save, Clear, Cancel, @@ -211,17 +210,6 @@ pub(super) const ONBOARDING_WELCOME_ACTIONS: &[ModalActionSpec<ModalAction>] = & bindings: &[ModalKeyBinding::Enter], }]; -pub(super) const ONBOARDING_NOTIFICATION_ACTIONS: &[ModalActionSpec<ModalAction>] = &[ - ModalActionSpec { - action: ModalAction::Back, - bindings: &[ModalKeyBinding::Esc], - }, - ModalActionSpec { - action: ModalAction::Save, - bindings: &[ModalKeyBinding::Enter], - }, -]; - pub(super) const RELEASE_NOTES_ACTIONS: &[ModalActionSpec<ModalAction>] = &[ModalActionSpec { action: ModalAction::Close, bindings: &[ModalKeyBinding::Enter, ModalKeyBinding::Esc], diff --git a/src/app/input/overlays.rs b/src/app/input/overlays.rs index 6475895e..b283847e 100644 --- a/src/app/input/overlays.rs +++ b/src/app/input/overlays.rs @@ -272,51 +272,17 @@ impl AppState { return; } - match self.onboarding_step { - 0 => { - let Some(inner) = self.onboarding_modal_inner(64, 16) else { - return; - }; - let actions = crate::ui::modal_stack_areas(inner, 2, 0, 1, 1) - .actions - .unwrap_or_default(); - let button = crate::ui::onboarding_welcome_continue_rect(actions); - if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) - && modal_action_from_buttons( - mouse.column, - mouse.row, - &[(button, ModalAction::Continue)], - ) == Some(ModalAction::Continue) - { - self.onboarding_step = 1; - } - } - _ => { - let Some(inner) = self.onboarding_modal_inner(56, 14) else { - return; - }; - let stack = crate::ui::modal_stack_areas(inner, 3, 0, 1, 1); - if mouse.row >= stack.content.y && mouse.row < stack.content.y + 4 { - self.onboarding_list - .select((mouse.row - stack.content.y) as usize); - return; - } - - let (back, save) = crate::ui::onboarding_notification_button_rects( - stack.actions.unwrap_or_default(), - ); - if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) { - match modal_action_from_buttons( - mouse.column, - mouse.row, - &[(back, ModalAction::Back), (save, ModalAction::Save)], - ) { - Some(ModalAction::Back) => self.onboarding_step = 0, - Some(ModalAction::Save) => self.request_complete_onboarding = true, - _ => {} - } - } - } + let Some(inner) = self.onboarding_modal_inner(64, 16) else { + return; + }; + let actions = crate::ui::modal_stack_areas(inner, 2, 0, 1, 1) + .actions + .unwrap_or_default(); + let button = crate::ui::onboarding_welcome_continue_rect(actions); + if modal_action_from_buttons(mouse.column, mouse.row, &[(button, ModalAction::Continue)]) + == Some(ModalAction::Continue) + { + self.request_complete_onboarding = true; } } @@ -453,31 +419,30 @@ mod tests { fn onboarding_hover_does_not_change_selection() { let mut app = app_for_mouse_test(); app.state.mode = Mode::Onboarding; - app.state.onboarding_step = 1; - app.state.onboarding_list.select(1); - let inner = app.state.onboarding_modal_inner(56, 14).unwrap(); - let content = crate::ui::modal_stack_areas(inner, 3, 0, 1, 1).content; + let inner = app.state.onboarding_modal_inner(64, 16).unwrap(); + let content = crate::ui::modal_stack_areas(inner, 2, 0, 1, 1).content; app.handle_mouse(mouse(MouseEventKind::Moved, content.x + 2, content.y)); - assert_eq!(app.state.onboarding_list.selected, 1); + assert!(!app.state.request_complete_onboarding); } #[test] - fn onboarding_click_selects_notification_option() { + fn onboarding_click_continue_requests_completion() { let mut app = app_for_mouse_test(); app.state.mode = Mode::Onboarding; - app.state.onboarding_step = 1; - app.state.onboarding_list.select(0); - let inner = app.state.onboarding_modal_inner(56, 14).unwrap(); - let content = crate::ui::modal_stack_areas(inner, 3, 0, 1, 1).content; + let inner = app.state.onboarding_modal_inner(64, 16).unwrap(); + let actions = crate::ui::modal_stack_areas(inner, 2, 0, 1, 1) + .actions + .unwrap(); + let continue_rect = crate::ui::onboarding_welcome_continue_rect(actions); app.handle_mouse(mouse( MouseEventKind::Down(MouseButton::Left), - content.x + 2, - content.y + 2, + continue_rect.x, + continue_rect.y, )); - assert_eq!(app.state.onboarding_list.selected, 2); + assert!(app.state.request_complete_onboarding); } } diff --git a/src/app/input/settings.rs b/src/app/input/settings.rs index 9433b7d8..8bf06dc6 100644 --- a/src/app/input/settings.rs +++ b/src/app/input/settings.rs @@ -1,16 +1,19 @@ use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind}; use ratatui::layout::Rect; -use crate::app::{ - state::{AppState, SettingsSection, THEME_NAMES}, - App, Mode, +use crate::{ + app::{ + state::{AppState, SettingsSection, THEME_NAMES}, + App, Mode, + }, + config::ToastDelivery, }; #[derive(Debug, Clone, PartialEq, Eq)] pub(super) enum SettingsAction { SaveTheme(String), SaveSound(bool), - SaveToast(bool), + SaveToastDelivery(ToastDelivery), } impl App { @@ -19,7 +22,7 @@ impl App { match action { SettingsAction::SaveTheme(name) => self.save_theme(&name), SettingsAction::SaveSound(enabled) => self.save_sound(enabled), - SettingsAction::SaveToast(enabled) => self.save_toast(enabled), + SettingsAction::SaveToastDelivery(delivery) => self.save_toast_delivery(delivery), } } } @@ -37,6 +40,22 @@ fn current_theme_index(theme_name: &str) -> usize { .unwrap_or(0) } +fn toast_delivery_index(delivery: ToastDelivery) -> usize { + match delivery { + ToastDelivery::Off => 0, + ToastDelivery::Herdr => 1, + ToastDelivery::Terminal => 2, + } +} + +fn toast_delivery_for_index(idx: usize) -> ToastDelivery { + match idx { + 0 => ToastDelivery::Off, + 1 => ToastDelivery::Herdr, + _ => ToastDelivery::Terminal, + } +} + fn preview_selected_theme(state: &mut AppState) { use crate::app::state::Palette; @@ -111,7 +130,7 @@ pub(super) fn update_settings_state(state: &mut AppState, key: KeyEvent) -> Opti } KeyCode::Tab | KeyCode::Right | KeyCode::Char('l') => { state.settings.section = SettingsSection::Toast; - state.settings.list.selected = usize::from(!state.toast_config.enabled); + state.settings.list.selected = toast_delivery_index(state.toast_delivery()); } KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => { state.settings.section = SettingsSection::Theme; @@ -123,13 +142,13 @@ pub(super) fn update_settings_state(state: &mut AppState, key: KeyEvent) -> Opti }, }, SettingsSection::Toast => match key.code { - KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => { - state.settings.list.selected = 1 - state.settings.list.selected.min(1); - } + KeyCode::Up | KeyCode::Char('k') => state.settings.list.move_prev(), + KeyCode::Down | KeyCode::Char('j') => state.settings.list.move_next(3), KeyCode::Enter | KeyCode::Char(' ') => { - let enabled = state.settings.list.selected == 0; - state.toast_config.enabled = enabled; - return Some(SettingsAction::SaveToast(enabled)); + let delivery = toast_delivery_for_index(state.settings.list.selected); + state.toast_config.delivery = delivery; + state.toast = None; + return Some(SettingsAction::SaveToastDelivery(delivery)); } KeyCode::BackTab | KeyCode::Left | KeyCode::Char('h') => { state.settings.section = SettingsSection::Sound; @@ -149,7 +168,7 @@ pub(super) fn update_settings_state(state: &mut AppState, key: KeyEvent) -> Opti None } -pub(super) fn open_settings(state: &mut AppState) { +pub(crate) fn open_settings(state: &mut AppState) { state.settings.original_palette = Some(state.palette.clone()); state.settings.original_theme = Some(state.theme_name.clone()); state.settings.section = SettingsSection::Theme; @@ -159,7 +178,7 @@ pub(super) fn open_settings(state: &mut AppState) { impl AppState { fn settings_popup_rect(&self) -> Rect { - crate::ui::centered_popup_rect(self.screen_rect(), 56, 20).unwrap_or_default() + crate::ui::centered_popup_rect(self.screen_rect(), 76, 22).unwrap_or_default() } fn settings_inner_rect(&self) -> Rect { @@ -212,14 +231,22 @@ impl AppState { let idx = scroll + (row - area.y) as usize; (idx < THEME_NAMES.len()).then_some(idx) } - SettingsSection::Sound | SettingsSection::Toast => { - let list_y = area.y + 2; + SettingsSection::Sound => { + let list_y = area.y + 3; if row >= list_y && row < list_y + 2 { Some((row - list_y) as usize) } else { None } } + SettingsSection::Toast => { + let list_y = area.y + 3; + if row >= list_y && row < list_y + 6 { + Some(((row - list_y) / 2) as usize) + } else { + None + } + } } } @@ -231,7 +258,7 @@ impl AppState { self.settings.list.select(match section { SettingsSection::Theme => current_theme_index(&self.theme_name), SettingsSection::Sound => usize::from(!self.sound_enabled()), - SettingsSection::Toast => usize::from(!self.toast_config.enabled), + SettingsSection::Toast => toast_delivery_index(self.toast_delivery()), }); return None; } @@ -248,9 +275,10 @@ impl AppState { Some(SettingsAction::SaveSound(enabled)) } SettingsSection::Toast => { - let enabled = idx == 0; - self.toast_config.enabled = enabled; - Some(SettingsAction::SaveToast(enabled)) + let delivery = toast_delivery_for_index(idx); + self.toast_config.delivery = delivery; + self.toast = None; + Some(SettingsAction::SaveToastDelivery(delivery)) } }; } |
