add sub-bar usage widget extension with opencode-go support
This commit is contained in:
@@ -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
Reference in New Issue
Block a user