Last active
June 28, 2020 01:43
-
-
Save jsejcksn/b4b1e86e504f16239aec90df4e9b29a9 to your computer and use it in GitHub Desktop.
Deno text clipboard
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {assert, assertEquals} from './deps.ts'; | |
import {readText, writeText} from './mod.ts'; | |
type Test = [string, () => void | Promise<void>]; | |
const tests: Test[] = [ | |
[ | |
'reads/writes without throwing', async () => { | |
const input = 'hello world'; | |
await writeText(input); | |
await readText(); | |
}, | |
], | |
[ | |
'single line data', async () => { | |
const input = 'single line data'; | |
await writeText(input); | |
const output = await readText(); | |
assertEquals(output.replace(/\n+$/u, ''), input.replace(/\n+$/u, '')); | |
}, | |
], | |
[ | |
'multi line data', async () => { | |
const input = 'multi\nline\ndata'; | |
await writeText(input); | |
const output = await readText(); | |
assertEquals(output.replace(/\n+$/u, ''), input.replace(/\n+$/u, '')); | |
}, | |
], | |
[ | |
'multi line data dangling newlines', async () => { | |
const input = '\n\n\nmulti\n\n\n\n\n\nline\ndata\n\n\n\n\n'; | |
await writeText(input); | |
const output = await readText({trimFinalNewlines: false}); | |
assertEquals(output.replace(/\n+$/u, ''), input.replace(/\n+$/u, '')); | |
}, | |
], | |
[ | |
'data with special characters', async () => { | |
const input = '`~!@#$%^&*()_+-=[]{};\':",./<>?\t\n'; | |
await writeText(input); | |
const output = await readText({trimFinalNewlines: false}); | |
assertEquals(output.replace(/\n+$/u, ''), input.replace(/\n+$/u, '')); | |
}, | |
], | |
[ | |
'data with unicode characters', async () => { | |
const input = 'Rafał'; | |
await writeText(input); | |
const output = await readText(); | |
assertEquals(output.replace(/\n+$/u, ''), input.replace(/\n+$/u, '')); | |
}, | |
], | |
[ | |
'option: trim', async () => { | |
const input = 'hello world\n\n'; | |
const inputTrimmed = 'hello world'; | |
await writeText(input); | |
const output = await readText({trimFinalNewlines: false}); | |
const outputTrimmed = await readText({trimFinalNewlines: true}); | |
const outputDefault = await readText(); | |
assert(output !== inputTrimmed && output.trim() === inputTrimmed); | |
assertEquals(inputTrimmed, outputTrimmed); | |
assertEquals(inputTrimmed, outputDefault); | |
}, | |
], | |
[ | |
'option: unixNewlines', async () => { | |
const inputCRLF = 'hello\r\nworld'; | |
const inputLF = 'hello\nworld'; | |
await writeText(inputCRLF); | |
const output = await readText({unixNewlines: false}); | |
const outputUnix = await readText({unixNewlines: true}); | |
const outputDefault = await readText(); | |
assertEquals(inputCRLF, output); | |
assertEquals(inputLF, outputUnix); | |
assertEquals(inputLF, outputDefault); | |
}, | |
], | |
]; | |
for (const [name, fn] of tests) Deno.test({fn, name}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const decoder = new TextDecoder(); | |
const encoder = new TextEncoder(); | |
type LinuxBinary = 'wsl' | 'xclip' | 'xsel'; | |
type Config = { | |
linuxBinary: LinuxBinary; | |
}; | |
const config: Config = {linuxBinary: 'xsel'}; | |
const errMsg = { | |
genericRead: 'There was a problem reading from the clipboard', | |
genericWrite: 'There was a problem writing to the clipboard', | |
noClipboardUtility: 'No supported clipboard utility. "xsel" or "xclip" must be installed.', | |
osUnsupported: 'Unsupported operating system', | |
}; | |
const normalizeNewlines = (str: string) => str.replace(/\r\n/gu, '\n'); | |
const trimNewlines = (str: string) => str.replace(/(?:\r\n|\n)+$/u, ''); | |
/** | |
* Options to change the parsing behavior when reading the clipboard text | |
* | |
* `trimFinalNewlines?` — Trim trailing newlines. Default is `true`. | |
* | |
* `unixNewlines?` — Convert all CRLF newlines to LF newlines. Default is `true`. | |
*/ | |
export type ReadTextOptions = { | |
trimFinalNewlines?: boolean; | |
unixNewlines?: boolean; | |
}; | |
type TextClipboard = { | |
readText: (readTextOptions?: ReadTextOptions) => Promise<string>; | |
writeText: (data: string) => Promise<void>; | |
}; | |
const shared = { | |
async readText ( | |
cmd: string[], | |
{trimFinalNewlines = true, unixNewlines = true}: ReadTextOptions = {}, | |
): Promise<string> { | |
const p = Deno.run({cmd, stdout: 'piped'}); | |
const {success} = await p.status(); | |
const stdout = decoder.decode(await p.output()); | |
p.close(); | |
if (!success) throw new Error(errMsg.genericRead); | |
let result = stdout; | |
if (unixNewlines) result = normalizeNewlines(result); | |
if (trimFinalNewlines) return trimNewlines(result); | |
return result; | |
}, | |
async writeText (cmd: string[], data: string): Promise<void> { | |
const p = Deno.run({cmd, stdin: 'piped'}); | |
if (!p.stdin) throw new Error(errMsg.genericWrite); | |
await p.stdin.write(encoder.encode(data)); | |
p.stdin.close(); | |
const {success} = await p.status(); | |
if (!success) throw new Error(errMsg.genericWrite); | |
p.close(); | |
}, | |
}; | |
const darwin: TextClipboard = { | |
readText (readTextOptions?: ReadTextOptions): Promise<string> { | |
const cmd: string[] = ['pbpaste']; | |
return shared.readText(cmd, readTextOptions); | |
}, | |
writeText (data: string): Promise<void> { | |
const cmd: string[] = ['pbcopy']; | |
return shared.writeText(cmd, data); | |
}, | |
}; | |
const linux: TextClipboard = { | |
readText (readTextOptions?: ReadTextOptions): Promise<string> { | |
const cmds: {[key in LinuxBinary]: string[]} = { | |
wsl: ['powershell.exe', '-NoProfile', '-Command', 'Get-Clipboard'], | |
xclip: ['xclip', '-selection', 'clipboard', '-o'], | |
xsel: ['xsel', '-b', '-o'], | |
}; | |
const cmd = cmds[config.linuxBinary]; | |
return shared.readText(cmd, readTextOptions); | |
}, | |
writeText (data: string): Promise<void> { | |
const cmds: {[key in LinuxBinary]: string[]} = { | |
wsl: ['clip.exe'], | |
xclip: ['xclip', '-selection', 'clipboard'], | |
xsel: ['xsel', '-b', '-i'], | |
}; | |
const cmd = cmds[config.linuxBinary]; | |
return shared.writeText(cmd, data); | |
}, | |
}; | |
const windows: TextClipboard = { | |
readText (readTextOptions?: ReadTextOptions): Promise<string> { | |
const cmd: string[] = ['powershell', '-NoProfile', '-Command', 'Get-Clipboard']; | |
return shared.readText(cmd, readTextOptions); | |
}, | |
writeText (data: string): Promise<void> { | |
const cmd: string[] = ['powershell', '-NoProfile', '-Command', '$input|Set-Clipboard']; | |
return shared.writeText(cmd, data); | |
}, | |
}; | |
const getProcessOutput = async (cmd: string[]): Promise<string> => { | |
try { | |
const p = Deno.run({cmd, stdout: 'piped'}); | |
const stdout = decoder.decode(await p.output()); | |
p.close(); | |
return stdout.trim(); | |
} | |
catch (err) { | |
return ''; | |
} | |
}; | |
const resolveLinuxBinary = async (): Promise<LinuxBinary> => { | |
type BinaryEntry = [LinuxBinary, () => boolean | Promise<boolean>]; | |
const binaryEntries: BinaryEntry[] = [ | |
[ | |
'wsl', async () => ( | |
(await getProcessOutput(['uname', '-r', '-v'])).toLowerCase().includes('microsoft') | |
&& Boolean(await getProcessOutput(['which', 'clip.exe'])) | |
&& Boolean(await getProcessOutput(['which', 'powershell.exe'])) | |
), | |
], | |
['xsel', async () => Boolean(await getProcessOutput(['which', 'xsel']))], | |
['xclip', async () => Boolean(await getProcessOutput(['which', 'xclip']))], | |
]; | |
for (const [binary, matchFn] of binaryEntries) { | |
const binaryMatches = await matchFn(); | |
if (binaryMatches) return binary; | |
} | |
throw new Error(errMsg.noClipboardUtility); | |
}; | |
type Clipboards = {[key in typeof Deno.build.os]: TextClipboard}; | |
const clipboards: Clipboards = { | |
darwin, | |
linux, | |
windows, | |
}; | |
const {build: {os}} = Deno; | |
if (os === 'linux') config.linuxBinary = await resolveLinuxBinary(); | |
else if (!clipboards[os]) throw new Error(errMsg.osUnsupported); | |
/** | |
* Reads the clipboard and returns a string containing the text contents. Requires the `--allow-run` flag. | |
*/ | |
export const {readText} = clipboards[os]; | |
/** | |
* Writes a string to the clipboard. Requires the `--allow-run` flag. | |
*/ | |
export const {writeText} = clipboards[os]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment