fix(tui): fit project paths in open menu (#39887)
This commit is contained in:
parent
7aaf4e7750
commit
7fd12c560c
5 changed files with 52 additions and 17 deletions
|
|
@ -2,8 +2,8 @@ import { useTerminalDimensions } from "@opentui/solid"
|
||||||
import { TextAttributes } from "@opentui/core"
|
import { TextAttributes } from "@opentui/core"
|
||||||
import { createMemo, createResource, createSignal, onMount, Show } from "solid-js"
|
import { createMemo, createResource, createSignal, onMount, Show } from "solid-js"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { DialogSelect, type DialogSelectOption } from "../ui/dialog-select"
|
import { DialogSelect, dialogSelectContentWidth, type DialogSelectOption } from "../ui/dialog-select"
|
||||||
import { useDialog } from "../ui/dialog"
|
import { dialogWidth, useDialog } from "../ui/dialog"
|
||||||
import { useClient } from "../context/client"
|
import { useClient } from "../context/client"
|
||||||
import { Keymap } from "../context/keymap"
|
import { Keymap } from "../context/keymap"
|
||||||
import { useTheme } from "../context/theme"
|
import { useTheme } from "../context/theme"
|
||||||
|
|
@ -159,7 +159,10 @@ export function DialogMoveSession(props: DialogMoveSessionProps) {
|
||||||
if (b.location === b.root.directory) return 1
|
if (b.location === b.root.directory) return 1
|
||||||
return a.location.localeCompare(b.location)
|
return a.location.localeCompare(b.location)
|
||||||
})
|
})
|
||||||
const titleWidth = Math.max(1, Math.min(116, dimensions().width - 2) - 12)
|
const titleWidth = Math.max(
|
||||||
|
1,
|
||||||
|
dialogSelectContentWidth(Math.min(dialogWidth("xlarge"), dimensions().width - 2)),
|
||||||
|
)
|
||||||
|
|
||||||
return list.map((item) => {
|
return list.map((item) => {
|
||||||
const title = abbreviateHome(item.location, paths.home)
|
const title = abbreviateHome(item.location, paths.home)
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import path from "path"
|
||||||
import { createMemo, createResource, createSignal, onMount } from "solid-js"
|
import { createMemo, createResource, createSignal, onMount } from "solid-js"
|
||||||
import type { SessionInfo } from "@opencode-ai/client"
|
import type { SessionInfo } from "@opencode-ai/client"
|
||||||
import { useTerminalDimensions } from "@opentui/solid"
|
import { useTerminalDimensions } from "@opentui/solid"
|
||||||
import { useDialog } from "../ui/dialog"
|
import { dialogWidth, useDialog } from "../ui/dialog"
|
||||||
import { DialogSelect } from "../ui/dialog-select"
|
import { DialogSelect, dialogSelectContentWidth } from "../ui/dialog-select"
|
||||||
import { useRoute } from "../context/route"
|
import { useRoute } from "../context/route"
|
||||||
import { useData } from "../context/data"
|
import { useData } from "../context/data"
|
||||||
import { useClient } from "../context/client"
|
import { useClient } from "../context/client"
|
||||||
|
|
@ -105,8 +105,8 @@ export function DialogOpen() {
|
||||||
.map((project) => {
|
.map((project) => {
|
||||||
const title = project.name ?? path.basename(project.canonical)
|
const title = project.name ?? path.basename(project.canonical)
|
||||||
const footer = abbreviateHome(project.canonical, paths.home)
|
const footer = abbreviateHome(project.canonical, paths.home)
|
||||||
// Dialog padding, the gutter column, title padding, and the separating space use nine columns.
|
const width =
|
||||||
const width = Math.min(60, dimensions().width - 2) - 9 - stringWidth(title)
|
dialogSelectContentWidth(Math.min(dialogWidth("large"), dimensions().width - 2)) - stringWidth(title)
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
footer: truncateFilePath(footer, width),
|
footer: truncateFilePath(footer, width),
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,11 @@ export interface DialogSelectOption<T = any> {
|
||||||
onSelect?: (ctx: DialogContext) => void
|
onSelect?: (ctx: DialogContext) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function dialogSelectContentWidth(dialogWidth: number) {
|
||||||
|
// Scroll padding, row padding, the gutter, title padding, and the separating gap.
|
||||||
|
return dialogWidth - 12
|
||||||
|
}
|
||||||
|
|
||||||
export type DialogSelectRef<T> = {
|
export type DialogSelectRef<T> = {
|
||||||
filter: string
|
filter: string
|
||||||
filtered: DialogSelectOption<T>[]
|
filtered: DialogSelectOption<T>[]
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,17 @@ import { useToast } from "./toast"
|
||||||
import { useClipboard } from "../context/clipboard"
|
import { useClipboard } from "../context/clipboard"
|
||||||
import { useConfig } from "../config"
|
import { useConfig } from "../config"
|
||||||
|
|
||||||
|
export type DialogSize = "medium" | "large" | "xlarge"
|
||||||
|
|
||||||
|
export function dialogWidth(size: DialogSize) {
|
||||||
|
if (size === "xlarge") return 116
|
||||||
|
if (size === "large") return 88
|
||||||
|
return 60
|
||||||
|
}
|
||||||
|
|
||||||
export function Dialog(
|
export function Dialog(
|
||||||
props: ParentProps<{
|
props: ParentProps<{
|
||||||
size?: "medium" | "large" | "xlarge"
|
size?: DialogSize
|
||||||
centered?: boolean
|
centered?: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}>,
|
}>,
|
||||||
|
|
@ -20,12 +28,6 @@ export function Dialog(
|
||||||
const renderer = useRenderer()
|
const renderer = useRenderer()
|
||||||
|
|
||||||
let dismiss = false
|
let dismiss = false
|
||||||
const width = () => {
|
|
||||||
if (props.size === "xlarge") return 116
|
|
||||||
if (props.size === "large") return 88
|
|
||||||
return 60
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
onMouseDown={() => {
|
onMouseDown={() => {
|
||||||
|
|
@ -57,7 +59,7 @@ export function Dialog(
|
||||||
dismiss = false
|
dismiss = false
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
}}
|
}}
|
||||||
width={width()}
|
width={dialogWidth(props.size ?? "medium")}
|
||||||
maxWidth={dimensions().width - 2}
|
maxWidth={dimensions().width - 2}
|
||||||
backgroundColor={theme.background.default}
|
backgroundColor={theme.background.default}
|
||||||
paddingTop={1}
|
paddingTop={1}
|
||||||
|
|
@ -74,7 +76,7 @@ function init() {
|
||||||
element: JSX.Element
|
element: JSX.Element
|
||||||
onClose?: () => void
|
onClose?: () => void
|
||||||
}[],
|
}[],
|
||||||
size: "medium" as "medium" | "large" | "xlarge",
|
size: "medium" as DialogSize,
|
||||||
centered: false,
|
centered: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@ import { expect, test } from "bun:test"
|
||||||
import { mkdir } from "node:fs/promises"
|
import { mkdir } from "node:fs/promises"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { createSignal, onCleanup, onMount } from "solid-js"
|
import { createSignal, onCleanup, onMount } from "solid-js"
|
||||||
import type { DialogSelectOption } from "../../../src/ui/dialog-select"
|
import { dialogWidth } from "../../../src/ui/dialog"
|
||||||
|
import { dialogSelectContentWidth, type DialogSelectOption } from "../../../src/ui/dialog-select"
|
||||||
|
import { truncateFilePath } from "../../../src/ui/file-path"
|
||||||
|
import { stringWidth } from "../../../src/util/string-width"
|
||||||
import { tmpdir } from "../../fixture/fixture"
|
import { tmpdir } from "../../fixture/fixture"
|
||||||
import { TestTuiContexts } from "../../fixture/tui-environment"
|
import { TestTuiContexts } from "../../fixture/tui-environment"
|
||||||
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
|
||||||
|
|
@ -147,6 +150,28 @@ async function mountSelect(root: string, initial: DialogSelectOption<string>[],
|
||||||
return { app, moved, replaceOptions, selected }
|
return { app, moved, replaceOptions, selected }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test("budgets option content for constrained and full-width large dialogs", () => {
|
||||||
|
expect(dialogSelectContentWidth(Math.min(dialogWidth("large"), 62 - 2)) - 7).toBe(41)
|
||||||
|
expect(dialogSelectContentWidth(Math.min(dialogWidth("large"), 100 - 2)) - 7).toBe(69)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("renders the complete truncated footer within the option row", async () => {
|
||||||
|
await using tmp = await tmpdir()
|
||||||
|
const title = "Project"
|
||||||
|
const footer = truncateFilePath(
|
||||||
|
"/tmp/opencode/projects/a-very-long-project-directory/distinctive-tail.tsx",
|
||||||
|
dialogSelectContentWidth(dialogWidth("medium")) - stringWidth(title),
|
||||||
|
)
|
||||||
|
const select = await mountSelect(tmp.path, [{ title, footer, value: "project" }])
|
||||||
|
|
||||||
|
try {
|
||||||
|
await select.app.waitForFrame((frame) => frame.includes(footer))
|
||||||
|
expect(select.app.captureCharFrame()).toContain(footer)
|
||||||
|
} finally {
|
||||||
|
select.app.renderer.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("renders actions with a current selection", async () => {
|
test("renders actions with a current selection", async () => {
|
||||||
await using tmp = await tmpdir()
|
await using tmp = await tmpdir()
|
||||||
const app = await renderSelect(
|
const app = await renderSelect(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue