aboutsummaryrefslogtreecommitdiff
path: root/src/app/input
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-04-22 17:01:25 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-04-22 17:01:25 +0300
commit4b482f37a1e233b9aca6bf99b12e8b4da3e5c05e (patch)
tree69d1a56ad6335547eec9727691310cf96ebe9623 /src/app/input
parent5bd939fbfc92a8073d0eac11dfb02a31ea638b15 (diff)
feat: add terminal-delivered client notifications
Diffstat (limited to 'src/app/input')
-rw-r--r--src/app/input/mod.rs37
-rw-r--r--src/app/input/modal.rs12
-rw-r--r--src/app/input/overlays.rs81
-rw-r--r--src/app/input/settings.rs68
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))
}
};
}