Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton c5ac6ba4f3 refactor(tui): prototype settings side panel 2026-08-12 00:17:07 +00:00
2 changed files with 40 additions and 98 deletions
+27 -93
View File
@@ -1,10 +1,10 @@
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core" import { TextAttributes } from "@opentui/core"
import { useTerminalDimensions } from "@opentui/solid" import { useTerminalDimensions } from "@opentui/solid"
import { createMemo, createSignal, For, onMount, Show } from "solid-js" import { createMemo, createSignal, onMount, Show } from "solid-js"
import { useConfig } from "../config" import { useConfig } from "../config"
import { useTheme } from "../context/theme" import { useTheme } from "../context/theme"
import { useBindings } from "../keymap"
import { useDialog } from "../ui/dialog" import { useDialog } from "../ui/dialog"
import { DialogSelect } from "../ui/dialog-select"
import { useToast } from "../ui/toast" import { useToast } from "../ui/toast"
type Setting = { type Setting = {
@@ -249,9 +249,8 @@ export function DialogConfig() {
const themeState = useTheme() const themeState = useTheme()
const { theme } = themeState const { theme } = themeState
const dimensions = useTerminalDimensions() const dimensions = useTerminalDimensions()
const [selected, setSelected] = createSignal(0) const [selected, setSelected] = createSignal(settings[0])
const [saving, setSaving] = createSignal(false) const [saving, setSaving] = createSignal(false)
let scroll: ScrollBoxRenderable | undefined
onMount(() => { onMount(() => {
dialog.setSize("xlarge") dialog.setSize("xlarge")
dialog.setCentered(true) dialog.setCentered(true)
@@ -275,34 +274,19 @@ export function DialogConfig() {
const index = setting.values?.indexOf(current) const index = setting.values?.indexOf(current)
return index === undefined || index < 0 ? String(current) : (setting.labels?.[index] ?? String(current)) return index === undefined || index < 0 ? String(current) : (setting.labels?.[index] ?? String(current))
} }
const rows = createMemo(() => const options = createMemo(() =>
settings.map((setting, index) => ({ settings.map((setting) => ({
setting, title: setting.title,
index, category: setting.category,
heading: index === 0 || settings[index - 1].category !== setting.category, value: setting,
footer: selected() === setting ? ` ${display(setting)} ` : ` ${display(setting)} `,
})), })),
) )
const split = createMemo(() => dimensions().width >= 110) const split = createMemo(() => dimensions().width >= 110)
const height = createMemo(() => Math.max(8, Math.min(36, dimensions().height - 12))) const height = createMemo(() => Math.max(8, Math.min(36, dimensions().height - 12)))
function move(direction: number) { async function change(setting: Setting, direction: number) {
const next = (selected() + direction + settings.length) % settings.length
setSelected(next)
queueMicrotask(() => {
if (!scroll) return
const row =
next +
settings.slice(0, next + 1).filter((setting, index) => {
return index === 0 || settings[index - 1].category !== setting.category
}).length
if (row < scroll.scrollTop) scroll.scrollTo(row)
if (row >= scroll.scrollTop + scroll.viewport.height) scroll.scrollTo(row - scroll.viewport.height + 1)
})
}
async function change(direction: number) {
if (saving()) return if (saving()) return
const setting = settings[selected()]
const current = value(setting) const current = value(setting)
const choices = values(setting) const choices = values(setting)
const next = choices const next = choices
@@ -322,72 +306,22 @@ export function DialogConfig() {
.finally(() => setSaving(false)) .finally(() => setSaving(false))
} }
useBindings(() => ({
bindings: [
{
key: "up",
desc: "Previous setting",
group: "Settings",
cmd: () => move(-1),
},
{
key: "down",
desc: "Next setting",
group: "Settings",
cmd: () => move(1),
},
{ key: "left", desc: "Previous value", group: "Settings", cmd: () => void change(-1) },
{ key: "right", desc: "Next value", group: "Settings", cmd: () => void change(1) },
{ key: "return", desc: "Next value", group: "Settings", cmd: () => void change(1) },
],
}))
return ( return (
<box flexDirection="row" height={height() + 1}> <box flexDirection="row" height={height() + 1}>
<box width={split() ? "54%" : "100%"} paddingLeft={4} paddingRight={split() ? 3 : 4} paddingBottom={1}> <box width={split() ? "54%" : "100%"}>
<text fg={theme.text} attributes={TextAttributes.BOLD} paddingBottom={1}> <DialogSelect
Settings title="Settings"
</text> options={options()}
<scrollbox renderFilter={false}
ref={(element: ScrollBoxRenderable) => (scroll = element)} hideClose={split()}
flexGrow={1} maxHeight={height() - 2}
scrollbarOptions={{ visible: false }} onMove={(option) => setSelected(option.value)}
> onSelect={(option) => void change(option.value, 1)}
<For each={rows()}> bindings={[
{(row) => ( { key: "left", desc: "Previous value", group: "Settings", cmd: () => void change(selected(), -1) },
<> { key: "right", desc: "Next value", group: "Settings", cmd: () => void change(selected(), 1) },
<Show when={row.heading}> ]}
<box paddingTop={row.index === 0 ? 0 : 1}> />
<text fg={theme.primary} attributes={TextAttributes.BOLD}>
{row.setting.category}
</text>
</box>
</Show>
<box flexDirection="row" height={1}>
<text
width={25}
fg={row.index === selected() ? theme.text : theme.textMuted}
attributes={row.index === selected() ? TextAttributes.BOLD : undefined}
>
{row.setting.title}
</text>
<box flexGrow={1} flexDirection="row" justifyContent="flex-end">
<box flexDirection="row">
<text fg={theme.textMuted}>{row.index === selected() ? " " : " "}</text>
<text
fg={row.index === selected() ? theme.primary : theme.textMuted}
attributes={row.index === selected() ? TextAttributes.BOLD : undefined}
>
{display(row.setting)}
</text>
<text fg={theme.textMuted}>{row.index === selected() ? " " : " "}</text>
</box>
</box>
</box>
</>
)}
</For>
</scrollbox>
</box> </box>
<Show when={split()}> <Show when={split()}>
<box <box
@@ -402,7 +336,7 @@ export function DialogConfig() {
> >
<box flexDirection="row" justifyContent="space-between"> <box flexDirection="row" justifyContent="space-between">
<text fg={theme.primary} attributes={TextAttributes.BOLD}> <text fg={theme.primary} attributes={TextAttributes.BOLD}>
{settings[selected()].title} {selected().title}
</text> </text>
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}> <text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
esc esc
@@ -410,7 +344,7 @@ export function DialogConfig() {
</box> </box>
<box paddingTop={1}> <box paddingTop={1}>
<text fg={theme.text} wrapMode="word"> <text fg={theme.text} wrapMode="word">
{settings[selected()].detail ?? settings[selected()].description} {selected().detail ?? selected().description}
</text> </text>
</box> </box>
</box> </box>
+13 -5
View File
@@ -51,6 +51,8 @@ export interface DialogSelectProps<T> {
}[] }[]
bindings?: readonly Binding<Renderable, KeyEvent>[] bindings?: readonly Binding<Renderable, KeyEvent>[]
current?: T current?: T
hideClose?: boolean
maxHeight?: number
} }
export interface DialogSelectOption<T = any> { export interface DialogSelectOption<T = any> {
@@ -212,7 +214,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
}) })
const dimensions = useTerminalDimensions() const dimensions = useTerminalDimensions()
const height = createMemo(() => Math.min(rows(), Math.floor(dimensions().height / 2) - 6)) const height = createMemo(() => Math.min(rows(), props.maxHeight ?? Math.floor(dimensions().height / 2) - 6))
const selected = createMemo(() => flat()[store.selected]) const selected = createMemo(() => flat()[store.selected])
@@ -565,9 +567,11 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
{props.title} {props.title}
</text> </text>
)} )}
<text fg={theme.textMuted} onMouseUp={() => dialog.clear()}> <Show when={!props.hideClose}>
esc <text fg={theme.textMuted} onMouseUp={() => dialog.clear()}>
</text> esc
</text>
</Show>
</box> </box>
<Show when={props.renderFilter !== false}> <Show when={props.renderFilter !== false}>
<box paddingTop={1}> <box paddingTop={1}>
@@ -789,7 +793,11 @@ function Option(props: {
</text> </text>
<Show when={props.footer}> <Show when={props.footer}>
<box flexShrink={0}> <box flexShrink={0}>
<text fg={props.active && !props.muted ? fg : theme.textMuted}>{props.footer}</text> {typeof props.footer === "string" ? (
<text fg={props.active && !props.muted ? fg : theme.textMuted}>{props.footer}</text>
) : (
props.footer
)}
</box> </box>
</Show> </Show>
</> </>