Skip to content

Commit 61c02d8

Browse files
authored
KAN-208: Fix Shift+Y branch copy crash on Linux/Wayland (#167)
* chore: add Wayland/X11 clipboard dependencies * feat(tui): enable Wayland support with clipboard manager handoff * feat(tui): add reusable Banner component * refactor(tui): replace last_error with unified Banner system * docs: document Linux clipboard manager requirement * chore: update kanban todo * chore: add changeset * chore: cargo fmt
1 parent 4c77bc3 commit 61c02d8

File tree

12 files changed

+1304
-302
lines changed

12 files changed

+1304
-302
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
bump: patch
3+
---
4+
5+
- docs: document Linux clipboard manager requirement
6+
- refactor(tui): replace last_error with unified Banner system
7+
- feat(tui): add reusable Banner component
8+
- feat(tui): enable Wayland support with clipboard manager handoff
9+
- chore: add Wayland/X11 clipboard dependencies

Cargo.lock

Lines changed: 157 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ cargo install --path crates/kanban-cli
5454
nix run github:fulsomenko/kanban
5555
```
5656

57+
### Linux Clipboard Support
58+
59+
For clipboard operations (`y`/`Y` to copy branch names) to persist after exiting, you need a clipboard manager running:
60+
61+
- **Wayland**: `wl-clip-persist`, `cliphist`, `clipman`, or your DE's built-in manager
62+
- **X11**: Most desktop environments include one by default
63+
64+
Without a clipboard manager, copied content is lost when the app exits (this is a Linux platform limitation, not a bug).
65+
5766
## Quick Start
5867

5968
### TUI

crates/kanban-tui/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ uuid.workspace = true
2525
chrono.workspace = true
2626
async-trait.workspace = true
2727
tracing.workspace = true
28-
arboard = "3.4"
28+
arboard = { version = "3.4", features = ["wayland-data-control"] }
2929
pulldown-cmark = "0.13"
3030

3131
[dev-dependencies]

crates/kanban-tui/src/app.rs

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use crate::{
22
card_list::{CardList, CardListId},
33
card_list_component::{CardListComponent, CardListComponentConfig},
44
clipboard,
5-
components::generic_list::ListComponent,
5+
components::{generic_list::ListComponent, Banner},
66
editor::edit_in_external_editor,
77
events::{Event, EventHandler},
88
filters::FilterDialogState,
@@ -93,7 +93,7 @@ pub struct App {
9393
pub file_watcher: Option<kanban_persistence::FileWatcher>,
9494
pub save_worker_handle: Option<tokio::task::JoinHandle<()>>,
9595
pub save_completion_rx: Option<tokio::sync::mpsc::UnboundedReceiver<()>>,
96-
pub last_error: Option<(String, Instant)>,
96+
pub banner: Option<Banner>,
9797
}
9898

9999
#[derive(Debug, Clone, PartialEq)]
@@ -265,7 +265,7 @@ impl App {
265265
file_watcher: None,
266266
save_worker_handle: None,
267267
save_completion_rx,
268-
last_error: None,
268+
banner: None,
269269
};
270270

271271
if let Some(ref filename) = save_file {
@@ -316,16 +316,16 @@ impl App {
316316
self.push_mode(AppMode::Dialog(dialog));
317317
}
318318

319-
pub fn set_error(&mut self, message: String) {
320-
self.last_error = Some((message, Instant::now()));
319+
pub fn set_error(&mut self, message: impl Into<String>) {
320+
self.banner = Some(Banner::error(message));
321321
}
322322

323-
pub fn clear_error(&mut self) {
324-
self.last_error = None;
323+
pub fn set_success(&mut self, message: impl Into<String>) {
324+
self.banner = Some(Banner::success(message));
325325
}
326326

327-
pub fn should_clear_error(&self) -> bool {
328-
false
327+
pub fn clear_banner(&mut self) {
328+
self.banner = None;
329329
}
330330

331331
fn keycode_matches_binding_key(
@@ -459,9 +459,9 @@ impl App {
459459
use crossterm::event::KeyCode;
460460
let mut should_restart_events = false;
461461

462-
// Clear error on any key press
463-
if self.last_error.is_some() {
464-
self.clear_error();
462+
// Clear banner on any key press
463+
if self.banner.is_some() {
464+
self.clear_banner();
465465
return false;
466466
}
467467

@@ -1594,9 +1594,11 @@ impl App {
15941594
Event::Tick => {
15951595
self.handle_animation_tick();
15961596

1597-
// Auto-clear errors after 5 seconds
1598-
if self.should_clear_error() {
1599-
self.clear_error();
1597+
// Auto-clear banner after 3 seconds
1598+
if let Some(ref banner) = self.banner {
1599+
if banner.is_expired(std::time::Duration::from_secs(3)) {
1600+
self.clear_banner();
1601+
}
16001602
}
16011603

16021604
// Handle pending conflict resolution actions
@@ -1879,9 +1881,9 @@ impl App {
18791881
self.app_config.effective_default_card_prefix(),
18801882
);
18811883
if let Err(e) = clipboard::copy_to_clipboard(&output) {
1882-
tracing::error!("Failed to copy to clipboard: {}", e);
1884+
self.set_error(format!("Failed to copy: {}", e));
18831885
} else {
1884-
tracing::info!("Copied {}: {}", output_type, output);
1886+
self.set_success(format!("Copied {}", output_type));
18851887
}
18861888
}
18871889
}

crates/kanban-tui/src/clipboard.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,24 @@
11
use std::io;
22

33
pub fn copy_to_clipboard(text: &str) -> io::Result<()> {
4-
arboard::Clipboard::new()
5-
.and_then(|mut clipboard| clipboard.set_text(text))
6-
.map_err(io::Error::other)
4+
let mut clipboard = arboard::Clipboard::new().map_err(io::Error::other)?;
5+
6+
#[cfg(target_os = "linux")]
7+
{
8+
use arboard::SetExtLinux;
9+
use std::time::{Duration, Instant};
10+
11+
// wait_until gives clipboard managers time to take ownership
12+
// This prevents clipboard clearing when our app exits
13+
clipboard
14+
.set()
15+
.wait_until(Instant::now() + Duration::from_millis(250))
16+
.text(text.to_owned())
17+
.map_err(io::Error::other)
18+
}
19+
20+
#[cfg(not(target_os = "linux"))]
21+
{
22+
clipboard.set_text(text).map_err(io::Error::other)
23+
}
724
}

0 commit comments

Comments
 (0)