opencode/packages/tui/src/routes/session/composer/shell-tab.tsx

128 lines
4.1 KiB
TypeScript

import { createMemo, For, Show, createEffect, onMount, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import { TextAttributes, ScrollBoxRenderable } from "@opentui/core"
import { useData } from "../../../context/data"
import { useLocation } from "../../../context/location"
import { useClient } from "../../../context/client"
import { useTheme } from "../../../context/theme"
import { Keymap } from "../../../context/keymap"
import { useComposerTab } from "./index"
export function ShellTab(props: { sessionID: string }) {
const data = useData()
const location = useLocation()
const client = useClient()
const theme = useTheme()
const composer = useComposerTab()
const shortcuts = Keymap.useShortcuts()
const entries = createMemo(() => data.shell.listBySession(props.sessionID).filter((shell) => shell.status === "running"))
const [store, setStore] = createStore({ selected: 0 })
let scroll: ScrollBoxRenderable | undefined
const selectedEntry = createMemo(() => entries()[store.selected])
createEffect(() => {
if (store.selected >= entries().length) setStore("selected", Math.max(0, entries().length - 1))
})
createEffect(() => {
if (!scroll) return
const target = scroll.getChildren()[store.selected]
if (!target) return
const y = target.y - scroll.y
if (y >= scroll.height || y < 0) {
const center = Math.floor(scroll.height / 2)
scroll.scrollBy(y - center)
}
})
onMount(() => {
const cleanup = composer.register({
id: "shell",
label: "Shell",
hints: () => (selectedEntry() ? [{ label: "kill", shortcut: shortcuts.get("composer.shell.kill") ?? "" }] : []),
})
onCleanup(cleanup)
})
Keymap.createLayer(() => ({
mode: "composer",
enabled: () => composer.active("shell"),
commands: [
{
id: "composer.shell.up",
title: "Previous shell",
group: "Composer",
bind: "up",
run() {
if (store.selected === 0) {
composer.close()
return
}
setStore("selected", (prev) => prev - 1)
},
},
{
id: "composer.shell.down",
title: "Next shell",
group: "Composer",
bind: "down",
run() {
const list = entries()
if (list.length === 0) return
setStore("selected", (prev) => (prev + 1) % list.length)
},
},
{
id: "composer.shell.kill",
title: "Kill shell command",
group: "Composer",
bind: "ctrl+d",
run() {
const entry = selectedEntry()
if (!entry) return
const ref = location.current
void client.api.shell.remove({
id: entry.id,
location: ref ? { directory: ref.directory, workspace: ref.workspaceID } : undefined,
})
},
},
],
}))
return (
<Show when={composer.active("shell")}>
<scrollbox scrollbarOptions={{ visible: false }} maxHeight={5} ref={(r: ScrollBoxRenderable) => (scroll = r)}>
<Show when={entries().length > 0} fallback={<text fg={theme.text.subdued}> No shell commands</text>}>
<For each={entries()}>
{(shell, index) => {
const active = createMemo(() => index() === store.selected)
return (
<box
flexDirection="row"
paddingLeft={1}
paddingRight={1}
backgroundColor={
active() ? theme.background.action.primary.focused : theme.background.action.primary.default
}
onMouseOver={() => setStore("selected", index())}
>
<text
fg={active() ? theme.text.action.primary.focused : theme.text.action.primary.default}
attributes={active() ? TextAttributes.BOLD : undefined}
wrapMode="none"
>
{shell.command}
</text>
</box>
)
}}
</For>
</Show>
</scrollbox>
</Show>
)
}