Compare commits

...
Author SHA1 Message Date
Kit Langton effb48ea91 feat(tui): highlight Bash commands 2026-08-12 22:47:37 -04:00
2 changed files with 66 additions and 3 deletions
@@ -5,6 +5,7 @@ import {
MouseEvent,
PasteEvent,
decodePasteBytes,
getTreeSitterClient,
type ColorInput,
type KeyEvent,
} from "@opentui/core"
@@ -98,6 +99,7 @@ export type PromptRef = {
}
const DRAFT_RETENTION_MIN_CHARS = 20
const SHELL_SYNTAX_HIGHLIGHT_REF = 65_535
function randomIndex(count: number) {
if (count <= 0) return 0
@@ -340,6 +342,47 @@ export function Prompt(props: PromptProps) {
})
let disposed = false
let pasteQueue = Promise.resolve()
let syntaxHighlightVersion = 0
createEffect(() => {
const mode = store.mode
const content = store.prompt.text
const style = syntax()
const version = ++syntaxHighlightVersion
if (input && !input.isDestroyed) input.editBuffer.removeHighlightsByRef(SHELL_SYNTAX_HIGHLIGHT_REF)
if (mode !== "shell" || !content || !input || input.isDestroyed) return
const timeout = setTimeout(() => {
getTreeSitterClient()
.highlightOnce(content, "bash")
.then((result) => {
if (version !== syntaxHighlightVersion || !result.highlights || !input || input.isDestroyed) return
const bytes = new TextEncoder().encode(content)
const decoder = new TextDecoder()
result.highlights.forEach(([start, end, group]) => {
const styleId = style.getStyleId(group)
if (styleId === null) return
const before = decoder.decode(bytes.subarray(0, start))
const highlighted = decoder.decode(bytes.subarray(start, end))
input.editBuffer.addHighlightByCharRange({
start: promptOffsetWidth(before) - (before.match(/\n/g)?.length ?? 0),
end:
promptOffsetWidth(before) +
promptOffsetWidth(highlighted) -
((before + highlighted).match(/\n/g)?.length ?? 0),
styleId,
priority: 0,
hlRef: SHELL_SYNTAX_HIGHLIGHT_REF,
})
})
renderer.requestRender()
})
.catch(() => {})
}, 50)
onCleanup(() => clearTimeout(timeout))
})
function enqueuePaste(run: (changed: () => boolean) => Promise<void>) {
pasteQueue = pasteQueue
+23 -3
View File
@@ -1938,6 +1938,7 @@ function RevertMessage(props: {
function ShellMessage(props: { message: Extract<SessionMessageInfo, { type: "shell" }> }) {
const theme = useTheme("elevated")
const { currentSyntax: syntax } = useThemes()
const output = createMemo(() => stripAnsi(props.message.output?.output.trim() ?? ""))
return (
@@ -1952,7 +1953,16 @@ function ShellMessage(props: { message: Extract<SessionMessageInfo, { type: "she
customBorderChars={SplitBorder.customBorderChars}
borderColor={theme.background.default}
>
<text fg={theme.text.default}>$ {props.message.command}</text>
<box flexDirection="row" gap={1}>
<text fg={theme.text.default}>$</text>
<code
conceal={false}
fg={theme.text.default}
filetype="bash"
syntaxStyle={syntax()}
content={props.message.command}
/>
</box>
<Show when={output()}>
<text fg={theme.text.subdued}>{output()}</text>
</Show>
@@ -2784,6 +2794,7 @@ const SHELL_DISPLAY_LIMIT = 1024 * 1024
function Shell(props: ToolProps) {
const theme = useTheme()
const { currentSyntax: syntax } = useThemes()
const ctx = use()
const client = useClient()
const data = useData()
@@ -2915,11 +2926,20 @@ function Shell(props: ToolProps) {
fallback={
<box flexDirection="row" gap={1}>
<text fg={theme.text.default}>{prompt()}</text>
<text fg={theme.text.default}>{limitedInput()}</text>
<code
conceal={false}
fg={theme.text.default}
filetype="bash"
syntaxStyle={syntax()}
content={limitedInput()}
/>
</box>
}
>
<Spinner color={color()}>{limitedInput()}</Spinner>
<box flexDirection="row" gap={1}>
<Spinner color={color()} />
<code conceal={false} fg={color()} filetype="bash" syntaxStyle={syntax()} content={limitedInput()} />
</box>
</Show>
<Show when={limitedOutput()}>
<text fg={theme.text.subdued}>{limitedOutput()}</text>