Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/cli/src/services/update-preflight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ async function open(from?: string): Promise<Session> {
screenMode: "split-footer",
footerHeight: 4,
targetFps: 60,
useKittyKeyboard: {},
useKittyKeyboard: { events: true, allKeysAsEscapes: true, reportText: true },
consoleOptions: {
keyBindings: [{ name: "y", ctrl: true, action: "copy-selection" }],
},
Expand Down
1 change: 1 addition & 0 deletions packages/theme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"scripts": {
"build": "bun run script/build.ts",
"test": "bun test --timeout 30000",
"typecheck": "tsgo --noEmit"
},
"dependencies": {
Expand Down
12 changes: 12 additions & 0 deletions packages/theme/src/tui/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ export const DEFAULT_THEME = {
$disabled: "$hue.neutral.500",
$selected: "$hue.interactive.700",
},
status: {
running: "$hue.interactive.800",
question: "$text.feedback.info.default",
permission: "$text.feedback.warning.default",
unread: "$hue.accent.800",
},
feedback: {
error: { default: "$hue.red.700", subdued: "$hue.red.600" },
warning: { default: "$hue.yellow.800", subdued: "$hue.yellow.700" },
Expand Down Expand Up @@ -336,6 +342,12 @@ export const DEFAULT_THEME = {
$disabled: "$hue.neutral.500",
$selected: "$hue.interactive.500",
},
status: {
running: "$hue.interactive.200",
question: "$text.feedback.info.default",
permission: "$text.feedback.warning.default",
unread: "$hue.accent.200",
},
feedback: {
error: { default: "$hue.red.300", subdued: "$hue.red.400" },
warning: { default: "$hue.yellow.200", subdued: "$hue.yellow.300" },
Expand Down
6 changes: 4 additions & 2 deletions packages/theme/src/tui/fallback.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type { ThemeTokensDefinition } from "./index.js"
import type { Mode, ThemeTokensDefinition } from "./index.js"
import { DEFAULT_THEME } from "./defaults.js"
import { ActionVariant, FeedbackKind } from "./schema.js"

export function fallback(): ThemeTokensDefinition {
export function fallback(mode: Mode): ThemeTokensDefinition {
const red = "#ff0000"

return {
text: {
default: red,
action: Object.fromEntries(ActionVariant.literals.map((variant) => [variant, { default: red }])),
formfield: { default: red },
status: DEFAULT_THEME[mode].text.status,
feedback: Object.fromEntries(FeedbackKind.literals.map((kind) => [kind, { default: red }])),
},
background: {
Expand Down
2 changes: 1 addition & 1 deletion packages/theme/src/tui/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function resolveThemeDocument(document: ThemeDocument, mode?: "light" | "
const selected = selectThemeMode(document, mode)
const definition = selected.expanded ? selected.theme : expandTheme(selected.theme)
const defaults = expandTheme(selectTheme(DEFAULT_THEME, selected.mode))
const core = expandTokens(fallback())
const core = expandTokens(fallback(selected.mode))
const merged = document.standalone ? mergeTheme(core, definition) : mergeTheme(core, defaults, definition)
if (!merged["hue"]) throw new Error("Standalone themes must provide hues")
return resolveExpandedTheme({
Expand Down
8 changes: 8 additions & 0 deletions packages/theme/src/tui/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ const TextDefinition = Schema.Struct({
subdued: Schema.optional(ColorValue),
action: Schema.optional(ActionColorDefinition),
formfield: Schema.optional(StatefulColorDefinition),
status: Schema.optional(
Schema.Struct({
running: Schema.optional(ColorValue),
question: Schema.optional(ColorValue),
permission: Schema.optional(ColorValue),
unread: Schema.optional(ColorValue),
}),
),
feedback: Schema.optional(
Schema.Struct({
error: Schema.optional(TextFeedbackDefinition),
Expand Down
6 changes: 6 additions & 0 deletions packages/theme/src/tui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export type ResolvedThemeTokens = {
readonly subdued: RGBA
readonly action: Readonly<Record<ActionVariant, StatefulColor>>
readonly formfield: FormfieldColor
readonly status: {
readonly running: RGBA
readonly question: RGBA
readonly permission: RGBA
readonly unread: RGBA
}
readonly feedback: Readonly<Record<FeedbackKind, { readonly default: RGBA; readonly subdued: RGBA }>>
}
readonly background: {
Expand Down
42 changes: 42 additions & 0 deletions packages/theme/test/status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { expect, test } from "bun:test"
import { RGBA } from "@opentui/core"
import { Schema } from "effect"
import { DEFAULT_THEME, ThemeDocument, migrateV1, resolveThemeDocument } from "../src/tui/index.js"
import type { ThemeV1Json } from "../src/tui/v1.js"

test.each(["light", "dark"] as const)("built-in %s themes resolve status colors", async (mode) => {
const source: ThemeV1Json = await Bun.file(
new URL("../../tui/src/theme/assets/opencode.json", import.meta.url),
).json()
for (const document of [DEFAULT_THEME, migrateV1(source)]) {
const theme = resolveThemeDocument(document, mode)
expect(theme.text.status.running.equals(theme.hue.interactive[mode === "light" ? 800 : 200])).toBeTrue()
expect(theme.text.status.question.equals(theme.text.feedback.info.default)).toBeTrue()
expect(theme.text.status.permission.equals(theme.text.feedback.warning.default)).toBeTrue()
expect(theme.text.status.unread.equals(theme.hue.accent[mode === "light" ? 800 : 200])).toBeTrue()
expect(theme.contextual.elevated.text.status).toEqual(theme.text.status)
}
})

test.each(["light", "dark"] as const)("custom %s themes inherit and override status colors", (mode) => {
for (const standalone of [false, true]) {
const theme = resolveThemeDocument(
Schema.decodeUnknownSync(ThemeDocument)({
version: 2,
standalone,
[mode]: {
hue: { ...DEFAULT_THEME[mode].hue, interactive: "$hue.purple", accent: "$hue.orange" },
text: {
feedback: { warning: { default: "#654321" } },
status: { question: "#123456" },
},
},
}),
mode,
)
expect(theme.text.status.running.equals(theme.hue.purple[mode === "light" ? 800 : 200])).toBeTrue()
expect(theme.text.status.unread.equals(theme.hue.orange[mode === "light" ? 800 : 200])).toBeTrue()
expect(theme.text.status.question.equals(RGBA.fromHex("#123456"))).toBeTrue()
expect(theme.text.status.permission.equals(RGBA.fromHex("#654321"))).toBeTrue()
}
})
2 changes: 1 addition & 1 deletion packages/tui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
targetFps: 60,
gatherStats: false,
exitOnCtrlC: false,
useKittyKeyboard: {},
useKittyKeyboard: { events: true, allKeysAsEscapes: true, reportText: true },
autoFocus: false,
openConsoleOnError: false,
useMouse: config.mouse,
Expand Down
9 changes: 9 additions & 0 deletions packages/tui/src/component/dialog-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ export const settings: Setting[] = [
values: ["horizontal", "vertical"],
keywords: ["sidebar", "orientation", "left"],
},
{
title: "Indicators",
category: "Tabs",
path: ["tabs", "indicators"],
default: "status",
values: ["status", "numbers"],
labels: ["status icons", "always show numbers"],
keywords: ["tab numbers", "number mode", "status icons"],
},
{
title: "Layout",
category: "Diffs",
Expand Down
Loading
Loading