fix: up highlight made legible for transparent bg, bring back mouse nav
This commit is contained in:
@@ -50,6 +50,7 @@ export function App() {
|
||||
});
|
||||
|
||||
const backgroundColor = () =>
|
||||
themeContext.transparentBackground() ||
|
||||
themeContext.selected === "system"
|
||||
? "transparent"
|
||||
: themeContext.theme.surface;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/**
|
||||
* PaneRow — the shared parent | current | preview 3-pane layout primitive.
|
||||
*
|
||||
* Implements yazi's `mgr.ratio = [1, 3, 3]` contract: three bordered columns
|
||||
* grow at 1/7 : 3/7 : 3/7 of the row width via Yoga `flexGrow`, so every list
|
||||
* Implements yazi's `mgr.ratio = [1, 2, 2]` contract: three bordered columns
|
||||
* grow at 1/5 : 2/5 : 2/5 of the row width via Yoga `flexGrow`, so every list
|
||||
* tab renders an identical, layout-stable shell. Columns use `flexBasis={0}`
|
||||
* so the ratio is exact regardless of content width — a column's content can
|
||||
* never stretch its slot.
|
||||
*
|
||||
* Column semantics (per the yazi depth model):
|
||||
* parent — the previous-depth list. Renders a muted `—` placeholder and
|
||||
* KEEPS its 1/7 slot when blank (never collapses to width 0).
|
||||
* KEEPS its 1/5 slot when blank (never collapses to width 0).
|
||||
* current — the current-depth list. The only focusable content column; it
|
||||
* carries the active-border focus ring when `focused` is truthy.
|
||||
* preview — detail of the hovered item in `current`; always muted border.
|
||||
@@ -43,7 +43,7 @@ type PaneLabel = string | (() => string);
|
||||
|
||||
export type PaneRowProps = {
|
||||
/** Parent column content (previous-depth list, or null for a muted
|
||||
* placeholder — the 1/7 slot is always preserved). */
|
||||
* placeholder — the 1/5 slot is always preserved). */
|
||||
parent?: PaneContent;
|
||||
/** Current column content (the focused list). */
|
||||
current?: PaneContent;
|
||||
@@ -99,7 +99,8 @@ function Pane(props: {
|
||||
borderColor: () => RGBA;
|
||||
scrollFocused: () => boolean;
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
const themeContext = useTheme();
|
||||
const theme = themeContext.theme;
|
||||
const muted = () => theme.muted ?? theme.textMuted ?? theme.text;
|
||||
|
||||
// Memoize accessor results so the prop expressions below stay reactive
|
||||
@@ -115,7 +116,15 @@ function Pane(props: {
|
||||
height="100%"
|
||||
>
|
||||
{/* ── slim header label row ─────────────────────────────────────────── */}
|
||||
<box height={1} paddingLeft={1} backgroundColor={theme.background}>
|
||||
<box
|
||||
height={1}
|
||||
paddingLeft={1}
|
||||
backgroundColor={
|
||||
themeContext.transparentBackground()
|
||||
? "transparent"
|
||||
: theme.background
|
||||
}
|
||||
>
|
||||
<text fg={theme.textSecondary}>{props.label()}</text>
|
||||
</box>
|
||||
{/* ── bordered scrollbox ────────────────────────────────────────────── */}
|
||||
@@ -124,7 +133,11 @@ function Pane(props: {
|
||||
focused={scrollFocused()}
|
||||
border
|
||||
borderColor={borderColor()}
|
||||
backgroundColor={theme.background}
|
||||
backgroundColor={
|
||||
themeContext.transparentBackground()
|
||||
? "transparent"
|
||||
: theme.background
|
||||
}
|
||||
>
|
||||
{props.content() ?? <Placeholder color={muted} />}
|
||||
</scrollbox>
|
||||
@@ -163,7 +176,7 @@ export function PaneRow(props: PaneRowProps) {
|
||||
|
||||
return (
|
||||
<box flexDirection="row" flexGrow={1} width="100%" height="100%">
|
||||
{/* ── parent (1/7) — previous-depth list; always muted ─────────────── */}
|
||||
{/* ── parent (1/5) — previous-depth list; always muted ─────────────── */}
|
||||
<Pane
|
||||
grow={PANE_RATIO.parent}
|
||||
label={parentLabel}
|
||||
@@ -179,7 +192,7 @@ export function PaneRow(props: PaneRowProps) {
|
||||
borderColor={() => (focused() ? theme.borderActive : theme.border)}
|
||||
scrollFocused={() => focused()}
|
||||
/>
|
||||
{/* ── preview (3/7) — hovered-item detail; always muted ────────────── */}
|
||||
{/* ── preview (2/5) — hovered-item detail; always muted ────────────── */}
|
||||
<Show when={panes() === 3}>
|
||||
<Pane
|
||||
grow={PANE_RATIO.preview}
|
||||
|
||||
@@ -20,7 +20,8 @@ export const SelectableBox: ParentComponent<
|
||||
backgroundColor={
|
||||
props.selected()
|
||||
? theme.primary
|
||||
: themeContext.selected === "system"
|
||||
: themeContext.transparentBackground() ||
|
||||
themeContext.selected === "system"
|
||||
? "transparent"
|
||||
: themeContext.theme.surface
|
||||
}
|
||||
|
||||
@@ -241,11 +241,13 @@ export function Shell() {
|
||||
|
||||
return (
|
||||
<box
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
height="100%"
|
||||
backgroundColor={t.surface}
|
||||
>
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
height="100%"
|
||||
backgroundColor={
|
||||
theme.transparentBackground() ? "transparent" : t.surface
|
||||
}
|
||||
>
|
||||
{/* ── Middle row: tab list (pane 0) + active tab content (panes 1..n) ─ */}
|
||||
<box flexDirection="row" flexGrow={1} width="100%">
|
||||
<Show
|
||||
@@ -281,7 +283,11 @@ export function Shell() {
|
||||
flexDirection="row"
|
||||
height={1}
|
||||
width="100%"
|
||||
backgroundColor={t.backgroundPanel ?? t.background}
|
||||
backgroundColor={
|
||||
theme.transparentBackground()
|
||||
? "transparent"
|
||||
: (t.backgroundPanel ?? t.background)
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={nav.mode() === NavMode.COMMAND}
|
||||
|
||||
@@ -53,7 +53,11 @@ export function TabListPane(props: { muted?: boolean }) {
|
||||
? theme.border
|
||||
: undefined;
|
||||
const focusFg = (t: TABS) =>
|
||||
t === cursor() && active() ? theme.surface : theme.text;
|
||||
t === cursor() && active()
|
||||
? theme.surface
|
||||
: t === cursor()
|
||||
? theme.selectedListItemText ?? theme.text
|
||||
: theme.text;
|
||||
|
||||
return (
|
||||
<For each={TAB_ORDER}>
|
||||
@@ -77,6 +81,14 @@ export function TabListPane(props: { muted?: boolean }) {
|
||||
flexDirection="row"
|
||||
paddingRight={1}
|
||||
backgroundColor={focusBg(tab)}
|
||||
onMouseDown={() => {
|
||||
// Click = hover + open, the yazi "open" of the row
|
||||
// (switches to the tab and enters its content), the same
|
||||
// as l/Enter. Restores mouse support the tab-strip
|
||||
// refactor dropped.
|
||||
nav.setTabCursor(tab);
|
||||
nav.activateTabCursor();
|
||||
}}
|
||||
>
|
||||
{/* ── selection marker (j/k cursor) ─────────────────────────── */}
|
||||
<text fg={focusFg(tab)}>{isCursor() ? "❯" : " "}</text>
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
//
|
||||
// Yazi heritage: j/k move, h/l swipe between panes, Enter open, Space select,
|
||||
// v visual mode, gg/G top/bottom, [ ] switch tabs, 1-6 goto tab,
|
||||
// : command bar, q quit, ~ help. Audio transport kept on shifted keys / ctrl.
|
||||
// : / q command palette (q + Enter quits there), Q quick quit, ~ help.
|
||||
// Audio transport kept on shifted keys / ctrl.
|
||||
|
||||
// ── Movement (within a pane) ─────────────────────────────────────────────
|
||||
"move-down": ["j", "down"],
|
||||
@@ -50,9 +51,11 @@
|
||||
"tab-goto-5": ["5"],
|
||||
"tab-goto-6": ["6"],
|
||||
|
||||
// ── Command bar & help & quit ────────────────────────────────────────────
|
||||
"command": [":"],
|
||||
"quit": ["q", "ctrl-c"],
|
||||
// ── Command palette & help & quit ────────────────────────────────────────
|
||||
// q opens the command palette (neovim-style: type q + Enter to quit there).
|
||||
// Q (shift+q) is the instant quick quit. ctrl-c also quits.
|
||||
"command": [":", "q"],
|
||||
"quit": ["Q", "ctrl-c"],
|
||||
"help": ["~", "f1"],
|
||||
|
||||
// ── List operations (yazi: s search, f filter, , sort, . hidden, r refresh)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createEffect, createMemo, onMount, onCleanup } from "solid-js";
|
||||
import { createStore, produce } from "solid-js/store";
|
||||
import { useRenderer } from "@opentui/solid";
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
generateSubtleSyntax,
|
||||
} from "../utils/syntax-highlighter";
|
||||
import { resolveTerminalTheme, loadThemes } from "../utils/theme";
|
||||
import { detectModeFromBackground } from "../utils/system-theme";
|
||||
import { createSimpleContext } from "./helper";
|
||||
import {
|
||||
setupThemeSignalHandler,
|
||||
@@ -84,6 +86,8 @@ export type ThemeResolved = {
|
||||
muted?: RGBA;
|
||||
surface?: RGBA;
|
||||
selectedListItemText?: RGBA;
|
||||
/** Theme declares a transparent (terminal-bg-visible) background. */
|
||||
transparent?: boolean;
|
||||
layerBackgrounds?: {
|
||||
layer0: RGBA;
|
||||
layer1: RGBA;
|
||||
@@ -94,6 +98,61 @@ export type ThemeResolved = {
|
||||
thinkingOpacity?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* A TerminalColors with no values — used to keep the "system" theme rendering
|
||||
* with default ANSI colors + the detected dark/light mode when the terminal
|
||||
* cannot answer OSC queries (e.g. inside tmux without OSC forwarding).
|
||||
*/
|
||||
const EMPTY_TERMINAL_COLORS: TerminalColors = {
|
||||
palette: Array.from({ length: 16 }, () => null),
|
||||
defaultForeground: null,
|
||||
defaultBackground: null,
|
||||
cursorColor: null,
|
||||
mouseForeground: null,
|
||||
mouseBackground: null,
|
||||
tekForeground: null,
|
||||
tekBackground: null,
|
||||
highlightBackground: null,
|
||||
highlightForeground: null,
|
||||
};
|
||||
|
||||
/** Cached macOS appearance (dark/light), independent of the terminal. */
|
||||
let cachedOsMode: "dark" | "light" | null = null;
|
||||
|
||||
/**
|
||||
* Detect the terminal's dark/light mode.
|
||||
*
|
||||
* Priority:
|
||||
* 1. The terminal's real background color (OSC 11 response) — terminal-specific.
|
||||
* 2. The macOS appearance via `defaults read -g AppleInterfaceStyle` — works
|
||||
* even inside tmux, where OSC queries are usually not forwarded.
|
||||
* An unset value means light mode (macOS defaults to light).
|
||||
* 3. null → keep whatever mode is currently active.
|
||||
*/
|
||||
function detectSystemMode(
|
||||
colors: TerminalColors | null,
|
||||
): "dark" | "light" | null {
|
||||
const fromBg = detectModeFromBackground(colors?.defaultBackground);
|
||||
if (fromBg) return fromBg;
|
||||
|
||||
if (process.platform === "darwin" && cachedOsMode === null) {
|
||||
let style: string | null = null;
|
||||
try {
|
||||
style = execFileSync("defaults", ["read", "-g", "AppleInterfaceStyle"], {
|
||||
encoding: "utf8",
|
||||
timeout: 2000,
|
||||
})
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
} catch {
|
||||
// Unset → light appearance (macOS default).
|
||||
}
|
||||
cachedOsMode = style?.includes("dark") ? "dark" : "light";
|
||||
}
|
||||
|
||||
return cachedOsMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme context using the createSimpleContext pattern.
|
||||
*
|
||||
@@ -195,6 +254,16 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
}
|
||||
}
|
||||
|
||||
// ── dark/light mode detection ─────────────────────────────────────────
|
||||
// The provider starts with a hardcoded mode (e.g. "dark"); detect the
|
||||
// real one from the terminal's background color (OSC 11) or, when that
|
||||
// is unavailable (tmux without OSC forwarding), the OS appearance.
|
||||
const detectedMode = detectSystemMode(colors);
|
||||
if (detectedMode && detectedMode !== store.mode) {
|
||||
setStore("mode", detectedMode);
|
||||
emitThemeModeChanged(detectedMode);
|
||||
}
|
||||
|
||||
const hasPalette = Boolean(
|
||||
colors?.palette?.some((value) => Boolean(value)),
|
||||
);
|
||||
@@ -203,13 +272,14 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
);
|
||||
|
||||
if (!hasPalette && !hasDefaultColors) {
|
||||
// No system colors available, fall back to default
|
||||
// This happens when the terminal doesn't support OSC palette queries
|
||||
// (e.g., running inside tmux, or on unsupported terminals)
|
||||
// No system colors available — the terminal can't answer OSC queries
|
||||
// (e.g. inside tmux, or unsupported terminals). Keep the "system"
|
||||
// theme anyway: the detected dark/light mode plus default ANSI colors
|
||||
// still produce a usable, mode-correct palette.
|
||||
if (store.active === "system") {
|
||||
setStore(
|
||||
produce((draft) => {
|
||||
draft.active = "catppuccin";
|
||||
draft.system = colors ?? EMPTY_TERMINAL_COLORS;
|
||||
draft.ready = true;
|
||||
}),
|
||||
);
|
||||
@@ -293,6 +363,15 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
|
||||
mode() {
|
||||
return store.mode;
|
||||
},
|
||||
/** Whether the app background should be transparent (no solid fill):
|
||||
* either the global preference is on, or the selected theme declares
|
||||
* transparency (e.g. the system theme). */
|
||||
transparentBackground() {
|
||||
return (
|
||||
appStore.state().settings.transparentBackground ||
|
||||
values().transparent === true
|
||||
);
|
||||
},
|
||||
setMode(mode: "dark" | "light") {
|
||||
setStore("mode", mode);
|
||||
emitThemeModeChanged(mode);
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
*
|
||||
* parent | current | preview
|
||||
*
|
||||
* Layout ratios (1/7 : 3/7 : 3/7 in the final remake) live in
|
||||
* Layout ratios (1/5 : 2/5 : 2/5 in the final remake) live in
|
||||
* `@/utils/navigation` (PANE_RATIO). This module owns only the *focusable*
|
||||
* nav model — which column is focused and where its list cursor lives. The
|
||||
* parent/preview columns are always derived, never focused.
|
||||
@@ -267,6 +267,9 @@ export function createNavigation() {
|
||||
/** The tab the root's cursor is hovering (independent of activeTab). */
|
||||
const tabCursor = (): TABS => tabCursorSignal();
|
||||
|
||||
/** Directly set the root's tab cursor (e.g. a mouse click on a tab row). */
|
||||
const setTabCursorTo = (tab: TABS) => setTabCursor(tab);
|
||||
|
||||
/** Move the root's cursor to the adjacent tab (clamped, no wrap). */
|
||||
const moveTabCursor = (dir: -1 | 1) => {
|
||||
setTabCursor((c) => Math.max(1, Math.min(TabsCount, c + dir)) as TABS);
|
||||
@@ -469,6 +472,7 @@ export function createNavigation() {
|
||||
enterTabContent,
|
||||
backToTabRoot,
|
||||
tabCursor,
|
||||
setTabCursor: setTabCursorTo,
|
||||
moveTabCursor,
|
||||
activateTabCursor,
|
||||
// pane focus
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* DiscoverPage — yazi depth-stack view of discoverable podcasts.
|
||||
*
|
||||
* depth 0 (current) — category list. Parent pane shows the muted
|
||||
* placeholder (1/7 slot kept).
|
||||
* placeholder (1/5 slot kept).
|
||||
* depth 1 (current) — podcast results for the drilled category. Parent
|
||||
* pane = the categories list.
|
||||
* preview — detail of the hovered item (category summary, or
|
||||
@@ -146,7 +146,11 @@ function DiscoverPage() {
|
||||
const focusBg = (i: number, lf: number, active: boolean) =>
|
||||
i === lf && active ? theme.primary : i === lf ? theme.border : undefined;
|
||||
const focusFg = (i: number, lf: number, active: boolean) =>
|
||||
i === lf && active ? theme.surface : theme.text;
|
||||
i === lf && active
|
||||
? theme.surface
|
||||
: i === lf
|
||||
? theme.selectedListItemText ?? theme.text
|
||||
: theme.text;
|
||||
|
||||
const currentLabel = () =>
|
||||
depth() === 0
|
||||
|
||||
@@ -168,7 +168,11 @@ function FeedPage() {
|
||||
? theme.border
|
||||
: undefined;
|
||||
const focusFg = (i: number, listFocus: number, active: boolean) =>
|
||||
i === listFocus && active ? theme.surface : theme.text;
|
||||
i === listFocus && active
|
||||
? theme.surface
|
||||
: i === listFocus
|
||||
? theme.selectedListItemText ?? theme.text
|
||||
: theme.text;
|
||||
|
||||
const currentLabel = () => `Feed · ${episodes().length}`;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* MyShowsPage — yazi depth-stack view of subscribed shows.
|
||||
*
|
||||
* depth 0 (current) — subscribed shows. Parent pane shows the muted
|
||||
* placeholder (1/7 slot kept).
|
||||
* placeholder (1/5 slot kept).
|
||||
* depth 1 (current) — episodes of the drilled show. Parent pane = shows.
|
||||
* preview — detail of the hovered item in the current column.
|
||||
*
|
||||
@@ -199,7 +199,11 @@ export function MyShowsPage() {
|
||||
const focusBg = (i: number, lf: number, active: boolean) =>
|
||||
i === lf && active ? theme.primary : i === lf ? theme.border : undefined;
|
||||
const focusFg = (i: number, lf: number, active: boolean) =>
|
||||
i === lf && active ? theme.surface : theme.text;
|
||||
i === lf && active
|
||||
? theme.surface
|
||||
: i === lf
|
||||
? theme.selectedListItemText ?? theme.text
|
||||
: theme.text;
|
||||
const showTitle = (f: Feed) => f.customName || f.podcast.title;
|
||||
|
||||
const currentLabel = () =>
|
||||
|
||||
@@ -205,7 +205,11 @@ function SearchPage() {
|
||||
? theme.border
|
||||
: undefined;
|
||||
const focusFg = (i: number, listFocus: number, active: boolean) =>
|
||||
i === listFocus && active ? theme.surface : theme.text;
|
||||
i === listFocus && active
|
||||
? theme.surface
|
||||
: i === listFocus
|
||||
? theme.selectedListItemText ?? theme.text
|
||||
: theme.text;
|
||||
|
||||
// ── parent pane: previous-depth content (tab list at depth 0) ──────────────
|
||||
const parentContent = () => (
|
||||
|
||||
@@ -39,6 +39,19 @@ export function usePreferencesItems(): SettingItem[] {
|
||||
app.setTheme(THEME_LABELS[next].value);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "transparentBackground",
|
||||
label: "Transparent Background",
|
||||
kind: "toggle",
|
||||
display: () =>
|
||||
settings().transparentBackground ? "On" : "Off",
|
||||
help: () =>
|
||||
`Let the terminal's own background show through (no app background fill).\nType: toggle\nDefault: false\nCurrent: ${settings().transparentBackground ? "On" : "Off"}\nSpace/Enter to toggle.`,
|
||||
toggle: () =>
|
||||
app.updateSettings({
|
||||
transparentBackground: !settings().transparentBackground,
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: "fontSize",
|
||||
label: "Font Size",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*
|
||||
* Renders entirely through `<PaneRow>` (parent | current | preview):
|
||||
* parent = previous depth's list (sections at depth 1, items at depth 2);
|
||||
* blank placeholder at depth 0 (1/7 slot kept).
|
||||
* blank placeholder at depth 0 (1/5 slot kept).
|
||||
* current = the current-depth list (or editor at depth 2); the only
|
||||
* focusable column.
|
||||
* preview = help/preview text for the hovered item in current.
|
||||
@@ -424,7 +424,12 @@ function Row(props: {
|
||||
: props.focused
|
||||
? theme.border
|
||||
: undefined;
|
||||
const fg = () => (props.focused && props.active ? theme.surface : theme.text);
|
||||
const fg = () =>
|
||||
props.focused && props.active
|
||||
? theme.surface
|
||||
: props.focused
|
||||
? theme.selectedListItemText ?? theme.text
|
||||
: theme.text;
|
||||
const ref = useScrollIntoView(() => props.focused);
|
||||
return (
|
||||
<box
|
||||
|
||||
@@ -29,6 +29,7 @@ const defaultSettings: AppSettings = {
|
||||
fontSize: 14,
|
||||
playbackSpeed: 1,
|
||||
downloadPath: "",
|
||||
transparentBackground: false,
|
||||
visualizer: defaultVisualizerSettings,
|
||||
};
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@ export type AppSettings = {
|
||||
fontSize: number;
|
||||
playbackSpeed: number;
|
||||
downloadPath: string;
|
||||
/** Render the app background transparent (let the terminal's own bg show). */
|
||||
transparentBackground: boolean;
|
||||
visualizer: VisualizerSettings;
|
||||
};
|
||||
|
||||
|
||||
@@ -13,10 +13,12 @@ export type ColorValue = HexColor | RefName | Variant | RGBA | number
|
||||
export type ThemeJson = {
|
||||
$schema?: string
|
||||
defs?: Record<string, HexColor | RefName>
|
||||
theme: Record<string, ColorValue> & {
|
||||
theme: Record<string, ColorValue | boolean> & {
|
||||
selectedListItemText?: ColorValue
|
||||
backgroundMenu?: ColorValue
|
||||
thinkingOpacity?: number
|
||||
/** Render the app background transparent (let the terminal's own bg show). */
|
||||
transparent?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -179,9 +179,8 @@ export function CommandProvider(props: ParentProps) {
|
||||
const dialog = useDialog();
|
||||
const keybind = useKeybinds();
|
||||
|
||||
// Open the command palette via the `command` keybind (bound to `:` in
|
||||
// keybinds.jsonc). The old hardcoded "command_list" name was never a
|
||||
// canonical action, so the palette was unreachable dead code.
|
||||
// Open the command palette via the `command` keybind (bound to `:` or `q`
|
||||
// in keybinds.jsonc; the Shell router owns the action and runs it first).
|
||||
useKeyboard((evt) => {
|
||||
if (value.suspended()) return;
|
||||
if (dialog.isOpen) return;
|
||||
|
||||
@@ -32,6 +32,7 @@ const defaultSettings: AppSettings = {
|
||||
fontSize: 14,
|
||||
playbackSpeed: 1,
|
||||
downloadPath: "",
|
||||
transparentBackground: false,
|
||||
visualizer: defaultVisualizerSettings,
|
||||
};
|
||||
|
||||
|
||||
@@ -53,9 +53,10 @@ const DEFAULT_KEYBINDS: KeybindsResolved = {
|
||||
"tab-goto-4": ["4"],
|
||||
"tab-goto-5": ["5"],
|
||||
"tab-goto-6": ["6"],
|
||||
// command / help / quit
|
||||
command: [":"],
|
||||
quit: ["q", "ctrl-c"],
|
||||
// command palette / help / quit
|
||||
// q opens the palette (type q + Enter to quit there); Q is the quick quit.
|
||||
command: [":", "q"],
|
||||
quit: ["Q", "ctrl-c"],
|
||||
help: ["~", "f1"],
|
||||
// list ops
|
||||
search: ["s"],
|
||||
|
||||
@@ -57,13 +57,13 @@ export function rootFrameFor(
|
||||
// terminal size — more robust than fixed percentages and exactly mirrors
|
||||
// yazi's `mgr.ratio` config. Set a slot's ratio to 0 to hide it (2-pane tabs).
|
||||
//
|
||||
// NOTE (task 01 leave-behind): the nav-model task intentionally does NOT
|
||||
// touch these values. Task 02 re-tunes them to the remake target ratios
|
||||
// (parent : current : preview = 1 : 3 : 3 i.e. 1/7 : 3/7 : 3/7). Do it there.
|
||||
// Current ratios: parent : current : preview = 1 : 2 : 2, i.e. 1/5 : 2/5 : 2/5
|
||||
// (20% / 40% / 40% of the row width). 2-pane tabs drop the preview slot and
|
||||
// give `current` the combined 4/5.
|
||||
export const PANE_RATIO = {
|
||||
parent: 1,
|
||||
current: 3,
|
||||
preview: 3,
|
||||
current: 2,
|
||||
preview: 2,
|
||||
} as const;
|
||||
|
||||
// Number of *focusable* content panes per tab. The three visible columns
|
||||
|
||||
@@ -13,19 +13,40 @@ export function clearPaletteCache() {
|
||||
cached = null;
|
||||
}
|
||||
|
||||
/** Relative luminance of a hex color (0 = black, 1 = white). */
|
||||
function luminance(hex: string): number {
|
||||
const c = RGBA.fromHex(hex);
|
||||
return 0.299 * c.r + 0.587 * c.g + 0.114 * c.b;
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer the terminal's dark/light mode from its default background color
|
||||
* (the OSC 11 query response). Returns null when no background is available.
|
||||
*/
|
||||
export function detectModeFromBackground(
|
||||
background: string | null | undefined,
|
||||
): "dark" | "light" | null {
|
||||
if (!background) return null;
|
||||
return luminance(background) < 0.5 ? "dark" : "light";
|
||||
}
|
||||
|
||||
export function generateSystemTheme(
|
||||
colors: TerminalColors,
|
||||
mode: "dark" | "light",
|
||||
): ThemeJson {
|
||||
cached = colors;
|
||||
const isDark = mode === "dark";
|
||||
const bg = RGBA.fromHex(
|
||||
colors.defaultBackground ?? colors.palette[0] ?? "#000000",
|
||||
colors.defaultBackground ??
|
||||
colors.palette[0] ??
|
||||
(isDark ? "#000000" : "#ffffff"),
|
||||
);
|
||||
const fg = RGBA.fromHex(
|
||||
colors.defaultForeground ?? colors.palette[7] ?? "#ffffff",
|
||||
colors.defaultForeground ??
|
||||
colors.palette[7] ??
|
||||
(isDark ? "#ffffff" : "#000000"),
|
||||
);
|
||||
const transparent = RGBA.fromInts(0, 0, 0, 0);
|
||||
const isDark = mode === "dark";
|
||||
|
||||
const col = (i: number) => {
|
||||
const value = colors.palette[i];
|
||||
@@ -87,6 +108,7 @@ export function generateSystemTheme(
|
||||
textSelectedTertiary: selectedTertiary,
|
||||
selectedListItemText: bg,
|
||||
background: transparent,
|
||||
transparent: true,
|
||||
backgroundPanel: grays[2],
|
||||
backgroundElement: grays[3],
|
||||
backgroundMenu: grays[3],
|
||||
|
||||
@@ -18,7 +18,7 @@ export function resolveTheme(theme: ThemeJson, mode: ThemeMode) {
|
||||
if (value.startsWith("#")) return RGBA.fromHex(value)
|
||||
if (defs[value] != null) return resolveColor(defs[value])
|
||||
const ref = theme.theme[value]
|
||||
if (ref != null) return resolveColor(ref)
|
||||
if (ref != null && typeof ref !== "boolean") return resolveColor(ref)
|
||||
throw new Error(`Color reference "${value}" not found in defs or theme`)
|
||||
}
|
||||
return resolveColor(value[mode])
|
||||
@@ -26,8 +26,15 @@ export function resolveTheme(theme: ThemeJson, mode: ThemeMode) {
|
||||
|
||||
const resolved = Object.fromEntries(
|
||||
Object.entries(theme.theme)
|
||||
.filter(([key]) => key !== "selectedListItemText" && key !== "backgroundMenu" && key !== "thinkingOpacity")
|
||||
.map(([key, value]) => [key, resolveColor(value)])
|
||||
.filter(
|
||||
(entry): entry is [string, ColorValue] =>
|
||||
entry[0] !== "selectedListItemText" &&
|
||||
entry[0] !== "backgroundMenu" &&
|
||||
entry[0] !== "thinkingOpacity" &&
|
||||
entry[0] !== "transparent" &&
|
||||
typeof entry[1] !== "boolean",
|
||||
)
|
||||
.map(([key, value]) => [key, resolveColor(value)]),
|
||||
) as Record<string, RGBA>
|
||||
|
||||
const hasSelected = theme.theme.selectedListItemText !== undefined
|
||||
@@ -40,6 +47,7 @@ export function resolveTheme(theme: ThemeJson, mode: ThemeMode) {
|
||||
: resolved.backgroundElement
|
||||
|
||||
const thinkingOpacity = theme.theme.thinkingOpacity ?? 0.6
|
||||
const transparent = theme.theme.transparent === true
|
||||
|
||||
const background = resolved.background
|
||||
const backgroundPanel = resolved.backgroundPanel ?? background
|
||||
@@ -58,5 +66,6 @@ export function resolveTheme(theme: ThemeJson, mode: ThemeMode) {
|
||||
},
|
||||
_hasSelectedListItemText: hasSelected,
|
||||
thinkingOpacity,
|
||||
transparent,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user