@brimveyn/aimux 1.20.3 → 1.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/platform/clipboard.ts +140 -4
package/package.json
CHANGED
|
@@ -1,13 +1,135 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
|
|
1
3
|
import { logDebug } from '../debug/input-log'
|
|
2
4
|
import { toast } from '../state/toast-store'
|
|
3
5
|
|
|
6
|
+
export interface ClipboardCandidate {
|
|
7
|
+
argv: string[]
|
|
8
|
+
// powershell's Get-Clipboard emits CRLF line endings and appends a trailing
|
|
9
|
+
// newline of its own; both have to be undone to get the copied text back.
|
|
10
|
+
normalizeWindowsOutput?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ClipboardPlatform {
|
|
14
|
+
env: Record<string, string | undefined>
|
|
15
|
+
isWsl: boolean
|
|
16
|
+
platform: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const POWERSHELL_PASTE: ClipboardCandidate = {
|
|
20
|
+
argv: ['powershell.exe', '-NoProfile', '-NonInteractive', '-Command', 'Get-Clipboard'],
|
|
21
|
+
normalizeWindowsOutput: true,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isSet(value: string | undefined): boolean {
|
|
25
|
+
return value !== undefined && value !== ''
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function detectClipboardPlatform(): ClipboardPlatform {
|
|
29
|
+
return {
|
|
30
|
+
env: process.env,
|
|
31
|
+
isWsl: detectWsl(),
|
|
32
|
+
platform: process.platform,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function detectWsl(): boolean {
|
|
37
|
+
if (process.platform !== 'linux') return false
|
|
38
|
+
if (isSet(process.env.WSL_DISTRO_NAME) || isSet(process.env.WSL_INTEROP)) return true
|
|
39
|
+
try {
|
|
40
|
+
return readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft')
|
|
41
|
+
} catch {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function copyCandidates({ env, isWsl, platform }: ClipboardPlatform): ClipboardCandidate[] {
|
|
47
|
+
if (platform === 'darwin') return [{ argv: ['pbcopy'] }]
|
|
48
|
+
if (platform === 'win32') return [{ argv: ['clip'] }]
|
|
49
|
+
|
|
50
|
+
const candidates: ClipboardCandidate[] = []
|
|
51
|
+
// On WSL the Windows clipboard is the one the user pastes from, and clip.exe
|
|
52
|
+
// always reaches it. The X/Wayland bridges only exist under WSLg.
|
|
53
|
+
if (isWsl) {
|
|
54
|
+
candidates.push({ argv: ['clip.exe'] }, { argv: ['/mnt/c/Windows/System32/clip.exe'] })
|
|
55
|
+
}
|
|
56
|
+
const wayland: ClipboardCandidate = { argv: ['wl-copy'] }
|
|
57
|
+
const xorg: ClipboardCandidate[] = [
|
|
58
|
+
{ argv: ['xclip', '-selection', 'clipboard'] },
|
|
59
|
+
{ argv: ['xsel', '--clipboard', '--input'] },
|
|
60
|
+
]
|
|
61
|
+
candidates.push(...(isSet(env.WAYLAND_DISPLAY) ? [wayland, ...xorg] : [...xorg, wayland]))
|
|
62
|
+
return candidates
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function pasteCandidates({ env, isWsl, platform }: ClipboardPlatform): ClipboardCandidate[] {
|
|
66
|
+
if (platform === 'darwin') return [{ argv: ['pbpaste'] }]
|
|
67
|
+
if (platform === 'win32') return [POWERSHELL_PASTE]
|
|
68
|
+
|
|
69
|
+
const candidates: ClipboardCandidate[] = []
|
|
70
|
+
if (isWsl) candidates.push(POWERSHELL_PASTE)
|
|
71
|
+
const wayland: ClipboardCandidate = { argv: ['wl-paste', '--no-newline'] }
|
|
72
|
+
const xorg: ClipboardCandidate[] = [
|
|
73
|
+
{ argv: ['xclip', '-selection', 'clipboard', '-o'] },
|
|
74
|
+
{ argv: ['xsel', '--clipboard', '--output'] },
|
|
75
|
+
]
|
|
76
|
+
candidates.push(...(isSet(env.WAYLAND_DISPLAY) ? [wayland, ...xorg] : [...xorg, wayland]))
|
|
77
|
+
return candidates
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveCandidate(candidates: ClipboardCandidate[]): ClipboardCandidate | null {
|
|
81
|
+
for (const candidate of candidates) {
|
|
82
|
+
const [bin, ...args] = candidate.argv
|
|
83
|
+
if (bin === undefined) continue
|
|
84
|
+
let resolved: string | null
|
|
85
|
+
if (bin.includes('/')) {
|
|
86
|
+
resolved = existsSync(bin) ? bin : null
|
|
87
|
+
} else {
|
|
88
|
+
resolved = Bun.which(bin)
|
|
89
|
+
}
|
|
90
|
+
if (resolved !== null) return { ...candidate, argv: [resolved, ...args] }
|
|
91
|
+
}
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let cachedCopy: ClipboardCandidate | null | undefined
|
|
96
|
+
let cachedPaste: ClipboardCandidate | null | undefined
|
|
97
|
+
|
|
98
|
+
function copyCommand(): ClipboardCandidate | null {
|
|
99
|
+
cachedCopy ??= resolveCandidate(copyCandidates(detectClipboardPlatform()))
|
|
100
|
+
return cachedCopy
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function pasteCommand(): ClipboardCandidate | null {
|
|
104
|
+
cachedPaste ??= resolveCandidate(pasteCandidates(detectClipboardPlatform()))
|
|
105
|
+
return cachedPaste
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const MISSING_TOOL_MESSAGE =
|
|
109
|
+
process.platform === 'darwin'
|
|
110
|
+
? 'Copy failed: pbcopy not found'
|
|
111
|
+
: 'Copy failed: install xclip, wl-clipboard, or xsel'
|
|
112
|
+
|
|
4
113
|
export function copyToSystemClipboard(text: string): void {
|
|
114
|
+
const command = copyCommand()
|
|
115
|
+
if (!command) {
|
|
116
|
+
logDebug('platform.clipboard.noCopyCommand', { platform: process.platform })
|
|
117
|
+
toast.error(MISSING_TOOL_MESSAGE)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
5
120
|
try {
|
|
6
|
-
const proc = Bun.spawn(
|
|
121
|
+
const proc = Bun.spawn(command.argv, { stderr: 'pipe', stdin: 'pipe' })
|
|
7
122
|
void proc.stdin.write(text)
|
|
8
123
|
void proc.stdin.end()
|
|
124
|
+
void (async () => {
|
|
125
|
+
const code = await proc.exited
|
|
126
|
+
if (code === 0) return
|
|
127
|
+
logDebug('platform.clipboard.copyExit', { argv: command.argv, code })
|
|
128
|
+
toast.error('Copy failed')
|
|
129
|
+
})()
|
|
9
130
|
} catch (error) {
|
|
10
131
|
logDebug('platform.clipboard.copyError', {
|
|
132
|
+
argv: command.argv,
|
|
11
133
|
error: error instanceof Error ? error.message : String(error),
|
|
12
134
|
})
|
|
13
135
|
toast.error('Copy failed')
|
|
@@ -15,15 +137,29 @@ export function copyToSystemClipboard(text: string): void {
|
|
|
15
137
|
}
|
|
16
138
|
|
|
17
139
|
export async function readFromSystemClipboard(): Promise<string> {
|
|
140
|
+
const command = pasteCommand()
|
|
141
|
+
if (!command) {
|
|
142
|
+
logDebug('platform.clipboard.noPasteCommand', { platform: process.platform })
|
|
143
|
+
return ''
|
|
144
|
+
}
|
|
18
145
|
try {
|
|
19
|
-
const proc = Bun.spawn(
|
|
146
|
+
const proc = Bun.spawn(command.argv, { stderr: 'pipe', stdout: 'pipe' })
|
|
20
147
|
const text = await new Response(proc.stdout).text()
|
|
21
|
-
await proc.exited
|
|
22
|
-
|
|
148
|
+
const code = await proc.exited
|
|
149
|
+
if (code !== 0) {
|
|
150
|
+
logDebug('platform.clipboard.readExit', { argv: command.argv, code })
|
|
151
|
+
return ''
|
|
152
|
+
}
|
|
153
|
+
return command.normalizeWindowsOutput === true ? normalizeWindowsClipboardText(text) : text
|
|
23
154
|
} catch (error) {
|
|
24
155
|
logDebug('platform.clipboard.readError', {
|
|
156
|
+
argv: command.argv,
|
|
25
157
|
error: error instanceof Error ? error.message : String(error),
|
|
26
158
|
})
|
|
27
159
|
return ''
|
|
28
160
|
}
|
|
29
161
|
}
|
|
162
|
+
|
|
163
|
+
export function normalizeWindowsClipboardText(text: string): string {
|
|
164
|
+
return text.replaceAll('\r\n', '\n').replace(/\n$/, '')
|
|
165
|
+
}
|