@@ -29,6 +29,7 @@ import {
2929 isJumpParentsForwardShortcut ,
3030} from "./lib/keyboard-shortcuts" ;
3131import { resolveTaskLogColor } from "./lib/logs" ;
32+ import { getSelectedTextByRow } from "./lib/selection-copy" ;
3233import {
3334 buildDisplayStatusByTaskKey ,
3435 canCancelRun ,
@@ -69,6 +70,26 @@ function App() {
6970 const [ isCancellingBeforeExit , setIsCancellingBeforeExit ] =
7071 createSignal ( false ) ;
7172 const [ errorMessage , setErrorMessage ] = createSignal < string | null > ( null ) ;
73+ const [ copyToastMessage , setCopyToastMessage ] = createSignal < string | null > (
74+ null
75+ ) ;
76+ let copyToastTimeoutId : ReturnType < typeof setTimeout > | null = null ;
77+ const showCopyToast = ( message : string ) => {
78+ setCopyToastMessage ( message ) ;
79+ if ( copyToastTimeoutId !== null ) {
80+ clearTimeout ( copyToastTimeoutId ) ;
81+ }
82+ copyToastTimeoutId = setTimeout ( ( ) => {
83+ setCopyToastMessage ( null ) ;
84+ copyToastTimeoutId = null ;
85+ } , 2000 ) ;
86+ } ;
87+ onCleanup ( ( ) => {
88+ if ( copyToastTimeoutId !== null ) {
89+ clearTimeout ( copyToastTimeoutId ) ;
90+ copyToastTimeoutId = null ;
91+ }
92+ } ) ;
7293
7394 const taskTree = createMemo ( ( ) => buildTaskTree ( tasks ( ) ) ) ;
7495 const taskRows = createMemo ( ( ) => flattenTaskRows ( taskTree ( ) ) ) ;
@@ -279,6 +300,46 @@ function App() {
279300 return key . name === "q" || ( key . ctrl && key . name === "c" ) ;
280301 }
281302
303+ function copySelectionToClipboard ( ) : boolean {
304+ const selection = renderer . getSelection ( ) ;
305+ if ( ! selection ?. isActive ) {
306+ return false ;
307+ }
308+ const text = getSelectedTextByRow ( selection ) ;
309+ if ( text . length === 0 ) {
310+ return false ;
311+ }
312+ const copied = renderer . copyToClipboardOSC52 ( text ) ;
313+ const lineCount = text . split ( "\n" ) . length ;
314+ const linesLabel = lineCount === 1 ? "1 line" : `${ lineCount } lines` ;
315+ showCopyToast (
316+ copied
317+ ? `Copied ${ linesLabel } to clipboard`
318+ : "Copy failed (terminal does not support OSC52)"
319+ ) ;
320+ renderer . clearSelection ( ) ;
321+ return true ;
322+ }
323+
324+ function handleCopySelectionKey ( key : {
325+ name : string ;
326+ ctrl ?: boolean ;
327+ meta ?: boolean ;
328+ super ?: boolean ;
329+ } ) {
330+ if ( key . name !== "c" ) {
331+ return false ;
332+ }
333+ // `ctrl` covers Ctrl+C on every platform.
334+ // `super`/`meta` covers Cmd+C in terminals that forward it (Kitty,
335+ // Ghostty, WezTerm with kitty keyboard protocol). Apple Terminal and
336+ // default iTerm2 swallow Cmd+C themselves before it ever reaches us.
337+ if ( ! ( key . ctrl || key . meta || key . super ) ) {
338+ return false ;
339+ }
340+ return copySelectionToClipboard ( ) ;
341+ }
342+
282343 async function cancelRunningTasksBeforeExit ( ) {
283344 if ( isCancellingBeforeExit ( ) ) {
284345 return ;
@@ -502,6 +563,9 @@ function App() {
502563 if ( handleQuitConfirmationKeys ( ) ) {
503564 return ;
504565 }
566+ if ( handleCopySelectionKey ( key ) ) {
567+ return ;
568+ }
505569 if ( handleQuitKey ( key ) ) {
506570 requestQuit ( ) ;
507571 return ;
@@ -663,6 +727,7 @@ function App() {
663727 canNavigateTasks = { canNavigateTasks ( ) }
664728 canRunOrRestart = { hasTaskSelection ( ) }
665729 canToggleLogMode = { canToggleLogMode ( ) }
730+ copyToastMessage = { copyToastMessage ( ) }
666731 errorMessage = { errorMessage ( ) }
667732 logMode = { logMode ( ) }
668733 runAction = { selectedRunAction ( ) }
@@ -672,7 +737,9 @@ function App() {
672737 isCancelling = { isCancellingBeforeExit ( ) }
673738 onConfirm = { ( action ) => {
674739 if ( action === "cancelAll" ) {
675- cancelRunningTasksBeforeExit ( ) . catch ( ( ) => quit ( ) ) ;
740+ cancelRunningTasksBeforeExit ( ) . catch ( ( ) =>
741+ quit ( )
742+ ) ;
676743 } else {
677744 quit ( ) ;
678745 }
@@ -693,7 +760,10 @@ async function main() {
693760
694761 cliOptions = mode . cliOptions ;
695762 cwd = cliOptions . cwd ;
696- render ( ( ) => < App /> ) ;
763+ // We handle Ctrl+C ourselves so it can copy a selection instead of always
764+ // exiting. Without this the renderer would call destroy() on Ctrl+C even
765+ // when our useKeyboard handler returns early.
766+ render ( ( ) => < App /> , { exitOnCtrlC : false } ) ;
697767}
698768
699769main ( ) . catch ( ( error : unknown ) => {
0 commit comments