Compare commits

...

1 Commits

Author SHA1 Message Date
neriousy 9e621726a9 fix(tui): coalesce rapid selection copies 2026-08-23 15:18:48 +00:00
3 changed files with 77 additions and 7 deletions
+5 -7
View File
@@ -571,15 +571,13 @@ function App(props: { pair?: DialogPairCredentials }) {
offSelectionKeys()
})
// Wire up console copy-to-clipboard via opentui's onCopySelection callback
renderer.console.onCopySelection = async (text: string) => {
// Windows clipboard mutations are serialized by OpenTUI. Coalesce rapid selections here so
// they cannot fill its native operation queue and starve renderer polling.
const copySelection = Selection.createSelectionCopy(clipboard, toast)
renderer.console.onCopySelection = (text: string) => {
if (!text || text.length === 0) return
await clipboard
.write(text)
.then(() => toast.show({ message: "Copied to clipboard", variant: "info" }))
.catch(toast.error)
copySelection(text)
renderer.clearSelection()
}
const terminalTitleEnabled = () => config.data.terminal?.title ?? true
+27
View File
@@ -23,6 +23,33 @@ type SelectionKeyEvent = {
stopPropagation: () => void
}
export function createSelectionCopy(clipboard: ClipboardService, toast: Toast) {
let pending: string | undefined
let running = false
const drain = async () => {
while (pending !== undefined) {
const text = pending
pending = undefined
const error = await clipboard.write(text).then(
() => undefined,
(error) => error,
)
if (pending !== undefined) continue
if (error !== undefined) toast.error(error)
else toast.show({ message: "Copied to clipboard", variant: "info" })
}
running = false
}
return (text: string) => {
pending = text
if (running) return
running = true
void drain()
}
}
export function copy(renderer: Renderer, toast: Toast, clipboard: ClipboardService): boolean {
const selection = renderer.getSelection()
if (!selection) return false
+45
View File
@@ -0,0 +1,45 @@
import { expect, test } from "bun:test"
import { createSelectionCopy } from "../src/util/selection"
test("keeps one clipboard write active and coalesces rapid selections", async () => {
const writes: string[] = []
const releases: (() => void)[] = []
let active = 0
let maximumActive = 0
const clipboard = {
async read() {
return undefined
},
async write(text: string) {
writes.push(text)
active++
maximumActive = Math.max(maximumActive, active)
await new Promise<void>((resolve) => releases.push(resolve))
active--
},
}
const copied: string[] = []
const failed: unknown[] = []
const copy = createSelectionCopy(clipboard, {
show: (input) => copied.push(input.message),
error: (error) => failed.push(error),
})
copy("first")
for (let index = 0; index < 100; index++) copy(`selection-${index}`)
expect(writes).toEqual(["first"])
expect(maximumActive).toBe(1)
releases.shift()?.()
await Bun.sleep(0)
expect(writes).toEqual(["first", "selection-99"])
expect(maximumActive).toBe(1)
releases.shift()?.()
await Bun.sleep(0)
expect(copied).toEqual(["Copied to clipboard"])
expect(failed).toEqual([])
})