Skip to content

Add TUI yank-to-clipboard support - #9

Merged
adambuchweitz merged 1 commit into
mainfrom
feature/tui-clipboard-yank
Apr 23, 2026
Merged

Add TUI yank-to-clipboard support#9
adambuchweitz merged 1 commit into
mainfrom
feature/tui-clipboard-yank

Conversation

@adambuchweitz

@adambuchweitz adambuchweitz commented Apr 23, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add a new y action in the TUI to copy the selected link URL to the system clipboard.
  • Introduce a clipboard abstraction plus platform-specific system clipboard support for macOS, Windows, and Linux.
  • Update the TUI footer/help text and add tests covering the new yank flow.
  • Bump the crate version to 0.1.7 and refresh dependencies for clipboard support.

Testing

  • cargo test
  • ./scripts/run-linters.sh
  • Added TUI integration coverage for y followed by q using a fake clipboard tool on Unix
  • Added unit coverage for TUI event handling and clipboard candidate selection

Summary by CodeRabbit

  • New Features

    • Added keyboard shortcut (y) to copy the highlighted link to the system clipboard; supports Linux clipboard helpers.
  • Documentation

    • Updated README with clipboard usage and Linux utility requirements.
    • Updated contributor guidance to run linters then tests.
  • Tests

    • Added TUI integration test covering yank (y) + quit behavior with external clipboard helpers.
  • Chores

    • Bumped version to 0.1.7 and added a dev dependency for PTY testing.

@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds system clipboard support and a TUI "y" yank action. Introduces a LinkClipboard trait, a SystemClipboard implementation that invokes OS clipboard utilities, threads a clipboard dependency through the run path and TUI, updates docs and tests, and bumps the crate version.

Changes

Cohort / File(s) Summary
Docs & Manifest
AGENTS.md, README.md, Cargo.toml
Added linting step to contributor guidance; documented y to copy highlighted URL and required Linux clipboard utilities; bumped package version to 0.1.7 and added portable-pty as a dev-dependency.
Clipboard Implementation
src/clipboard.rs
New LinkClipboard trait and SystemClipboard that attempts OS-specific clipboard helper commands, writes stdin, handles settle timeout/background wait, and aggregates errors; includes unit tests for candidate ordering.
Public API & Main
src/lib.rs, src/main.rs
Exported clipboard module; updated run signature to accept &dyn LinkClipboard; instantiated SystemClipboard in main and threaded clipboard into plinks::run.
TUI Events
src/tui/events.rs
Added EventResult::YankSelected; map normal-mode 'y' key to this event; added unit test for key mapping.
TUI Runtime & Logic
src/tui/mod.rs
Threaded clipboard into run/run_loop; refactored open logic into open_selected; added yank_selected handling that resolves selection and calls clipboard; added recording fakes and tests verifying open and yank behaviors and status messages.
UI Footer
src/tui/view.rs
Updated normal-mode footer help text to include y yank binding.
Tests
tests/cli.rs, tests/tui.rs
Updated tests to supply a NoopClipboard in CLI tests; added Unix-only PTY integration test that validates y then q behavior and that a long-running clipboard helper does not block exit.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant TUI as TUI Event Loop
    participant Clipboard as LinkClipboard
    participant Helper as OS Clipboard Helper<br/>(pbcopy/wl-copy/clip)
    participant Kernel as System Clipboard

    User->>TUI: Press 'y'
    TUI->>TUI: map → EventResult::YankSelected
    TUI->>Clipboard: copy_text(url)
    Clipboard->>Helper: spawn helper process
    Clipboard->>Helper: write URL to stdin
    Helper->>Kernel: place text on system clipboard
    Helper-->>Clipboard: exit or remain running
    Clipboard-->>TUI: return Result
    TUI->>User: show "Copied URL for '{link}'"
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.03% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add TUI yank-to-clipboard support' accurately and concisely captures the main change: introducing a new 'y' key action in the TUI to copy selected link URLs to the clipboard.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/tui-clipboard-yank

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/clipboard.rs (1)

188-225: Tests duplicate production candidate logic.

linux_candidates in tests mirrors implementation logic, which can let regressions slip if both change together. Prefer testing clipboard_command_candidates() via a small injectable helper (e.g., a pure linux_candidates_from_flags(...) used by both).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/clipboard.rs` around lines 188 - 225, The test duplicates the production
logic in linux_candidates, so extract the decision logic into a pure helper
(e.g., linux_candidates_from_flags(wayland: bool, x11: bool) ->
Vec<ClipboardCommand>) and have the existing linux_candidates call that helper;
update clipboard_command_candidates() (or whatever production entrypoint) to use
the helper and make tests call linux_candidates_from_flags instead of
reimplementing the rules, ensuring a single source of truth for candidate
generation.
README.md (1)

46-47: Clarify Linux clipboard prerequisites.

Consider explicitly stating that at least one of wl-copy, xclip, or xsel must be installed; otherwise yank will fail with an error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 46 - 47, Update the README TUI clipboard note to
explicitly state that plinks requires at least one of the clipboard utilities
(wl-copy, xclip, or xsel) to be installed on Linux for the "y" yank/copy action
to work; mention that if none are present the yank will fail with an error and
optionally suggest installing one of them (e.g., via the system package
manager). Include the utility names (wl-copy, xclip, xsel) and the behavior
(yank will fail if none installed) so users know the prerequisite.
src/tui/mod.rs (1)

261-265: Consider renaming take() to copied() for clarity.

The method returns a clone rather than draining the vector. While this works for the current tests (single assertion per test), the name take() suggests ownership transfer. A name like copied() or get_copied() would better reflect the behavior.

♻️ Suggested rename
     impl RecordingClipboard {
-        fn take(&self) -> Vec<String> {
+        fn copied(&self) -> Vec<String> {
             self.copied.borrow().clone()
         }
     }

And similarly for RecordingOpener:

     impl RecordingOpener {
-        fn take(&self) -> Vec<String> {
+        fn opened(&self) -> Vec<String> {
             self.opened.borrow().clone()
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tui/mod.rs` around lines 261 - 265, The method RecordingClipboard::take
currently returns a cloned Vec<String> but its name implies it drains/consumes
the data; rename it to something like copied() or get_copied() to reflect that
it clones rather than transfers ownership, and update all call sites
accordingly; do the same for RecordingOpener (rename its take method to copied()
or get_copied()) and adjust references to these methods to match the new names
to preserve behavior and clarity.
tests/tui.rs (1)

50-53: Consider increasing delays for CI stability.

The 300ms and 500ms sleeps before sending y and q may be tight on slower CI runners or under load. If flakiness is observed, consider increasing these or adding retry logic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/tui.rs` around lines 50 - 53, Increase the timing margins around the
interactive test writes to avoid CI flakiness by lengthening the sleeps or
adding retry/wait logic: locate the calls in tests/tui.rs where
thread::sleep(Duration::from_millis(300)) and
thread::sleep(Duration::from_millis(500)) occur before
writer.write_all(b"y").unwrap() and writer.write_all(b"q").unwrap(), and either
bump those durations (e.g., to 500–1000ms) or replace them with a small loop
that retries/waits for readiness before calling writer.write_all on the writer
variable to ensure stable input delivery on slow CI runners.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/clipboard.rs`:
- Around line 85-89: The timeout branch returns early and drops the spawned
clipboard Child without reaping it, causing zombies; before returning Ok(())
when Instant::now() >= deadline, take ownership of the Child (the process handle
created for clipboard helpers) and spawn a background thread to call
child.wait() (or child.wait().ok()) so the OS reaps the process, then detach
that thread and return; reference the existing deadline variable and the Child
instance used when launching xclip/wl-copy and ignore the wait result/errors in
the background thread.

---

Nitpick comments:
In `@README.md`:
- Around line 46-47: Update the README TUI clipboard note to explicitly state
that plinks requires at least one of the clipboard utilities (wl-copy, xclip, or
xsel) to be installed on Linux for the "y" yank/copy action to work; mention
that if none are present the yank will fail with an error and optionally suggest
installing one of them (e.g., via the system package manager). Include the
utility names (wl-copy, xclip, xsel) and the behavior (yank will fail if none
installed) so users know the prerequisite.

In `@src/clipboard.rs`:
- Around line 188-225: The test duplicates the production logic in
linux_candidates, so extract the decision logic into a pure helper (e.g.,
linux_candidates_from_flags(wayland: bool, x11: bool) -> Vec<ClipboardCommand>)
and have the existing linux_candidates call that helper; update
clipboard_command_candidates() (or whatever production entrypoint) to use the
helper and make tests call linux_candidates_from_flags instead of reimplementing
the rules, ensuring a single source of truth for candidate generation.

In `@src/tui/mod.rs`:
- Around line 261-265: The method RecordingClipboard::take currently returns a
cloned Vec<String> but its name implies it drains/consumes the data; rename it
to something like copied() or get_copied() to reflect that it clones rather than
transfers ownership, and update all call sites accordingly; do the same for
RecordingOpener (rename its take method to copied() or get_copied()) and adjust
references to these methods to match the new names to preserve behavior and
clarity.

In `@tests/tui.rs`:
- Around line 50-53: Increase the timing margins around the interactive test
writes to avoid CI flakiness by lengthening the sleeps or adding retry/wait
logic: locate the calls in tests/tui.rs where
thread::sleep(Duration::from_millis(300)) and
thread::sleep(Duration::from_millis(500)) occur before
writer.write_all(b"y").unwrap() and writer.write_all(b"q").unwrap(), and either
bump those durations (e.g., to 500–1000ms) or replace them with a small loop
that retries/waits for readiness before calling writer.write_all on the writer
variable to ensure stable input delivery on slow CI runners.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4b1f6117-3cc1-4baa-a4a5-a76837feaba2

📥 Commits

Reviewing files that changed from the base of the PR and between 6ac1a97 and 1ec5288.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • AGENTS.md
  • Cargo.toml
  • README.md
  • src/clipboard.rs
  • src/lib.rs
  • src/main.rs
  • src/tui/events.rs
  • src/tui/mod.rs
  • src/tui/view.rs
  • tests/cli.rs
  • tests/tui.rs

Comment thread src/clipboard.rs
- add a system clipboard abstraction with platform-specific implementations
- bind the y key to copy the selected link URL
- document the feature and extend CLI/TUI test coverage
@adambuchweitz
adambuchweitz force-pushed the feature/tui-clipboard-yank branch from 1ec5288 to 4da0625 Compare April 23, 2026 15:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/clipboard.rs (1)

228-249: Add a test for the Wayland+X11 simultaneous case.

linux_candidates_from_flags(true, true) currently has no direct test, so ordering between wl-copy, xclip, and xsel could regress silently.

Proposed test
+    #[cfg(all(unix, not(target_os = "macos")))]
+    #[test]
+    fn prefers_wayland_then_x11_when_both_session_hints_are_present() {
+        let candidates = linux_candidates_from_flags(true, true);
+        assert_eq!(
+            candidates,
+            vec![
+                ClipboardCommand {
+                    program: "wl-copy",
+                    args: &[],
+                },
+                ClipboardCommand {
+                    program: "xclip",
+                    args: &["-selection", "clipboard", "-in"],
+                },
+                ClipboardCommand {
+                    program: "xsel",
+                    args: &["--clipboard", "--input"],
+                }
+            ]
+        );
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/clipboard.rs` around lines 228 - 249, Add a unit test that verifies
linux_candidates_from_flags(true, true) returns the expected ordered candidates
(Wayland first then X11 tools) to prevent regressions: create a test (e.g.,
falls_back_to_wayland_then_x11_when_both_true) with the same #[cfg(all(unix,
not(target_os = "macos")))] attribute as the existing test, call
linux_candidates_from_flags(true, true), and assert equality against a vec of
ClipboardCommand entries in the exact order: program "wl-copy" with empty args,
then "xclip" with args ["-selection","clipboard","-in"], then "xsel" with args
["--clipboard","--input"] so ordering is enforced.
src/tui/mod.rs (1)

125-146: Consider extracting shared selected-entry resolution.

open_selected and yank_selected duplicate the same lookup/error path; a small helper would reduce drift risk.

Refactor sketch
+fn selected_entry<'a>(app: &'a App) -> Result<(String, &'a crate::config::Link)> {
+    let primary = app.selected_primary().context("No link selected")?;
+    let entry = app
+        .config
+        .links
+        .get(&primary)
+        .with_context(|| format!("selected link '{primary}' no longer exists"))?;
+    Ok((primary, entry))
+}
+
 fn open_selected(app: &mut App, opener: &dyn LinkOpener) -> Result<()> {
-    let primary = app.selected_primary().context("No link selected")?;
-    let entry = app
-        .config
-        .links
-        .get(&primary)
-        .with_context(|| format!("selected link '{primary}' no longer exists"))?;
+    let (primary, entry) = selected_entry(app)?;
     opener.open(&entry.url)?;
     app.set_info(format!("Opened '{primary}'"));
     Ok(())
 }
 
 fn yank_selected(app: &mut App, clipboard: &dyn LinkClipboard) -> Result<()> {
-    let primary = app.selected_primary().context("No link selected")?;
-    let entry = app
-        .config
-        .links
-        .get(&primary)
-        .with_context(|| format!("selected link '{primary}' no longer exists"))?;
+    let (primary, entry) = selected_entry(app)?;
     clipboard.copy_text(&entry.url)?;
     app.set_info(format!("Copied URL for '{primary}'"));
     Ok(())
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tui/mod.rs` around lines 125 - 146, Extract the duplicated
selection-and-lookup logic from open_selected and yank_selected into a small
helper (e.g. a function named resolve_selected_entry or get_selected_entry) that
takes &App and returns Result<&LinkEntry> or Result<(PrimaryKey, &LinkEntry)>
with the same error context ("No link selected" and "selected link '{primary}'
no longer exists"); update open_selected to call this helper then call
opener.open(&entry.url) and app.set_info(...) and update yank_selected to call
the same helper then clipboard.copy_text(&entry.url) and app.set_info(...),
preserving all existing error messages and Result propagation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/clipboard.rs`:
- Around line 228-249: Add a unit test that verifies
linux_candidates_from_flags(true, true) returns the expected ordered candidates
(Wayland first then X11 tools) to prevent regressions: create a test (e.g.,
falls_back_to_wayland_then_x11_when_both_true) with the same #[cfg(all(unix,
not(target_os = "macos")))] attribute as the existing test, call
linux_candidates_from_flags(true, true), and assert equality against a vec of
ClipboardCommand entries in the exact order: program "wl-copy" with empty args,
then "xclip" with args ["-selection","clipboard","-in"], then "xsel" with args
["--clipboard","--input"] so ordering is enforced.

In `@src/tui/mod.rs`:
- Around line 125-146: Extract the duplicated selection-and-lookup logic from
open_selected and yank_selected into a small helper (e.g. a function named
resolve_selected_entry or get_selected_entry) that takes &App and returns
Result<&LinkEntry> or Result<(PrimaryKey, &LinkEntry)> with the same error
context ("No link selected" and "selected link '{primary}' no longer exists");
update open_selected to call this helper then call opener.open(&entry.url) and
app.set_info(...) and update yank_selected to call the same helper then
clipboard.copy_text(&entry.url) and app.set_info(...), preserving all existing
error messages and Result propagation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c9f26f3-703f-4111-b8ae-c91bcafaf570

📥 Commits

Reviewing files that changed from the base of the PR and between 1ec5288 and 4da0625.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • AGENTS.md
  • Cargo.toml
  • README.md
  • src/clipboard.rs
  • src/lib.rs
  • src/main.rs
  • src/tui/events.rs
  • src/tui/mod.rs
  • src/tui/view.rs
  • tests/cli.rs
  • tests/tui.rs
✅ Files skipped from review due to trivial changes (4)
  • src/tui/view.rs
  • Cargo.toml
  • README.md
  • tests/cli.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • AGENTS.md
  • src/tui/events.rs
  • src/lib.rs
  • tests/tui.rs

@adambuchweitz
adambuchweitz merged commit aa35a6f into main Apr 23, 2026
5 checks passed
@adambuchweitz
adambuchweitz deleted the feature/tui-clipboard-yank branch April 23, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant