aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/app')
-rw-r--r--src/app/api.rs8
-rw-r--r--src/app/mod.rs43
-rw-r--r--src/app/runtime.rs10
-rw-r--r--src/app/state.rs7
4 files changed, 68 insertions, 0 deletions
diff --git a/src/app/api.rs b/src/app/api.rs
index c51a1bb0..2488ce40 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -16,6 +16,7 @@ impl App {
pub(crate) fn handle_internal_event(&mut self, ev: AppEvent) {
if let AppEvent::ClipboardWrite { content } = ev {
crate::selection::write_osc52_bytes(&content);
+ self.show_clipboard_feedback();
return;
}
@@ -177,6 +178,13 @@ impl App {
self.shutdown_detached_terminal_runtimes();
}
+ pub(crate) fn show_clipboard_feedback(&mut self) {
+ self.state.copy_feedback = Some(crate::app::state::CopyFeedback {
+ message: "copied to clipboard".to_string(),
+ });
+ self.copy_feedback_deadline = Some(Instant::now() + super::COPY_FEEDBACK_DURATION);
+ }
+
fn restore_overlay_after_exit(&mut self, overlay: OverlayPaneState) {
for temp_file in &overlay.temp_files {
let _ = std::fs::remove_file(temp_file);
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 534a81d8..1c221be8 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -38,6 +38,7 @@ const SESSION_SAVE_DEBOUNCE: Duration = Duration::from_secs(5);
const SIDEBAR_DOUBLE_CLICK_WINDOW: Duration = Duration::from_millis(350);
const PANE_DOUBLE_CLICK_WINDOW: Duration = Duration::from_millis(350);
const PANE_COPY_HIGHLIGHT_DURATION: Duration = Duration::from_millis(500);
+const COPY_FEEDBACK_DURATION: Duration = Duration::from_secs(2);
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture},
@@ -93,6 +94,7 @@ pub struct App {
pub(crate) last_terminal_size: Option<(u16, u16)>,
pub(crate) config_diagnostic_deadline: Option<Instant>,
pub(crate) toast_deadline: Option<Instant>,
+ pub(crate) copy_feedback_deadline: Option<Instant>,
pub(crate) last_git_remote_status_refresh: Instant,
pub(crate) git_refresh_in_flight: bool,
pub(crate) last_sidebar_divider_click: Option<Instant>,
@@ -467,6 +469,7 @@ impl App {
update_dismissed: false,
config_diagnostic,
toast: None,
+ copy_feedback: None,
outer_terminal_focus: None,
prefix_code,
prefix_mods,
@@ -547,6 +550,7 @@ impl App {
Self {
config_diagnostic_deadline: None,
toast_deadline: None,
+ copy_feedback_deadline: None,
state,
terminal_runtimes: restored_terminal_runtimes,
event_tx,
@@ -1465,6 +1469,45 @@ mod tests {
}
#[test]
+ fn clipboard_write_event_shows_feedback_toast() {
+ let mut app = test_app();
+
+ app.handle_internal_event(AppEvent::ClipboardWrite {
+ content: b"copied".to_vec(),
+ });
+
+ assert!(app.state.toast.is_none());
+ let feedback = app.state.copy_feedback.as_ref().expect("copy feedback");
+ assert_eq!(feedback.message, "copied to clipboard");
+ assert!(app.copy_feedback_deadline.is_some());
+ }
+
+ #[test]
+ fn clipboard_feedback_does_not_replace_notification_toast() {
+ let mut app = test_app();
+ app.state.toast = Some(crate::app::state::ToastNotification {
+ kind: crate::app::state::ToastKind::NeedsAttention,
+ title: "pi needs attention".to_string(),
+ context: "background ยท 2".to_string(),
+ target: None,
+ });
+ let original_toast = app.state.toast.clone();
+
+ app.handle_internal_event(AppEvent::ClipboardWrite {
+ content: b"copied".to_vec(),
+ });
+
+ assert_eq!(app.state.toast, original_toast);
+ assert_eq!(
+ app.state
+ .copy_feedback
+ .as_ref()
+ .map(|feedback| feedback.message.as_str()),
+ Some("copied to clipboard")
+ );
+ }
+
+ #[test]
fn internal_event_drain_limits_work_per_tick() {
let mut app = test_app();
for i in 0..=APP_EVENT_DRAIN_LIMIT {
diff --git a/src/app/runtime.rs b/src/app/runtime.rs
index bbef7444..2e1f4d81 100644
--- a/src/app/runtime.rs
+++ b/src/app/runtime.rs
@@ -163,6 +163,15 @@ impl App {
}
if self
+ .copy_feedback_deadline
+ .is_some_and(|deadline| now >= deadline)
+ {
+ self.copy_feedback_deadline = None;
+ self.state.copy_feedback = None;
+ changed = true;
+ }
+
+ if self
.next_animation_tick
.is_some_and(|deadline| now >= deadline)
{
@@ -431,6 +440,7 @@ impl App {
include_resize_poll.then_some(self.next_resize_poll),
self.config_diagnostic_deadline,
self.toast_deadline,
+ self.copy_feedback_deadline,
self.next_animation_tick,
self.git_refresh_deadline(),
self.next_auto_update_check,
diff --git a/src/app/state.rs b/src/app/state.rs
index 5e6aeafd..de1db052 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -1001,6 +1001,11 @@ pub struct ToastNotification {
pub target: Option<ToastTarget>,
}
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct CopyFeedback {
+ pub message: String,
+}
+
pub struct ReleaseNotesState {
pub version: String,
pub body: String,
@@ -1103,6 +1108,7 @@ pub struct AppState {
pub update_dismissed: bool,
pub config_diagnostic: Option<String>,
pub toast: Option<ToastNotification>,
+ pub copy_feedback: Option<CopyFeedback>,
/// Last reported focus state for the outer terminal hosting herdr.
/// None means unsupported or not yet reported, which preserves active-pane suppression.
pub outer_terminal_focus: Option<bool>,
@@ -1410,6 +1416,7 @@ impl AppState {
update_dismissed: false,
config_diagnostic: None,
toast: None,
+ copy_feedback: None,
outer_terminal_focus: None,
prefix_code: KeyCode::Char('b'),
prefix_mods: KeyModifiers::CONTROL,