add sub-bar usage widget extension with opencode-go support

This commit is contained in:
2026-03-12 12:30:09 +00:00
parent 9ad10a016c
commit 403650a437
70 changed files with 14775 additions and 6 deletions
@@ -0,0 +1,718 @@
/**
* Display settings UI helpers.
*/
import type { SettingItem } from "@mariozechner/pi-tui";
import type {
Settings,
BarStyle,
BarType,
ColorScheme,
BarCharacter,
DividerCharacter,
WidgetWrapping,
DisplayAlignment,
BarWidth,
DividerBlanks,
ProviderLabel,
BaseTextColor,
ResetTimeFormat,
ResetTimerContainment,
StatusIndicatorMode,
StatusIconPack,
DividerColor,
UsageColorTargets,
} from "../settings-types.js";
import {
BASE_COLOR_OPTIONS,
DIVIDER_COLOR_OPTIONS,
normalizeBaseTextColor,
normalizeDividerColor,
} from "../settings-types.js";
import { CUSTOM_OPTION } from "../ui/settings-list.js";
export function buildDisplayLayoutItems(settings: Settings): SettingItem[] {
return [
{
id: "showContextBar",
label: "Show Context Bar",
currentValue: settings.display.showContextBar ? "on" : "off",
values: ["on", "off"],
description: "Show context window usage as leftmost progress bar.",
},
{
id: "alignment",
label: "Alignment",
currentValue: settings.display.alignment,
values: ["left", "center", "right", "split"] as DisplayAlignment[],
description: "Align the usage line inside the widget.",
},
{
id: "overflow",
label: "Overflow",
currentValue: settings.display.overflow,
values: ["truncate", "wrap"] as WidgetWrapping[],
description: "Wrap the usage line or truncate with ellipsis (requires bar width ≠ fill and alignment ≠ split).",
},
{
id: "paddingLeft",
label: "Padding Left",
currentValue: String(settings.display.paddingLeft ?? 0),
values: ["0", "1", "2", "3", "4", CUSTOM_OPTION],
description: "Add left padding inside the widget.",
},
{
id: "paddingRight",
label: "Padding Right",
currentValue: String(settings.display.paddingRight ?? 0),
values: ["0", "1", "2", "3", "4", CUSTOM_OPTION],
description: "Add right padding inside the widget.",
},
];
}
export function buildDisplayResetItems(settings: Settings): SettingItem[] {
return [
{
id: "resetTimePosition",
label: "Reset Timer",
currentValue: settings.display.resetTimePosition,
values: ["off", "front", "back", "integrated"],
description: "Where to show the reset timer in each window.",
},
{
id: "resetTimeFormat",
label: "Reset Timer Format",
currentValue: settings.display.resetTimeFormat ?? "relative",
values: ["relative", "datetime"] as ResetTimeFormat[],
description: "Show relative countdown or reset datetime.",
},
{
id: "resetTimeContainment",
label: "Reset Timer Containment",
currentValue: settings.display.resetTimeContainment ?? "()",
values: ["none", "blank", "()", "[]", "<>", CUSTOM_OPTION] as ResetTimerContainment[],
description: "Wrapping characters for the reset timer (custom supported).",
},
];
}
export function resolveUsageColorTargets(targets?: UsageColorTargets): UsageColorTargets {
return {
title: targets?.title ?? true,
timer: targets?.timer ?? true,
bar: targets?.bar ?? true,
usageLabel: targets?.usageLabel ?? true,
status: targets?.status ?? true,
};
}
export function formatUsageColorTargetsSummary(targets?: UsageColorTargets): string {
const resolved = resolveUsageColorTargets(targets);
const enabled = [
resolved.title ? "Title" : null,
resolved.timer ? "Timer" : null,
resolved.bar ? "Bar" : null,
resolved.usageLabel ? "Usage label" : null,
resolved.status ? "Status" : null,
].filter(Boolean) as string[];
if (enabled.length === 0) return "off";
if (enabled.length === 5) return "all";
return enabled.join(", ");
}
export function buildUsageColorTargetItems(settings: Settings): SettingItem[] {
const targets = resolveUsageColorTargets(settings.display.usageColorTargets);
return [
{
id: "usageColorTitle",
label: "Title",
currentValue: targets.title ? "on" : "off",
values: ["on", "off"],
description: "Color the window title by usage.",
},
{
id: "usageColorTimer",
label: "Timer",
currentValue: targets.timer ? "on" : "off",
values: ["on", "off"],
description: "Color the reset timer by usage.",
},
{
id: "usageColorBar",
label: "Bar",
currentValue: targets.bar ? "on" : "off",
values: ["on", "off"],
description: "Color the usage bar by usage.",
},
{
id: "usageColorLabel",
label: "Usage label",
currentValue: targets.usageLabel ? "on" : "off",
values: ["on", "off"],
description: "Color the percentage text by usage.",
},
{
id: "usageColorStatus",
label: "Status",
currentValue: targets.status ? "on" : "off",
values: ["on", "off"],
description: "Color the status indicator by status.",
},
];
}
export function buildDisplayColorItems(settings: Settings): SettingItem[] {
return [
{
id: "baseTextColor",
label: "Base Color",
currentValue: normalizeBaseTextColor(settings.display.baseTextColor),
values: [...BASE_COLOR_OPTIONS] as BaseTextColor[],
description: "Base color for neutral labels and dividers.",
},
{
id: "backgroundColor",
label: "Background Color",
currentValue: normalizeBaseTextColor(settings.display.backgroundColor),
values: [...BASE_COLOR_OPTIONS] as BaseTextColor[],
description: "Background color for the widget line.",
},
{
id: "colorScheme",
label: "Color Indicator Scheme",
currentValue: settings.display.colorScheme,
values: [
"base-warning-error",
"success-base-warning-error",
"monochrome",
] as ColorScheme[],
description: "Choose how usage/status indicators are color-coded.",
},
{
id: "usageColorTargets",
label: "Color Indicator Targets",
currentValue: formatUsageColorTargetsSummary(settings.display.usageColorTargets),
description: "Pick which elements use the indicator colors.",
},
{
id: "errorThreshold",
label: "Error Threshold (%)",
currentValue: String(settings.display.errorThreshold),
values: ["10", "15", "20", "25", "30", "35", "40", CUSTOM_OPTION],
description: "Percent remaining below which usage is red.",
},
{
id: "warningThreshold",
label: "Warning Threshold (%)",
currentValue: String(settings.display.warningThreshold),
values: ["30", "40", "50", "60", "70", CUSTOM_OPTION],
description: "Percent remaining below which usage is yellow.",
},
{
id: "successThreshold",
label: "Success Threshold (%)",
currentValue: String(settings.display.successThreshold),
values: ["60", "70", "75", "80", "90", CUSTOM_OPTION],
description: "Percent remaining above which usage is green.",
},
];
}
export function buildDisplayBarItems(settings: Settings): SettingItem[] {
const items: SettingItem[] = [
{
id: "barType",
label: "Bar Type",
currentValue: settings.display.barType,
values: [
"horizontal-bar",
"horizontal-single",
"vertical",
"braille",
"shade",
] as BarType[],
description: "Choose the bar glyph style for usage.",
},
];
if (settings.display.barType === "horizontal-bar") {
items.push({
id: "barCharacter",
label: "H. Bar Character",
currentValue: settings.display.barCharacter,
values: ["light", "heavy", "double", "block", CUSTOM_OPTION],
description: "Custom bar character(s), set 1 or 2 (fill/empty)",
});
}
items.push(
{
id: "barWidth",
label: "Bar Width",
currentValue: String(settings.display.barWidth),
values: ["1", "4", "6", "8", "10", "12", "fill", CUSTOM_OPTION],
description: "Set the bar width or fill available space.",
},
{
id: "containBar",
label: "Contain Bar",
currentValue: settings.display.containBar ? "on" : "off",
values: ["on", "off"],
description: "Wrap the bar with ▕ and ▏ caps.",
},
);
if (settings.display.barType === "braille") {
items.push(
{
id: "brailleFillEmpty",
label: "Braille Empty Fill",
currentValue: settings.display.brailleFillEmpty ? "on" : "off",
values: ["on", "off"],
description: "Fill empty braille cells with dim blocks.",
},
{
id: "brailleFullBlocks",
label: "Braille Full Blocks",
currentValue: settings.display.brailleFullBlocks ? "on" : "off",
values: ["on", "off"],
description: "Use full 8-dot braille blocks for filled segments.",
},
);
}
items.push({
id: "barStyle",
label: "Bar Style",
currentValue: settings.display.barStyle,
values: ["bar", "percentage", "both"] as BarStyle[],
description: "Show bar, percentage, or both.",
});
return items;
}
export function buildDisplayProviderItems(settings: Settings): SettingItem[] {
return [
{
id: "showProviderName",
label: "Show Provider Name",
currentValue: settings.display.showProviderName ? "on" : "off",
values: ["on", "off"],
description: "Toggle the provider name prefix.",
},
{
id: "providerLabel",
label: "Provider Label",
currentValue: settings.display.providerLabel,
values: ["none", "plan", "subscription", "sub", CUSTOM_OPTION] as (ProviderLabel | typeof CUSTOM_OPTION)[],
description: "Suffix appended after the provider name.",
},
{
id: "providerLabelColon",
label: "Provider Label Colon",
currentValue: settings.display.providerLabelColon ? "on" : "off",
values: ["on", "off"],
description: "Show a colon after the provider label.",
},
{
id: "providerLabelBold",
label: "Show in Bold",
currentValue: settings.display.providerLabelBold ? "on" : "off",
values: ["on", "off"],
description: "Bold the provider name and colon.",
},
{
id: "showUsageLabels",
label: "Show Usage Labels",
currentValue: settings.display.showUsageLabels ? "on" : "off",
values: ["on", "off"],
description: "Show “used/rem.” labels after percentages.",
},
{
id: "showWindowTitle",
label: "Show Title",
currentValue: settings.display.showWindowTitle ? "on" : "off",
values: ["on", "off"],
description: "Show window titles like 5h, Week, etc.",
},
{
id: "boldWindowTitle",
label: "Bold Title",
currentValue: settings.display.boldWindowTitle ? "on" : "off",
values: ["on", "off"],
description: "Bold window titles like 5h, Week, etc.",
},
];
}
const STATUS_ICON_PACK_PREVIEW = {
minimal: "minimal (✓ ⚠ × ?)",
emoji: "emoji (✅ ⚠️ 🔴 ❓)",
faces: "faces (😎 😳 😵 🤔)",
} as const;
const STATUS_ICON_FACES_PRESET = "😎😳😵🤔";
const STATUS_ICON_CUSTOM_FALLBACK = ["✓", "⚠", "×", "?"];
const STATUS_ICON_CUSTOM_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
function resolveCustomStatusIcons(value?: string): [string, string, string, string] {
if (!value) return STATUS_ICON_CUSTOM_FALLBACK as [string, string, string, string];
const segments = Array.from(STATUS_ICON_CUSTOM_SEGMENTER.segment(value), (entry) => entry.segment)
.map((segment) => segment.trim())
.filter(Boolean);
if (segments.length < 3) return STATUS_ICON_CUSTOM_FALLBACK as [string, string, string, string];
if (segments.length === 3) {
return [segments[0], segments[1], segments[2], STATUS_ICON_CUSTOM_FALLBACK[3]] as [string, string, string, string];
}
return [segments[0], segments[1], segments[2], segments[3]] as [string, string, string, string];
}
function formatCustomStatusIcons(value?: string): string {
return resolveCustomStatusIcons(value).join(" ");
}
function formatStatusIconPack(pack: Exclude<StatusIconPack, "custom">): string {
return STATUS_ICON_PACK_PREVIEW[pack] ?? pack;
}
function parseStatusIconPack(value: string): StatusIconPack {
if (value.startsWith("minimal")) return "minimal";
if (value.startsWith("emoji")) return "emoji";
return "emoji";
}
export function buildDisplayStatusItems(settings: Settings): SettingItem[] {
const rawMode = settings.display.statusIndicatorMode ?? "icon";
const mode: StatusIndicatorMode = rawMode === "text" || rawMode === "icon+text" || rawMode === "icon"
? rawMode
: "icon";
const items: SettingItem[] = [
{
id: "statusIndicatorMode",
label: "Status Mode",
currentValue: mode,
values: ["icon", "text", "icon+text"] as StatusIndicatorMode[],
description: "Use icons, text, or both for status indicators.",
},
];
if (mode === "icon" || mode === "icon+text") {
const pack = settings.display.statusIconPack ?? "emoji";
const customIcons = settings.display.statusIconCustom;
items.push({
id: "statusIconPack",
label: "Status Icon Pack",
currentValue: pack === "custom" ? formatCustomStatusIcons(customIcons) : formatStatusIconPack(pack),
values: [
formatStatusIconPack("minimal"),
formatStatusIconPack("emoji"),
STATUS_ICON_PACK_PREVIEW.faces,
CUSTOM_OPTION,
],
description: "Pick the icon set used for status indicators. Choose custom to edit icons (OK/warn/error/unknown).",
});
}
items.push(
{
id: "statusDismissOk",
label: "Dismiss Operational Status",
currentValue: settings.display.statusDismissOk ? "on" : "off",
values: ["on", "off"],
description: "Hide status indicators when there are no incidents.",
}
);
return items;
}
export function buildDisplayDividerItems(settings: Settings): SettingItem[] {
return [
{
id: "dividerCharacter",
label: "Divider Character",
currentValue: settings.display.dividerCharacter,
values: ["none", "blank", "|", "│", "┃", "┆", "┇", "║", "•", "●", "○", "◇", CUSTOM_OPTION] as DividerCharacter[],
description: "Choose the divider glyph between windows.",
},
{
id: "dividerColor",
label: "Divider Color",
currentValue: normalizeDividerColor(settings.display.dividerColor ?? "borderMuted"),
values: [...DIVIDER_COLOR_OPTIONS] as DividerColor[],
description: "Color used for divider glyphs and lines.",
},
{
id: "statusProviderDivider",
label: "Status/Provider Divider",
currentValue: settings.display.statusProviderDivider ? "on" : "off",
values: ["on", "off"],
description: "Add a divider between status and provider label.",
},
{
id: "dividerBlanks",
label: "Blanks Before/After Divider",
currentValue: String(settings.display.dividerBlanks),
values: ["0", "1", "2", "3", "fill", CUSTOM_OPTION],
description: "Padding around the divider character.",
},
{
id: "showProviderDivider",
label: "Show Provider Divider",
currentValue: settings.display.showProviderDivider ? "on" : "off",
values: ["on", "off"],
description: "Show the divider after the provider label.",
},
{
id: "showTopDivider",
label: "Show Top Divider",
currentValue: settings.display.showTopDivider ? "on" : "off",
values: ["on", "off"],
description: "Show a divider line above the widget.",
},
{
id: "showBottomDivider",
label: "Show Bottom Divider",
currentValue: settings.display.showBottomDivider ? "on" : "off",
values: ["on", "off"],
description: "Show a divider line below the widget.",
},
{
id: "dividerFooterJoin",
label: "Connect Dividers",
currentValue: settings.display.dividerFooterJoin ? "on" : "off",
values: ["on", "off"],
description: "Draw reverse-T connectors for top/bottom dividers.",
},
];
}
function clampNumber(value: number, min: number, max: number): number {
return Math.min(max, Math.max(min, value));
}
function parseClampedNumber(value: string, min: number, max: number): number | null {
const parsed = Number.parseInt(value, 10);
if (Number.isNaN(parsed)) return null;
return clampNumber(parsed, min, max);
}
export function applyDisplayChange(settings: Settings, id: string, value: string): Settings {
switch (id) {
case "alignment":
settings.display.alignment = value as DisplayAlignment;
break;
case "barType":
settings.display.barType = value as BarType;
break;
case "barStyle":
settings.display.barStyle = value as BarStyle;
break;
case "barWidth": {
if (value === "fill") {
settings.display.barWidth = "fill" as BarWidth;
break;
}
const parsed = parseClampedNumber(value, 0, 100);
if (parsed !== null) {
settings.display.barWidth = parsed;
}
break;
}
case "containBar":
settings.display.containBar = value === "on";
break;
case "barCharacter":
settings.display.barCharacter = value as BarCharacter;
break;
case "brailleFillEmpty":
settings.display.brailleFillEmpty = value === "on";
break;
case "brailleFullBlocks":
settings.display.brailleFullBlocks = value === "on";
break;
case "colorScheme":
settings.display.colorScheme = value as ColorScheme;
break;
case "usageColorTitle":
settings.display.usageColorTargets = {
...resolveUsageColorTargets(settings.display.usageColorTargets),
title: value === "on",
};
break;
case "usageColorTimer":
settings.display.usageColorTargets = {
...resolveUsageColorTargets(settings.display.usageColorTargets),
timer: value === "on",
};
break;
case "usageColorBar":
settings.display.usageColorTargets = {
...resolveUsageColorTargets(settings.display.usageColorTargets),
bar: value === "on",
};
break;
case "usageColorLabel":
settings.display.usageColorTargets = {
...resolveUsageColorTargets(settings.display.usageColorTargets),
usageLabel: value === "on",
};
break;
case "usageColorStatus":
settings.display.usageColorTargets = {
...resolveUsageColorTargets(settings.display.usageColorTargets),
status: value === "on",
};
break;
case "usageColorTargets":
settings.display.usageColorTargets = resolveUsageColorTargets(settings.display.usageColorTargets);
break;
case "resetTimePosition":
settings.display.resetTimePosition = value as "off" | "front" | "back" | "integrated";
break;
case "resetTimeFormat":
settings.display.resetTimeFormat = value as ResetTimeFormat;
break;
case "resetTimeContainment":
if (value === CUSTOM_OPTION) {
break;
}
settings.display.resetTimeContainment = value as ResetTimerContainment;
break;
case "statusIndicatorMode":
settings.display.statusIndicatorMode = value as StatusIndicatorMode;
break;
case "statusIconPack":
if (value === CUSTOM_OPTION) {
settings.display.statusIconPack = "custom";
break;
}
if (value.startsWith("minimal") || value.startsWith("emoji")) {
settings.display.statusIconPack = parseStatusIconPack(value);
break;
}
if (value.startsWith("faces")) {
settings.display.statusIconCustom = STATUS_ICON_FACES_PRESET;
settings.display.statusIconPack = "custom";
break;
}
settings.display.statusIconCustom = value;
settings.display.statusIconPack = "custom";
break;
case "statusIconCustom":
settings.display.statusIconCustom = value;
settings.display.statusIconPack = "custom";
break;
case "statusProviderDivider":
settings.display.statusProviderDivider = value === "on";
break;
case "statusDismissOk":
settings.display.statusDismissOk = value === "on";
break;
case "showProviderName":
settings.display.showProviderName = value === "on";
break;
case "providerLabel":
settings.display.providerLabel = value as ProviderLabel;
break;
case "providerLabelColon":
settings.display.providerLabelColon = value === "on";
break;
case "providerLabelBold":
settings.display.providerLabelBold = value === "on";
break;
case "baseTextColor":
settings.display.baseTextColor = normalizeBaseTextColor(value);
break;
case "backgroundColor":
settings.display.backgroundColor = normalizeBaseTextColor(value);
break;
case "showUsageLabels":
settings.display.showUsageLabels = value === "on";
break;
case "showWindowTitle":
settings.display.showWindowTitle = value === "on";
break;
case "boldWindowTitle":
settings.display.boldWindowTitle = value === "on";
break;
case "showContextBar":
settings.display.showContextBar = value === "on";
break;
case "paddingLeft": {
const parsed = parseClampedNumber(value, 0, 100);
if (parsed !== null) {
settings.display.paddingLeft = parsed;
}
break;
}
case "paddingRight": {
const parsed = parseClampedNumber(value, 0, 100);
if (parsed !== null) {
settings.display.paddingRight = parsed;
}
break;
}
case "dividerCharacter":
settings.display.dividerCharacter = value as DividerCharacter;
break;
case "dividerColor":
settings.display.dividerColor = normalizeDividerColor(value);
break;
case "dividerBlanks": {
if (value === "fill") {
settings.display.dividerBlanks = "fill" as DividerBlanks;
break;
}
const parsed = parseClampedNumber(value, 0, 100);
if (parsed !== null) {
settings.display.dividerBlanks = parsed;
}
break;
}
case "showProviderDivider":
settings.display.showProviderDivider = value === "on";
break;
case "dividerFooterJoin":
settings.display.dividerFooterJoin = value === "on";
break;
case "showTopDivider":
settings.display.showTopDivider = value === "on";
break;
case "showBottomDivider":
settings.display.showBottomDivider = value === "on";
break;
case "overflow":
settings.display.overflow = value as WidgetWrapping;
break;
case "widgetWrapping":
settings.display.overflow = value as WidgetWrapping;
break;
case "errorThreshold": {
const parsed = parseClampedNumber(value, 0, 100);
if (parsed !== null) {
settings.display.errorThreshold = parsed;
}
break;
}
case "warningThreshold": {
const parsed = parseClampedNumber(value, 0, 100);
if (parsed !== null) {
settings.display.warningThreshold = parsed;
}
break;
}
case "successThreshold": {
const parsed = parseClampedNumber(value, 0, 100);
if (parsed !== null) {
settings.display.successThreshold = parsed;
}
break;
}
}
return settings;
}
@@ -0,0 +1,183 @@
/**
* Settings menu item builders.
*/
import type { SelectItem } from "@mariozechner/pi-tui";
import type { CoreProviderSettingsMap } from "../../shared.js";
import type { Settings } from "../settings-types.js";
import type { ProviderName } from "../types.js";
import { PROVIDERS, PROVIDER_DISPLAY_NAMES } from "../providers/metadata.js";
export type TooltipSelectItem = SelectItem & { tooltip?: string };
export function buildMainMenuItems(settings: Settings, pinnedProvider?: ProviderName | null): TooltipSelectItem[] {
const pinnedLabel = pinnedProvider ? PROVIDER_DISPLAY_NAMES[pinnedProvider] : "auto (current provider)";
const kb = settings.keybindings;
const kbDesc = `cycle: ${kb.cycleProvider}, reset: ${kb.toggleResetFormat}`;
return [
{
value: "display-theme",
label: "Themes",
description: "save, manage, share",
tooltip: "Save, load, and share display themes.",
},
{
value: "display",
label: "Adv. Display Settings",
description: "layout, bars, colors",
tooltip: "Adjust layout, colors, bar styling, status indicators, and dividers.",
},
{
value: "providers",
label: "Provider Settings",
description: "provider specific settings",
tooltip: "Configure provider display toggles and window visibility.",
},
{
value: "pin-provider",
label: "Provider Shown",
description: pinnedLabel,
tooltip: "Select which provider is shown in the widget.",
},
{
value: "keybindings",
label: "Keybindings",
description: kbDesc,
tooltip: "Configure keyboard shortcuts. Changes take effect after pi restart.",
},
{
value: "open-core-settings",
label: "Additional settings",
description: "in /sub-core:settings",
tooltip: "Open /sub-core:settings for refresh behavior and provider enablement.",
},
];
}
export function buildProviderListItems(settings: Settings, coreProviders?: CoreProviderSettingsMap): TooltipSelectItem[] {
const orderedProviders = settings.providerOrder.length > 0 ? settings.providerOrder : PROVIDERS;
const items: TooltipSelectItem[] = orderedProviders.map((provider) => {
const ps = settings.providers[provider];
const core = coreProviders?.[provider];
const enabledValue = core
? core.enabled === "auto"
? "auto"
: core.enabled === true || core.enabled === "on"
? "on"
: "off"
: "auto";
const status = ps.showStatus ? "status on" : "status off";
return {
value: `provider-${provider}`,
label: PROVIDER_DISPLAY_NAMES[provider],
description: `enabled ${enabledValue}, ${status}`,
tooltip: `Configure ${PROVIDER_DISPLAY_NAMES[provider]} display settings.`,
};
});
items.push({
value: "reset-providers",
label: "Reset Provider Defaults",
description: "restore provider settings",
tooltip: "Restore provider display settings to their defaults.",
});
return items;
}
export function buildDisplayMenuItems(): TooltipSelectItem[] {
return [
{
value: "display-layout",
label: "Layout & Structure",
description: "alignment, wrapping, padding",
tooltip: "Control alignment, wrapping, and padding.",
},
{
value: "display-bar",
label: "Bars",
description: "style, width, character",
tooltip: "Customize bar type, width, and bar styling.",
},
{
value: "display-provider",
label: "Labels & Text",
description: "labels, titles, usage text",
tooltip: "Adjust provider label visibility and text styling.",
},
{
value: "display-reset",
label: "Reset Timer",
description: "position, format, wrapping",
tooltip: "Control reset timer placement and formatting.",
},
{
value: "display-status",
label: "Status",
description: "mode, icons, text",
tooltip: "Configure status mode and icon packs.",
},
{
value: "display-divider",
label: "Dividers",
description: "character, blanks, status divider, lines",
tooltip: "Change divider character, spacing, status separator, and divider lines.",
},
{
value: "display-color",
label: "Colors",
description: "base, scheme, thresholds",
tooltip: "Tune base colors, color scheme, and thresholds.",
},
];
}
export function buildDisplayThemeMenuItems(): TooltipSelectItem[] {
return [
{
value: "display-theme-save",
label: "Save Theme",
description: "store current theme",
tooltip: "Save the current display theme with a custom name.",
},
{
value: "display-theme-load",
label: "Load & Manage themes",
description: "load, share, rename and delete themes",
tooltip: "Load, share, delete, rename, and restore saved themes.",
},
{
value: "display-theme-share",
label: "Share Theme",
description: "share current theme",
tooltip: "Post a share string for the current theme.",
},
{
value: "display-theme-import",
label: "Import theme",
description: "from share string",
tooltip: "Import a shared theme string.",
},
{
value: "display-theme-random",
label: "Random theme",
description: "generate a new theme",
tooltip: "Generate a random display theme as inspiration or a starting point.",
},
{
value: "display-theme-restore",
label: "Restore previous state",
description: "restore your last theme",
tooltip: "Restore your previous display theme.",
},
];
}
export function buildProviderSettingsItems(settings: Settings): TooltipSelectItem[] {
return buildProviderListItems(settings);
}
export function getProviderFromCategory(category: string): ProviderName | null {
const match = category.match(/^provider-(\w+)$/);
return match ? (match[1] as ProviderName) : null;
}
@@ -0,0 +1,349 @@
import type { Settings } from "../settings-types.js";
import type { TooltipSelectItem } from "./menu.js";
type DisplaySettings = Settings["display"];
type BarType = DisplaySettings["barType"];
type BarStyle = DisplaySettings["barStyle"];
type BarCharacter = DisplaySettings["barCharacter"];
type BarWidth = DisplaySettings["barWidth"];
type DividerCharacter = DisplaySettings["dividerCharacter"];
type DividerBlanks = DisplaySettings["dividerBlanks"];
type DisplayAlignment = DisplaySettings["alignment"];
type OverflowMode = DisplaySettings["overflow"];
type BaseTextColor = DisplaySettings["baseTextColor"];
type DividerColor = DisplaySettings["dividerColor"];
type ResetTimeFormat = DisplaySettings["resetTimeFormat"];
type ResetTimerContainment = DisplaySettings["resetTimeContainment"];
type StatusIndicatorMode = DisplaySettings["statusIndicatorMode"];
type StatusIconPack = DisplaySettings["statusIconPack"];
type ProviderLabel = DisplaySettings["providerLabel"];
const RANDOM_BAR_TYPES: BarType[] = ["horizontal-bar", "horizontal-single", "vertical", "braille", "shade"];
const RANDOM_BAR_STYLES: BarStyle[] = ["bar", "percentage", "both"];
const RANDOM_BAR_WIDTHS: BarWidth[] = [1, 4, 6, 8, 10, 12, "fill"];
const RANDOM_BAR_CHARACTERS: BarCharacter[] = [
"light",
"heavy",
"double",
"block",
"▮▯",
"■□",
"●○",
"▲△",
"◆◇",
"🚀_",
];
const RANDOM_ALIGNMENTS: DisplayAlignment[] = ["left", "center", "right", "split"];
const RANDOM_OVERFLOW: OverflowMode[] = ["truncate", "wrap"];
const RANDOM_RESET_POSITIONS: DisplaySettings["resetTimePosition"][] = ["off", "front", "back", "integrated"];
const RANDOM_RESET_FORMATS: ResetTimeFormat[] = ["relative", "datetime"];
const RANDOM_RESET_CONTAINMENTS: ResetTimerContainment[] = ["none", "blank", "()", "[]", "<>"];
const RANDOM_STATUS_MODES: StatusIndicatorMode[] = ["icon", "text", "icon+text"];
const RANDOM_STATUS_PACKS: StatusIconPack[] = ["minimal", "emoji"];
const RANDOM_PROVIDER_LABELS: ProviderLabel[] = ["plan", "subscription", "sub", "none"];
const RANDOM_DIVIDER_CHARACTERS: DividerCharacter[] = ["none", "blank", "|", "│", "┃", "┆", "┇", "║", "•", "●", "○", "◇"];
const RANDOM_DIVIDER_BLANKS: DividerBlanks[] = [0, 1, 2, 3];
const RANDOM_COLOR_SCHEMES: DisplaySettings["colorScheme"][] = [
"base-warning-error",
"success-base-warning-error",
"monochrome",
];
const RANDOM_BASE_TEXT_COLORS: BaseTextColor[] = ["dim", "muted", "text", "primary", "success", "warning", "error", "border", "borderMuted"];
const RANDOM_BACKGROUND_COLORS: BaseTextColor[] = [
"text",
"selectedBg",
"userMessageBg",
"customMessageBg",
"toolPendingBg",
"toolSuccessBg",
"toolErrorBg",
];
const RANDOM_DIVIDER_COLORS: DividerColor[] = [
"primary",
"text",
"muted",
"dim",
"success",
"warning",
"error",
"border",
"borderMuted",
"borderAccent",
];
const RANDOM_PADDING: number[] = [0, 1, 2, 3, 4];
function pickRandom<T>(items: readonly T[]): T {
return items[Math.floor(Math.random() * items.length)] ?? items[0]!;
}
function randomBool(probability = 0.5): boolean {
return Math.random() < probability;
}
const THEME_ID_LENGTH = 24;
const THEME_ID_FALLBACK = "theme";
function buildThemeId(name: string): string {
return name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").slice(0, THEME_ID_LENGTH) || THEME_ID_FALLBACK;
}
export interface DisplayThemeTarget {
id?: string;
name: string;
display: Settings["display"];
deletable: boolean;
}
export function buildDisplayThemeItems(
settings: Settings,
): TooltipSelectItem[] {
const items: TooltipSelectItem[] = [];
items.push({
value: "user",
label: "Restore backup",
description: "restore your last theme",
tooltip: "Restore your previous display theme.",
});
items.push({
value: "default",
label: "Default",
description: "restore default settings",
tooltip: "Reset display settings to defaults.",
});
items.push({
value: "minimal",
label: "Default Minimal",
description: "compact display",
tooltip: "Apply the default minimal theme.",
});
for (const theme of settings.displayThemes) {
const description = theme.source === "imported" ? "manually imported theme" : "manually saved theme";
items.push({
value: `theme:${theme.id}`,
label: theme.name,
description,
tooltip: `Manage ${theme.name}.`,
});
}
return items;
}
export function resolveDisplayThemeTarget(
value: string,
settings: Settings,
defaults: Settings,
fallbackUser: Settings["display"] | null,
): DisplayThemeTarget | null {
if (value === "user") {
const display = settings.displayUserTheme ?? fallbackUser ?? settings.display;
return { name: "Restore backup", display, deletable: false };
}
if (value === "default") {
return { name: "Default", display: { ...defaults.display }, deletable: false };
}
if (value === "minimal") {
return {
name: "Default Minimal",
display: {
...defaults.display,
alignment: "split",
barStyle: "percentage",
barType: "horizontal-bar",
barWidth: 1,
barCharacter: "heavy",
containBar: true,
brailleFillEmpty: false,
brailleFullBlocks: false,
colorScheme: "base-warning-error",
usageColorTargets: {
title: true,
timer: true,
bar: true,
usageLabel: true,
status: true,
},
resetTimePosition: "off",
resetTimeFormat: "relative",
resetTimeContainment: "blank",
statusIndicatorMode: "icon",
statusIconPack: "minimal",
statusProviderDivider: false,
statusDismissOk: true,
showProviderName: false,
providerLabel: "none",
providerLabelColon: false,
providerLabelBold: true,
baseTextColor: "muted",
backgroundColor: "text",
showWindowTitle: false,
boldWindowTitle: true,
showUsageLabels: false,
dividerCharacter: "none",
dividerColor: "dim",
dividerBlanks: 1,
showProviderDivider: true,
dividerFooterJoin: true,
showTopDivider: false,
showBottomDivider: false,
paddingLeft: 1,
paddingRight: 1,
widgetPlacement: "belowEditor",
errorThreshold: 25,
warningThreshold: 50,
overflow: "truncate",
successThreshold: 75,
},
deletable: false,
};
}
if (value.startsWith("theme:")) {
const id = value.replace("theme:", "");
const theme = settings.displayThemes.find((entry) => entry.id === id);
if (!theme) return null;
return { id: theme.id, name: theme.name, display: theme.display, deletable: true };
}
return null;
}
export function buildRandomDisplay(base: DisplaySettings): DisplaySettings {
const display: DisplaySettings = { ...base };
display.alignment = pickRandom(RANDOM_ALIGNMENTS);
display.overflow = pickRandom(RANDOM_OVERFLOW);
const padding = pickRandom(RANDOM_PADDING);
display.paddingLeft = padding;
display.paddingRight = padding;
display.barStyle = pickRandom(RANDOM_BAR_STYLES);
display.barType = pickRandom(RANDOM_BAR_TYPES);
display.barWidth = pickRandom(RANDOM_BAR_WIDTHS);
display.barCharacter = pickRandom(RANDOM_BAR_CHARACTERS);
display.containBar = randomBool();
display.brailleFillEmpty = randomBool();
display.brailleFullBlocks = randomBool();
display.colorScheme = pickRandom(RANDOM_COLOR_SCHEMES);
const usageColorTargets = {
title: randomBool(),
timer: randomBool(),
bar: randomBool(),
usageLabel: randomBool(),
status: randomBool(),
};
if (!usageColorTargets.title && !usageColorTargets.timer && !usageColorTargets.bar && !usageColorTargets.usageLabel && !usageColorTargets.status) {
usageColorTargets.bar = true;
}
display.usageColorTargets = usageColorTargets;
display.resetTimePosition = pickRandom(RANDOM_RESET_POSITIONS);
display.resetTimeFormat = pickRandom(RANDOM_RESET_FORMATS);
display.resetTimeContainment = pickRandom(RANDOM_RESET_CONTAINMENTS);
display.statusIndicatorMode = pickRandom(RANDOM_STATUS_MODES);
display.statusIconPack = pickRandom(RANDOM_STATUS_PACKS);
display.statusProviderDivider = randomBool();
display.statusDismissOk = randomBool();
display.showProviderName = randomBool();
display.providerLabel = pickRandom(RANDOM_PROVIDER_LABELS);
display.providerLabelColon = display.providerLabel !== "none" && randomBool();
display.providerLabelBold = randomBool();
display.baseTextColor = pickRandom(RANDOM_BASE_TEXT_COLORS);
display.backgroundColor = pickRandom(RANDOM_BACKGROUND_COLORS);
display.boldWindowTitle = randomBool();
display.showUsageLabels = randomBool();
display.dividerCharacter = pickRandom(RANDOM_DIVIDER_CHARACTERS);
display.dividerColor = pickRandom(RANDOM_DIVIDER_COLORS);
display.dividerBlanks = pickRandom(RANDOM_DIVIDER_BLANKS);
display.showProviderDivider = randomBool();
display.dividerFooterJoin = randomBool();
display.showTopDivider = randomBool();
display.showBottomDivider = randomBool();
if (display.dividerCharacter === "none") {
display.showProviderDivider = false;
display.dividerFooterJoin = false;
display.showTopDivider = false;
display.showBottomDivider = false;
}
if (display.providerLabel === "none") {
display.providerLabelColon = false;
}
return display;
}
export function buildThemeActionItems(target: DisplayThemeTarget): TooltipSelectItem[] {
const items: TooltipSelectItem[] = [
{
value: "load",
label: "Load",
description: "apply this theme",
tooltip: "Apply the selected theme.",
},
{
value: "share",
label: "Share",
description: "post share string",
tooltip: "Post a shareable theme string to chat.",
},
];
if (target.deletable) {
items.push({
value: "rename",
label: "Rename",
description: "rename saved theme",
tooltip: "Rename this saved theme.",
});
items.push({
value: "delete",
label: "Delete",
description: "remove saved theme",
tooltip: "Remove this theme from saved themes.",
});
}
return items;
}
export function upsertDisplayTheme(
settings: Settings,
name: string,
display: Settings["display"],
source?: "saved" | "imported",
): Settings {
const trimmed = name.trim() || "Theme";
const id = buildThemeId(trimmed);
const snapshot = { ...display };
const existing = settings.displayThemes.find((theme) => theme.id === id);
const resolvedSource = source ?? existing?.source ?? "saved";
if (existing) {
existing.name = trimmed;
existing.display = snapshot;
existing.source = resolvedSource;
} else {
settings.displayThemes.push({ id, name: trimmed, display: snapshot, source: resolvedSource });
}
return settings;
}
export function renameDisplayTheme(settings: Settings, id: string, name: string): Settings {
const trimmed = name.trim() || "Theme";
const nextId = buildThemeId(trimmed);
const existing = settings.displayThemes.find((theme) => theme.id === id);
if (!existing) return settings;
if (nextId === id) {
existing.name = trimmed;
return settings;
}
const collision = settings.displayThemes.find((theme) => theme.id === nextId);
if (collision) {
collision.name = trimmed;
collision.display = existing.display;
collision.source = existing.source;
settings.displayThemes = settings.displayThemes.filter((theme) => theme.id !== id);
return settings;
}
existing.id = nextId;
existing.name = trimmed;
return settings;
}
export function saveDisplayTheme(settings: Settings, name: string): Settings {
return upsertDisplayTheme(settings, name, settings.display, "saved");
}
File diff suppressed because it is too large Load Diff