|
| 1 | +import { TextAttributes } from "@opentui/core" |
| 2 | +import { useTheme } from "../context/theme" |
| 3 | +import { useDialog } from "@tui/ui/dialog" |
| 4 | +import { useSDK } from "@tui/context/sdk" |
| 5 | +import { createResource, onMount, Show } from "solid-js" |
| 6 | +import * as QRCode from "qrcode" |
| 7 | + |
| 8 | +type PairResult = { enabled: false } | { enabled: true; hosts: string[]; link: string; qr: string } |
| 9 | + |
| 10 | +const BLOCK = { |
| 11 | + WW: " ", |
| 12 | + WB: "▄", |
| 13 | + BB: "█", |
| 14 | + BW: "▀", |
| 15 | +} |
| 16 | + |
| 17 | +function renderQR(link: string): string { |
| 18 | + const qr = QRCode.create(link, { errorCorrectionLevel: "L" }) |
| 19 | + const size = qr.modules.size |
| 20 | + const data = qr.modules.data |
| 21 | + const margin = 2 |
| 22 | + |
| 23 | + const get = (r: number, c: number) => { |
| 24 | + if (r < 0 || r >= size || c < 0 || c >= size) return false |
| 25 | + return Boolean(data[r * size + c]) |
| 26 | + } |
| 27 | + |
| 28 | + const totalW = size + margin * 2 |
| 29 | + const blank = BLOCK.WW.repeat(totalW) |
| 30 | + const lines: string[] = [] |
| 31 | + |
| 32 | + // top margin |
| 33 | + for (let i = 0; i < margin / 2; i++) lines.push(blank) |
| 34 | + |
| 35 | + // QR rows, 2 at a time using half-block chars |
| 36 | + for (let r = -margin; r < size + margin; r += 2) { |
| 37 | + let row = "" |
| 38 | + for (let c = -margin; c < size + margin; c++) { |
| 39 | + const top = get(r, c) |
| 40 | + const bottom = get(r + 1, c) |
| 41 | + if (top && bottom) row += BLOCK.BB |
| 42 | + else if (top) row += BLOCK.BW |
| 43 | + else if (bottom) row += BLOCK.WB |
| 44 | + else row += BLOCK.WW |
| 45 | + } |
| 46 | + lines.push(row) |
| 47 | + } |
| 48 | + |
| 49 | + return lines.join("\n") |
| 50 | +} |
| 51 | + |
| 52 | +export function DialogPair() { |
| 53 | + const dialog = useDialog() |
| 54 | + const { theme } = useTheme() |
| 55 | + const sdk = useSDK() |
| 56 | + |
| 57 | + onMount(() => { |
| 58 | + dialog.setSize("large") |
| 59 | + }) |
| 60 | + |
| 61 | + const [data] = createResource(async () => { |
| 62 | + const res = await sdk.fetch(`${sdk.url}/experimental/push/pair`) |
| 63 | + if (!res.ok) return { enabled: false as const } |
| 64 | + const json = (await res.json()) as PairResult |
| 65 | + if (!json.enabled) return json |
| 66 | + |
| 67 | + const qrText = renderQR(json.link) |
| 68 | + return { ...json, qrText } |
| 69 | + }) |
| 70 | + |
| 71 | + return ( |
| 72 | + <box paddingLeft={2} paddingRight={2} gap={1} paddingBottom={1}> |
| 73 | + <box flexDirection="row" justifyContent="space-between"> |
| 74 | + <text fg={theme.text} attributes={TextAttributes.BOLD}> |
| 75 | + Pair Mobile Device |
| 76 | + </text> |
| 77 | + <text fg={theme.textMuted} onMouseUp={() => dialog.clear()}> |
| 78 | + esc |
| 79 | + </text> |
| 80 | + </box> |
| 81 | + <Show when={data.loading}> |
| 82 | + <text fg={theme.textMuted}>Loading pairing info...</text> |
| 83 | + </Show> |
| 84 | + <Show when={data.error}> |
| 85 | + <box gap={1}> |
| 86 | + <text fg={theme.error}>Could not load pairing info.</text> |
| 87 | + <text fg={theme.textMuted} wrapMode="word"> |
| 88 | + Check that the server is reachable and try again. |
| 89 | + </text> |
| 90 | + </box> |
| 91 | + </Show> |
| 92 | + <Show when={!data.loading && !data.error && data()}> |
| 93 | + {(result) => ( |
| 94 | + <Show |
| 95 | + when={result().enabled && result()} |
| 96 | + fallback={ |
| 97 | + <box gap={1}> |
| 98 | + <text fg={theme.warning}>Push relay is not enabled.</text> |
| 99 | + <text fg={theme.textMuted} wrapMode="word"> |
| 100 | + Start the server with push relay options to enable mobile pairing: |
| 101 | + </text> |
| 102 | + <text fg={theme.text} wrapMode="word"> |
| 103 | + opencode serve --relay-url <url> --relay-secret <secret> |
| 104 | + </text> |
| 105 | + </box> |
| 106 | + } |
| 107 | + > |
| 108 | + {(pair) => ( |
| 109 | + <box gap={1} alignItems="center"> |
| 110 | + <text fg={theme.text}>{(pair() as any).qrText}</text> |
| 111 | + <box gap={0} alignItems="center"> |
| 112 | + <text fg={theme.textMuted} wrapMode="word"> |
| 113 | + Scan with the OpenCode Control app |
| 114 | + </text> |
| 115 | + <text fg={theme.textMuted} wrapMode="word"> |
| 116 | + to pair your device for push notifications. |
| 117 | + </text> |
| 118 | + </box> |
| 119 | + </box> |
| 120 | + )} |
| 121 | + </Show> |
| 122 | + )} |
| 123 | + </Show> |
| 124 | + </box> |
| 125 | + ) |
| 126 | +} |
0 commit comments