mirror of
https://github.com/stablyai/orca
synced 2026-04-21 14:17:16 +00:00
feat(terminal): add "Copy on Select" clipboard setting (default off) (#862)
Adds an opt-in terminal setting that automatically copies the current selection to the system clipboard as the user selects, mirroring X11 / gnome-terminal behavior. xterm.js has no native option for this, so the renderer hooks `onSelectionChange` per pane and writes via the existing clipboard IPC. Defaults to false so existing users keep the explicit Cmd/Ctrl+Shift+C copy flow. Closes #860
This commit is contained in:
parent
c47b651f2d
commit
c6f6300bcf
7 changed files with 97 additions and 0 deletions
|
|
@ -55,6 +55,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
|
|||
terminalDividerThicknessPx: 1,
|
||||
terminalRightClickToPaste: false,
|
||||
terminalFocusFollowsMouse: false,
|
||||
terminalClipboardOnSelect: false,
|
||||
setupScriptLaunchMode: 'split-vertical',
|
||||
terminalScrollbackBytes: 10_000_000,
|
||||
openLinksInApp: false,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings
|
|||
terminalDividerThicknessPx: 1,
|
||||
terminalRightClickToPaste: false,
|
||||
terminalFocusFollowsMouse: false,
|
||||
terminalClipboardOnSelect: false,
|
||||
setupScriptLaunchMode: 'split-vertical',
|
||||
terminalScrollbackBytes: 10_000_000,
|
||||
openLinksInApp: false,
|
||||
|
|
|
|||
|
|
@ -368,6 +368,50 @@ export function TerminalPane({
|
|||
/>
|
||||
</button>
|
||||
</SearchableSetting>
|
||||
|
||||
<SearchableSetting
|
||||
title="Copy on Select"
|
||||
description="Automatically copy terminal selections to the clipboard as soon as a selection is made."
|
||||
keywords={[
|
||||
'clipboard',
|
||||
'copy',
|
||||
'select',
|
||||
'selection',
|
||||
'auto',
|
||||
'automatic',
|
||||
'x11',
|
||||
'linux',
|
||||
'gnome',
|
||||
'paste'
|
||||
]}
|
||||
className="flex items-center justify-between gap-4 px-1 py-2"
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<Label>Copy on Select</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Automatically copy terminal selections to the clipboard as soon as a selection is
|
||||
made.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
role="switch"
|
||||
aria-checked={settings.terminalClipboardOnSelect}
|
||||
onClick={() =>
|
||||
updateSettings({
|
||||
terminalClipboardOnSelect: !settings.terminalClipboardOnSelect
|
||||
})
|
||||
}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent transition-colors ${
|
||||
settings.terminalClipboardOnSelect ? 'bg-foreground' : 'bg-muted-foreground/30'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none block size-3.5 rounded-full bg-background shadow-sm transition-transform ${
|
||||
settings.terminalClipboardOnSelect ? 'translate-x-4' : 'translate-x-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</SearchableSetting>
|
||||
</section>
|
||||
) : null,
|
||||
matchesSettingsSearch(searchQuery, TERMINAL_DARK_THEME_SEARCH_ENTRIES) ? (
|
||||
|
|
|
|||
|
|
@ -47,6 +47,23 @@ export const TERMINAL_PANE_STYLE_SEARCH_ENTRIES: SettingsSearchEntry[] = [
|
|||
description:
|
||||
"Hovering a terminal pane activates it without needing to click. Mirrors Ghostty's focus-follows-mouse setting. Selections and window switching stay safe.",
|
||||
keywords: ['focus', 'follows', 'mouse', 'hover', 'pane', 'ghostty', 'active']
|
||||
},
|
||||
{
|
||||
title: 'Copy on Select',
|
||||
description:
|
||||
'Automatically copy terminal selections to the clipboard as soon as a selection is made.',
|
||||
keywords: [
|
||||
'clipboard',
|
||||
'copy',
|
||||
'select',
|
||||
'selection',
|
||||
'auto',
|
||||
'automatic',
|
||||
'x11',
|
||||
'linux',
|
||||
'gnome',
|
||||
'paste'
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -160,6 +160,9 @@ export function useTerminalPaneLifecycle({
|
|||
const systemPrefersDarkRef = useRef(systemPrefersDark)
|
||||
systemPrefersDarkRef.current = systemPrefersDark
|
||||
const linkProviderDisposablesRef = useRef(new Map<number, IDisposable>())
|
||||
// Why: read settingsRef at fire time so toggling "copy on select" takes
|
||||
// effect without recreating panes.
|
||||
const selectionDisposablesRef = useRef(new Map<number, IDisposable>())
|
||||
|
||||
const applyAppearance = (manager: PaneManager): void => {
|
||||
const currentSettings = settingsRef.current
|
||||
|
|
@ -186,6 +189,7 @@ export function useTerminalPaneLifecycle({
|
|||
const panePtyBindings = panePtyBindingsRef.current
|
||||
const pendingWrites = pendingWritesRef.current
|
||||
const linkDisposables = linkProviderDisposablesRef.current
|
||||
const selectionDisposables = selectionDisposablesRef.current
|
||||
const worktreePath =
|
||||
useAppStore
|
||||
.getState()
|
||||
|
|
@ -273,6 +277,21 @@ export function useTerminalPaneLifecycle({
|
|||
createFilePathLinkProvider(pane.id, linkDeps, pane.linkTooltip, fileOpenLinkHint)
|
||||
)
|
||||
linkProviderDisposablesRef.current.set(pane.id, linkProviderDisposable)
|
||||
// Why: skip empty selections so clicking to deselect doesn't clobber
|
||||
// whatever the user last copied elsewhere.
|
||||
const selectionDisposable = pane.terminal.onSelectionChange(() => {
|
||||
if (!settingsRef.current?.terminalClipboardOnSelect) {
|
||||
return
|
||||
}
|
||||
const selection = pane.terminal.getSelection()
|
||||
if (!selection) {
|
||||
return
|
||||
}
|
||||
void window.api.ui.writeClipboardText(selection).catch(() => {
|
||||
/* ignore clipboard write failures */
|
||||
})
|
||||
})
|
||||
selectionDisposablesRef.current.set(pane.id, selectionDisposable)
|
||||
pane.terminal.options.linkHandler = {
|
||||
allowNonHttpProtocols: true,
|
||||
activate: (event, text) => handleOscLink(text, event as MouseEvent | undefined, linkDeps),
|
||||
|
|
@ -316,6 +335,11 @@ export function useTerminalPaneLifecycle({
|
|||
linkProviderDisposable.dispose()
|
||||
linkProviderDisposablesRef.current.delete(paneId)
|
||||
}
|
||||
const selectionDisposable = selectionDisposablesRef.current.get(paneId)
|
||||
if (selectionDisposable) {
|
||||
selectionDisposable.dispose()
|
||||
selectionDisposablesRef.current.delete(paneId)
|
||||
}
|
||||
const transport = paneTransportsRef.current.get(paneId)
|
||||
const panePtyBinding = panePtyBindings.get(paneId)
|
||||
if (panePtyBinding) {
|
||||
|
|
@ -555,6 +579,10 @@ export function useTerminalPaneLifecycle({
|
|||
disposable.dispose()
|
||||
}
|
||||
linkDisposables.clear()
|
||||
for (const disposable of selectionDisposables.values()) {
|
||||
disposable.dispose()
|
||||
}
|
||||
selectionDisposables.clear()
|
||||
for (const transport of paneTransports.values()) {
|
||||
if (tabStillExists && transport.getPtyId()) {
|
||||
// Why: moving a terminal tab between groups currently rehomes the
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ export function getDefaultSettings(homedir: string): GlobalSettings {
|
|||
// { ...defaults.settings, ...parsed.settings } merge, so enabling
|
||||
// focus-follows-mouse never happens unexpectedly.
|
||||
terminalFocusFollowsMouse: false,
|
||||
terminalClipboardOnSelect: false,
|
||||
setupScriptLaunchMode: 'new-tab',
|
||||
terminalScrollbackBytes: 10_000_000,
|
||||
openLinksInApp: true,
|
||||
|
|
|
|||
|
|
@ -591,6 +591,11 @@ export type GlobalSettings = {
|
|||
* menu behavior and users can still reach the menu with Ctrl+right-click. */
|
||||
terminalRightClickToPaste: boolean
|
||||
terminalFocusFollowsMouse: boolean
|
||||
/** Why: mirrors X11 / gnome-terminal "copy on select" UX — making a terminal
|
||||
* selection copies it to the system clipboard automatically, so users can
|
||||
* paste with Cmd/Ctrl+V without an intervening Cmd/Ctrl+Shift+C. Defaults
|
||||
* to false so existing users keep the explicit-copy behavior. */
|
||||
terminalClipboardOnSelect: boolean
|
||||
/** Where the repo setup script runs on workspace create. Defaults to a
|
||||
* background "Setup" tab so the user's main terminal stays immediately
|
||||
* usable without the setup output crowding the initial pane. */
|
||||
|
|
|
|||
Loading…
Reference in a new issue