From 1e85bde2100255d4066a29b9ea7d233bad2fa8d8 Mon Sep 17 00:00:00 2001 From: Ronen Hoffer Date: Mon, 3 Aug 2026 11:31:13 +0300 Subject: [PATCH 1/2] feat: copy open PR URL --- README.md | 4 ++- docs/KEYBINDINGS.md | 1 + src/handler.rs | 72 ++++++++++++++++++++++++++++++-------------- src/ui/help_popup.rs | 7 +++++ 4 files changed, 61 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 5f7792189..57934f900 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,8 @@ required token scopes, and troubleshooting. ### To your coding agent -`y` or `:clip` copies a structured markdown block to your clipboard. Each comment has a number +`y` or `:clip` copies a structured markdown block to your clipboard. In PR mode, `:copy-url` +copies the open pull request URL. Each comment has a number and a file/line anchor: ```markdown @@ -266,6 +267,7 @@ A first-session cheatsheet. Press `?` inside tuicr for the full reference. | `e` | Open focused file in `$EDITOR` | | `y` | Copy review to clipboard | | `:edit` | Open focused file in `$EDITOR` | +| `:copy-url` | Copy the open PR URL (PR mode) | | `:submit` | Push review to GitHub, GitLab, or Bitbucket | | `Tab` in `:` prompt | Complete or cycle commands | | `?` | Toggle full help | diff --git a/docs/KEYBINDINGS.md b/docs/KEYBINDINGS.md index c5a08911b..f41111007 100644 --- a/docs/KEYBINDINGS.md +++ b/docs/KEYBINDINGS.md @@ -166,6 +166,7 @@ In command mode, | `:e` (`:reload`) | Reload diff files | | `:edit` | Open focused file in `$EDITOR` | | `:clip` (`:export`) | Copy review to clipboard | +| `:copy-url` | Copy the open PR URL to clipboard (PR mode) | | `:diff` | Toggle diff view (unified / side-by-side) | | `:vim` / `:novim` (`:set vim` / `:set novim`) | Enable/toggle/disable vim modal editing in the comment box (overrides `comment_vim`) | | `:commits` | Select commits to review | diff --git a/src/handler.rs b/src/handler.rs index 955d0acc9..5624c1c1d 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -9,7 +9,7 @@ use crate::forge::remote_comments::PrCommentsVisibility; use crate::forge::submit::SubmitEvent; use crate::input::Action; use crate::model::{ClearScope, LineSide}; -use crate::output::{export_to_clipboard, generate_export_content}; +use crate::output::{copy_text_to_clipboard, export_to_clipboard, generate_export_content}; use crate::text_edit::{ delete_char_before, delete_word_before, next_char_boundary, prev_char_boundary, }; @@ -28,6 +28,7 @@ const COMMAND_SPECS: &[CommandSpec] = &[ CommandSpec::new(&["e", "reload"], CommandKind::Reload), CommandSpec::new(&["edit"], CommandKind::Edit), CommandSpec::new(&["clip", "export"], CommandKind::Export), + CommandSpec::new(&["copy-url"], CommandKind::CopyUrl), CommandSpec::new( &["clear"], CommandKind::Clear(ClearScope::CommentsAndReviewed), @@ -119,6 +120,7 @@ enum CommandKind { Reload, Edit, Export, + CopyUrl, Clear(ClearScope), Help, Version, @@ -357,6 +359,20 @@ fn handle_left_click(app: &mut App, pos: Position) { } } +fn handle_copy_pr_url(app: &mut App) { + let app::DiffSource::PullRequest(pr) = &app.diff_source else { + app.set_warning(":copy-url only applies in PR mode"); + return; + }; + let url = pr.url.clone(); + + match copy_text_to_clipboard(&url) { + Ok(true) => app.set_message("PR URL copied to clipboard (via terminal)"), + Ok(false) => app.set_message("PR URL copied to clipboard"), + Err(e) => app.set_warning(format!("Failed to copy PR URL: {e}")), + } +} + /// Export review: either to clipboard or set pending stdout output based on app.output_to_stdout. /// When output_to_stdout is true, stores the content and sets should_quit. fn handle_export(app: &mut App) { @@ -573,27 +589,6 @@ fn command_spec_for(cmd: &str) -> Option<&'static CommandSpec> { COMMAND_SPECS.iter().find(|spec| spec.names.contains(&cmd)) } -#[cfg(test)] -mod relative_line_number_command_tests { - use super::{CommandKind, command_spec_for}; - - #[test] - fn parses_relative_line_number_commands() { - assert_eq!( - command_spec_for("set relativenumber").map(|spec| spec.kind), - Some(CommandKind::SetRelativeLineNumbers(true)) - ); - assert_eq!( - command_spec_for("set norelativenumber").map(|spec| spec.kind), - Some(CommandKind::SetRelativeLineNumbers(false)) - ); - assert_eq!( - command_spec_for("set relativenumber!").map(|spec| spec.kind), - Some(CommandKind::ToggleRelativeLineNumbers) - ); - } -} - /// CommandCompleter computes command-buffer replacements without mutating App. struct CommandCompleter<'a> { /// Registry whose command names are exposed as completion candidates. @@ -802,6 +797,10 @@ fn dispatch_command(app: &mut App, kind: CommandKind) -> CommandAfterDispatch { handle_export(app); CommandAfterDispatch::ExitCommandMode } + CommandKind::CopyUrl => { + handle_copy_pr_url(app); + CommandAfterDispatch::ExitCommandMode + } CommandKind::Clear(scope) => { app.clear_comments(scope); CommandAfterDispatch::ExitCommandMode @@ -1691,3 +1690,32 @@ pub fn handle_submit_confirm_action(app: &mut App, action: Action) { _ => {} } } + +#[cfg(test)] +mod command_tests { + use super::{CommandKind, command_spec_for}; + + #[test] + fn parses_relative_line_number_commands() { + assert_eq!( + command_spec_for("set relativenumber").map(|spec| spec.kind), + Some(CommandKind::SetRelativeLineNumbers(true)) + ); + assert_eq!( + command_spec_for("set norelativenumber").map(|spec| spec.kind), + Some(CommandKind::SetRelativeLineNumbers(false)) + ); + assert_eq!( + command_spec_for("set relativenumber!").map(|spec| spec.kind), + Some(CommandKind::ToggleRelativeLineNumbers) + ); + } + + #[test] + fn parses_copy_url_command() { + assert_eq!( + command_spec_for("copy-url").map(|spec| spec.kind), + Some(CommandKind::CopyUrl) + ); + } +} diff --git a/src/ui/help_popup.rs b/src/ui/help_popup.rs index 3669e8edf..e74257d9c 100644 --- a/src/ui/help_popup.rs +++ b/src/ui/help_popup.rs @@ -639,6 +639,13 @@ pub fn render_help(frame: &mut Frame, app: &mut App) { ), Span::raw("Copy review to clipboard"), ]), + Line::from(vec![ + Span::styled( + " :copy-url ", + Style::default().add_modifier(Modifier::BOLD), + ), + Span::raw("Copy the open PR URL to clipboard (PR mode)"), + ]), Line::from(vec![ Span::styled( " :wrap ", From 56b78813a5b4789c3b2fa496a2694bbae9b7dd36 Mon Sep 17 00:00:00 2001 From: Almog Gavra Date: Wed, 5 Aug 2026 15:58:55 -0700 Subject: [PATCH 2/2] Apply suggestion from @agavra --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 57934f900..3cc4e70df 100644 --- a/README.md +++ b/README.md @@ -143,8 +143,7 @@ required token scopes, and troubleshooting. ### To your coding agent -`y` or `:clip` copies a structured markdown block to your clipboard. In PR mode, `:copy-url` -copies the open pull request URL. Each comment has a number +`y` or `:clip` copies a structured markdown block to your clipboard. Each comment has a number and a file/line anchor: ```markdown