tui: improve clipboard handling and keybind display

- Add cross-platform clipboard utility with image and text support
- Fix keybind display to properly show leader key combinations
- Refactor logo rendering for better maintainability
- Update prompt component to use new clipboard functionality
This commit is contained in:
Dax Raad 2025-09-25 21:55:13 -04:00
parent 09b65ff1a7
commit 68e76b8d49
6 changed files with 86 additions and 20 deletions

View file

@ -14,7 +14,7 @@ import type { FilePart } from "@opencode-ai/sdk"
import fuzzysort from "fuzzysort"
import { useCommandDialog } from "./dialog-command"
import { useKeybind } from "../context/keybind"
import clipboard from "clipboardy"
import { Clipboard } from "../../../../util/clipboard"
export type PromptProps = {
sessionID?: string
@ -71,9 +71,11 @@ export function Prompt(props: PromptProps) {
<box paddingTop={1} paddingBottom={2} backgroundColor={Theme.backgroundElement} flexGrow={1}>
<input
onPaste={async function (text) {
const data = (await clipboard.read().catch(() => {})) ?? text
console.log(data)
this.insertText(data)
const content = await Clipboard.read()
console.log(content)
if (!content) {
this.insertText(text)
}
}}
onInput={(value) => {
let diff = value.length - store.input.length

View file

@ -90,7 +90,8 @@ export function init() {
print(key: keyof KeybindsConfig) {
const first = keybinds()[key]?.at(0)
if (!first) return ""
return Keybind.toString(first)
const result = Keybind.toString(first)
return result.replace("<leader>", Keybind.toString(keybinds().leader![0]!))
},
}
return result

View file

@ -2,6 +2,7 @@ import { Installation } from "../../../installation"
import { useTheme } from "./context/theme"
import { TextAttributes } from "@opentui/core"
import { Prompt } from "./component/prompt"
import { For } from "solid-js"
export function Home() {
const { currentTheme } = useTheme()
@ -46,23 +47,33 @@ function HelpRow(props: { children: string; slash: string; theme: any }) {
)
}
const LOGO_LEFT = [
` `,
`█▀▀█ █▀▀█ █▀▀█ █▀▀▄`,
`█░░█ █░░█ █▀▀▀ █░░█`,
`▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀`,
]
const LOGO_RIGHT = [
``,
`█▀▀▀ █▀▀█ █▀▀█ █▀▀█`,
`█░░░ █░░█ █░░█ █▀▀▀`,
`▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`,
]
function Logo(props: { theme: any }) {
return (
<box>
<box flexDirection="row">
<text fg={props.theme.textMuted}>{"█▀▀█ █▀▀█ █▀▀ █▀▀▄"}</text>
<text fg={props.theme.text} attributes={TextAttributes.BOLD}>
{" █▀▀ █▀▀█ █▀▀▄ █▀▀"}
</text>
</box>
<box flexDirection="row">
<text fg={props.theme.textMuted}>{`█░░█ █░░█ █▀▀ █░░█`}</text>
<text fg={props.theme.text}>{` █░░ █░░█ █░░█ █▀▀`}</text>
</box>
<box flexDirection="row">
<text fg={props.theme.textMuted}>{`▀▀▀▀ █▀▀▀ ▀▀▀ ▀ ▀`}</text>
<text fg={props.theme.text}>{` ▀▀▀ ▀▀▀▀ ▀▀▀ ▀▀▀`}</text>
</box>
<For each={LOGO_LEFT}>
{(line, index) => (
<box flexDirection="row" gap={1}>
<text fg={props.theme.textMuted}>{line}</text>
<text fg={props.theme.text} attributes={TextAttributes.BOLD}>
{LOGO_RIGHT[index()]}
</text>
</box>
)}
</For>
<box flexDirection="row" justifyContent="flex-end">
<text fg={props.theme.textMuted}>{Installation.VERSION}</text>
</box>

View file

@ -0,0 +1,3 @@
export namespace Clipboard {
export function copy() {}
}

View file

@ -0,0 +1,49 @@
import { $ } from "bun"
import { platform } from "os"
import clipboardy from "clipboardy"
export namespace Clipboard {
export interface Content {
data: string
mime: string
}
export async function read(): Promise<Content | undefined> {
const os = platform()
if (os === "darwin") {
const imageBuffer = await $`osascript -e 'try' -e 'the clipboard as «class PNGf»' -e 'end try'`.nothrow().text()
if (imageBuffer) {
return { data: Buffer.from(imageBuffer).toString("base64url"), mime: "image/png" }
}
}
if (os === "linux") {
const wayland = await $`wl-paste -t image/png`.nothrow().text()
if (wayland) {
return { data: Buffer.from(wayland).toString("base64url"), mime: "image/png" }
}
const x11 = await $`xclip -selection clipboard -t image/png -o`.nothrow().text()
if (x11) {
return { data: Buffer.from(x11).toString("base64url"), mime: "image/png" }
}
}
if (os === "win32") {
const script =
"Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }"
const base64 = await $`powershell -command "${script}"`.nothrow().text()
if (base64) {
const imageBuffer = Buffer.from(base64.trim(), "base64")
if (imageBuffer.length > 0) {
return { data: imageBuffer.toString("base64url"), mime: "image/png" }
}
}
}
const text = await clipboardy.read().catch(() => {})
if (text) {
return { data: text, mime: "text/plain" }
}
}
}

View file

@ -24,7 +24,7 @@ export namespace Keybind {
let result = parts.join("+")
if (info.leader) {
result = `<leader>${result}`
result = `<leader>,${result}`
}
return result