pi-cc-loop 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -5
- package/config.ts +31 -13
- package/index.ts +59 -9
- package/package.json +1 -1
- package/settings-panel.ts +135 -0
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ ln -sfn "$PWD" ~/.pi/agent/extensions/pi-cc-loop
|
|
|
38
38
|
| 次数上限 | `maxFires`,到顶自动删除 | `--max N` |
|
|
39
39
|
| 持久化 | `durable: true` 写入 `.pi/scheduled_tasks.json`,重启后继续;同一项目只由一个 Pi 会话执行 | `--durable` |
|
|
40
40
|
| 底部摘要 | Pi 底部状态栏一行:`loop 2 · next 3m`,无任务时不占位 | `/loop footer on\|off` |
|
|
41
|
-
| 界面设置 |
|
|
41
|
+
| 界面设置 | 和 Pi 的 `/settings` 一样的面板,本地命令,不进对话 | `/loop settings` |
|
|
42
42
|
|
|
43
43
|
## 命令
|
|
44
44
|
|
|
@@ -49,7 +49,7 @@ ln -sfn "$PWD" ~/.pi/agent/extensions/pi-cc-loop
|
|
|
49
49
|
/loop status 列出当前任务
|
|
50
50
|
/loop stop [id] 停止一个或全部
|
|
51
51
|
/loop footer on|off 本地:底部摘要
|
|
52
|
-
/loop settings
|
|
52
|
+
/loop settings 本地:设置面板
|
|
53
53
|
/loop help 本地:帮助
|
|
54
54
|
```
|
|
55
55
|
|
|
@@ -86,7 +86,7 @@ Ctrl+O 展开后多一行元信息(任务 id、节奏)。`compactMessages`
|
|
|
86
86
|
|
|
87
87
|
## 设置
|
|
88
88
|
|
|
89
|
-
两个 pi-cc 扩展共用 `~/.pi/agent/pi-cc.json
|
|
89
|
+
两个 pi-cc 扩展共用 Pi 配置目录下的 `pi-cc.json`(默认 `~/.pi/agent/pi-cc.json`,跟随 `PI_CODING_AGENT_DIR`),文件不存在时用默认值:
|
|
90
90
|
|
|
91
91
|
```jsonc
|
|
92
92
|
{
|
|
@@ -95,7 +95,28 @@ Ctrl+O 展开后多一行元信息(任务 id、节奏)。`compactMessages`
|
|
|
95
95
|
}
|
|
96
96
|
```
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
在 TUI 里输入 `/loop settings` 会打开设置面板,样子和操作都和 Pi 的 `/settings` 一样:上下键选择,Enter/空格切换,Esc 关闭;
|
|
99
|
+
改动立即保存,两个扩展同时生效。Pi 的 `/settings` 不接受扩展加项,所以面板放在 `/loop settings` 下。
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
─────────────────────────────────────────────────────────────
|
|
103
|
+
pi-cc settings · ~/.pi/agent/pi-cc.json
|
|
104
|
+
|
|
105
|
+
→ Message style compact
|
|
106
|
+
Footer summary on
|
|
107
|
+
|
|
108
|
+
How scheduled fires and Monitor events are drawn. compact: ✻ header
|
|
109
|
+
and indented body; card: the old padded card.
|
|
110
|
+
|
|
111
|
+
Enter/Space to change · Esc to cancel
|
|
112
|
+
─────────────────────────────────────────────────────────────
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
也可以用文字命令:`/loop settings messages on|off`、`/loop settings footer on|off`(或 `/loop footer on|off`),适合脚本和 RPC;
|
|
116
|
+
非 TUI 模式下,不带参数的 `/loop settings` 输出当前设置。命令只写改动的那一项,文件里的其他内容原样保留。
|
|
117
|
+
|
|
118
|
+
环境变量 `PI_CC_FOOTER`、`PI_CC_COMPACT_MESSAGES` 优先于文件,但不会被写进文件。设着的时候,面板里这一项显示实际生效的值、不能切换,
|
|
119
|
+
说明里写出是哪个变量;文字输出会在对应项后面标出来(如 `hidden (PI_CC_FOOTER=0)`);用文字命令切换会写进文件,但提示会说明变量还在覆盖它。
|
|
99
120
|
|
|
100
121
|
## 底部摘要
|
|
101
122
|
|
|
@@ -124,7 +145,7 @@ Ctrl+O 展开后多一行元信息(任务 id、节奏)。`compactMessages`
|
|
|
124
145
|
| `PI_LOOP_DURABLE_SYNC_MS` | `5000` | 持久化任务文件的同步间隔 |
|
|
125
146
|
| `PI_CC_FOOTER` | — | `0` / `1`,覆盖设置文件里的 `footer` |
|
|
126
147
|
| `PI_CC_COMPACT_MESSAGES` | — | `0` / `1`,覆盖设置文件里的 `compactMessages` |
|
|
127
|
-
| `PI_CC_CONFIG_FILE` | `~/.pi/agent/pi-cc.json` |
|
|
148
|
+
| `PI_CC_CONFIG_FILE` | `~/.pi/agent/pi-cc.json` | 设置文件路径;不设时在 Pi 的配置目录里,跟随 `PI_CODING_AGENT_DIR` |
|
|
128
149
|
|
|
129
150
|
## 兼容性
|
|
130
151
|
|
package/config.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Both pi-cc-loop and pi-cc-monitor read the same small JSON file so a single
|
|
5
5
|
* toggle changes the look of both packages. The file is optional; when it is
|
|
6
|
-
* missing or malformed the defaults apply.
|
|
6
|
+
* missing or malformed the defaults apply. It sits in Pi's agent directory
|
|
7
|
+
* (PI_CODING_AGENT_DIR, default ~/.pi/agent), next to Pi's own settings.
|
|
7
8
|
*
|
|
8
9
|
* ~/.pi/agent/pi-cc.json
|
|
9
10
|
* {
|
|
@@ -11,14 +12,15 @@
|
|
|
11
12
|
* "footer": true
|
|
12
13
|
* }
|
|
13
14
|
*
|
|
14
|
-
* Environment overrides (useful for tests and quick experiments)
|
|
15
|
+
* Environment overrides (useful for tests and quick experiments) win over the
|
|
16
|
+
* file while they are set, and are never written to it:
|
|
15
17
|
* PI_CC_CONFIG_FILE=/path/to/pi-cc.json
|
|
16
18
|
* PI_CC_COMPACT_MESSAGES=0|1
|
|
17
19
|
* PI_CC_FOOTER=0|1
|
|
18
20
|
*/
|
|
19
21
|
|
|
22
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
20
23
|
import fs from "node:fs";
|
|
21
|
-
import os from "node:os";
|
|
22
24
|
import path from "node:path";
|
|
23
25
|
|
|
24
26
|
export interface PiCcConfig {
|
|
@@ -36,6 +38,12 @@ export const DEFAULT_CONFIG: PiCcConfig = {
|
|
|
36
38
|
footer: true,
|
|
37
39
|
};
|
|
38
40
|
|
|
41
|
+
/** The environment variable that overrides each setting. */
|
|
42
|
+
const ENV_OVERRIDES: Record<keyof PiCcConfig, string> = {
|
|
43
|
+
compactMessages: "PI_CC_COMPACT_MESSAGES",
|
|
44
|
+
footer: "PI_CC_FOOTER",
|
|
45
|
+
};
|
|
46
|
+
|
|
39
47
|
function envBool(name: string): boolean | undefined {
|
|
40
48
|
const raw = process.env[name];
|
|
41
49
|
if (raw === undefined) return undefined;
|
|
@@ -44,11 +52,16 @@ function envBool(name: string): boolean | undefined {
|
|
|
44
52
|
return undefined;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
/** The
|
|
55
|
+
/** The variable overriding a setting right now, as `PI_CC_FOOTER=0`; undefined when none is. */
|
|
56
|
+
export function envOverride(key: keyof PiCcConfig): string | undefined {
|
|
57
|
+
const name = ENV_OVERRIDES[key];
|
|
58
|
+
return envBool(name) === undefined ? undefined : `${name}=${process.env[name]}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The settings file shared by pi-cc-loop and pi-cc-monitor, in Pi's agent directory. */
|
|
48
62
|
export function configPath(): string {
|
|
49
63
|
if (process.env.PI_CC_CONFIG_FILE) return process.env.PI_CC_CONFIG_FILE;
|
|
50
|
-
|
|
51
|
-
return path.join(agentDir, "pi-cc.json");
|
|
64
|
+
return path.join(getAgentDir(), "pi-cc.json");
|
|
52
65
|
}
|
|
53
66
|
|
|
54
67
|
function readConfig(): PiCcConfig {
|
|
@@ -61,10 +74,10 @@ function readConfig(): PiCcConfig {
|
|
|
61
74
|
}
|
|
62
75
|
return {
|
|
63
76
|
compactMessages:
|
|
64
|
-
envBool(
|
|
77
|
+
envBool(ENV_OVERRIDES.compactMessages) ??
|
|
65
78
|
(typeof file.compactMessages === "boolean" ? file.compactMessages : DEFAULT_CONFIG.compactMessages),
|
|
66
79
|
footer:
|
|
67
|
-
envBool(
|
|
80
|
+
envBool(ENV_OVERRIDES.footer) ?? (typeof file.footer === "boolean" ? file.footer : DEFAULT_CONFIG.footer),
|
|
68
81
|
};
|
|
69
82
|
}
|
|
70
83
|
|
|
@@ -81,9 +94,14 @@ export function invalidateConfig(): void {
|
|
|
81
94
|
cached = undefined;
|
|
82
95
|
}
|
|
83
96
|
|
|
84
|
-
/**
|
|
97
|
+
/**
|
|
98
|
+
* Write the changed keys to the settings file and return the settings now in
|
|
99
|
+
* effect. Only the patch is written: every other key stays as the user left
|
|
100
|
+
* it, and an environment override is never copied into the file. It still
|
|
101
|
+
* wins over the file while it is set, so the returned value can differ from
|
|
102
|
+
* the patch (see `envOverride`).
|
|
103
|
+
*/
|
|
85
104
|
export function saveConfig(patch: Partial<PiCcConfig>): PiCcConfig {
|
|
86
|
-
const next = { ...loadConfig(), ...patch };
|
|
87
105
|
const file = configPath();
|
|
88
106
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
89
107
|
// Keep keys this version does not know about: the file is the user's.
|
|
@@ -95,8 +113,8 @@ export function saveConfig(patch: Partial<PiCcConfig>): PiCcConfig {
|
|
|
95
113
|
// A missing or malformed file is rewritten from scratch.
|
|
96
114
|
}
|
|
97
115
|
const temp = `${file}.tmp`;
|
|
98
|
-
fs.writeFileSync(temp, `${JSON.stringify({ ...raw, ...
|
|
116
|
+
fs.writeFileSync(temp, `${JSON.stringify({ ...raw, ...patch }, null, 2)}\n`, "utf8");
|
|
99
117
|
fs.renameSync(temp, file);
|
|
100
|
-
cached =
|
|
101
|
-
return
|
|
118
|
+
cached = undefined;
|
|
119
|
+
return loadConfig();
|
|
102
120
|
}
|
package/index.ts
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
31
31
|
import { Type } from "typebox";
|
|
32
|
-
import { configPath, invalidateConfig, loadConfig, saveConfig, type PiCcConfig } from "./config.ts";
|
|
32
|
+
import { configPath, envOverride, invalidateConfig, loadConfig, saveConfig, type PiCcConfig } from "./config.ts";
|
|
33
33
|
import {
|
|
34
34
|
humanSchedule,
|
|
35
35
|
isUnreachable,
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
loopCommandGuidelines,
|
|
58
58
|
} from "./prompts.ts";
|
|
59
59
|
import { formatClock, formatEta, renderScheduledMessage } from "./renderers.ts";
|
|
60
|
+
import { settingsPanel } from "./settings-panel.ts";
|
|
60
61
|
import {
|
|
61
62
|
DEFAULT_TIMING,
|
|
62
63
|
generateId,
|
|
@@ -574,7 +575,8 @@ export default function piCcLoopExtension(pi: ExtensionAPI): void {
|
|
|
574
575
|
case "statusline": {
|
|
575
576
|
const arg = (rest[0] ?? "").toLowerCase();
|
|
576
577
|
if (!arg) {
|
|
577
|
-
|
|
578
|
+
const current = `${loadConfig().footer ? "on" : "off"}${overrideNote("footer")}`;
|
|
579
|
+
notify(ctx, `Usage: /loop footer on|off (currently ${current})`, "info");
|
|
578
580
|
return;
|
|
579
581
|
}
|
|
580
582
|
setSetting(ctx, "footer", arg);
|
|
@@ -584,7 +586,7 @@ export default function piCcLoopExtension(pi: ExtensionAPI): void {
|
|
|
584
586
|
case "config": {
|
|
585
587
|
const [key, value] = rest;
|
|
586
588
|
if (!key) {
|
|
587
|
-
showSettings(ctx);
|
|
589
|
+
if (!(await openSettingsPanel(ctx))) showSettings(ctx);
|
|
588
590
|
return;
|
|
589
591
|
}
|
|
590
592
|
const name = key.toLowerCase();
|
|
@@ -651,7 +653,7 @@ export default function piCcLoopExtension(pi: ExtensionAPI): void {
|
|
|
651
653
|
"/loop <prompt> self-paced: the agent picks the next delay",
|
|
652
654
|
"/loop [interval] the .pi/loop.md tasks, or the built-in maintenance prompt",
|
|
653
655
|
"/loop footer on|off show or hide the one-line summary in the footer",
|
|
654
|
-
"/loop settings
|
|
656
|
+
"/loop settings settings panel (a summary outside the TUI)",
|
|
655
657
|
"/loop settings messages on|off compact two-line markers",
|
|
656
658
|
"/loop settings footer on|off the one-line footer summary",
|
|
657
659
|
"",
|
|
@@ -662,12 +664,46 @@ export default function piCcLoopExtension(pi: ExtensionAPI): void {
|
|
|
662
664
|
notify(ctx, help, "info");
|
|
663
665
|
}
|
|
664
666
|
|
|
667
|
+
/** ` (PI_CC_FOOTER=0)` when an environment variable decides the setting. */
|
|
668
|
+
function overrideNote(key: keyof PiCcConfig): string {
|
|
669
|
+
const override = envOverride(key);
|
|
670
|
+
return override ? ` (${override})` : "";
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* A bare /loop settings in the TUI: a panel like Pi's /settings, which
|
|
675
|
+
* extensions cannot add to. Resolves when the panel closes; false when there
|
|
676
|
+
* is no TUI to show it in, so the caller prints the summary instead.
|
|
677
|
+
*/
|
|
678
|
+
async function openSettingsPanel(ctx: ExtensionCommandContext): Promise<boolean> {
|
|
679
|
+
if (ctx.mode !== "tui" || typeof ctx.ui.custom !== "function") return false;
|
|
680
|
+
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
681
|
+
const panel = settingsPanel({
|
|
682
|
+
theme,
|
|
683
|
+
file: configPath(),
|
|
684
|
+
onChange: (field, value) => {
|
|
685
|
+
applySetting(field, value);
|
|
686
|
+
},
|
|
687
|
+
onClose: () => done(undefined),
|
|
688
|
+
});
|
|
689
|
+
return {
|
|
690
|
+
render: (width: number) => panel.render(width),
|
|
691
|
+
invalidate: () => panel.invalidate(),
|
|
692
|
+
handleInput: (data: string) => {
|
|
693
|
+
panel.handleInput(data);
|
|
694
|
+
tui.requestRender();
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
});
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
|
|
665
701
|
function showSettings(ctx: ExtensionContext): void {
|
|
666
702
|
const config = loadConfig();
|
|
667
703
|
const lines = [
|
|
668
704
|
`pi-cc settings \u00b7 ${configPath()}`,
|
|
669
|
-
` messages: ${config.compactMessages ? "compact" : "card"}`,
|
|
670
|
-
` footer: ${config.footer ? "summary" : "hidden"}`,
|
|
705
|
+
` messages: ${config.compactMessages ? "compact" : "card"}${overrideNote("compactMessages")}`,
|
|
706
|
+
` footer: ${config.footer ? "summary" : "hidden"}${overrideNote("footer")}`,
|
|
671
707
|
"",
|
|
672
708
|
"Usage: /loop settings messages on|off",
|
|
673
709
|
" /loop settings footer on|off",
|
|
@@ -683,15 +719,29 @@ export default function piCcLoopExtension(pi: ExtensionAPI): void {
|
|
|
683
719
|
showSettings(ctx);
|
|
684
720
|
return;
|
|
685
721
|
}
|
|
686
|
-
const
|
|
722
|
+
const field: keyof PiCcConfig = key === "messages" ? "compactMessages" : "footer";
|
|
723
|
+
const next = applySetting(field, on);
|
|
724
|
+
const label = key === "messages" ? "Compact message style" : "Footer summary";
|
|
725
|
+
const override = envOverride(field);
|
|
726
|
+
if (override && next[field] !== on) {
|
|
727
|
+
// The file has the choice, but the variable decides while it is set.
|
|
728
|
+
notify(ctx, `${label} saved as ${on ? "on" : "off"}, but ${override} overrides it while set`, "warning");
|
|
729
|
+
} else {
|
|
730
|
+
notify(ctx, `${label} ${on ? "enabled" : "disabled"}`, "info");
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** Save one setting, tell the other pi-cc packages, and repaint the footer. Returns the settings in effect. */
|
|
735
|
+
function applySetting(field: keyof PiCcConfig, on: boolean): PiCcConfig {
|
|
736
|
+
const patch: Partial<PiCcConfig> = field === "compactMessages" ? { compactMessages: on } : { footer: on };
|
|
687
737
|
const next = saveConfig(patch);
|
|
688
738
|
try {
|
|
689
739
|
pi.events.emit(CONFIG_CHANNEL, next);
|
|
690
740
|
} catch {
|
|
691
741
|
// another extension must not break the command
|
|
692
742
|
}
|
|
693
|
-
|
|
694
|
-
|
|
743
|
+
if (field === "footer") refreshStatus();
|
|
744
|
+
return next;
|
|
695
745
|
}
|
|
696
746
|
|
|
697
747
|
// ------------------------------------------------------------------ tools
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cc-loop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Claude Code compatible /loop for the Pi coding agent: cron-scheduled fixed intervals, self-paced chains, and the same dispatch semantics as Claude Code.",
|
|
5
5
|
"author": "tarnish233",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The /loop settings panel: the same kind of screen as Pi's /settings.
|
|
3
|
+
*
|
|
4
|
+
* Pi's /settings cannot take items from an extension (its panel is a fixed
|
|
5
|
+
* list, and the command is handled before extension commands), so the shared
|
|
6
|
+
* pi-cc settings get their own panel, built from the same parts: SettingsList
|
|
7
|
+
* between two borders, the selected setting's description below it, and
|
|
8
|
+
* Enter/Space to change a value. A change is saved at once and Esc closes.
|
|
9
|
+
*
|
|
10
|
+
* A setting that an environment variable overrides shows the value in effect
|
|
11
|
+
* and cannot be changed here; its description names the variable.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { Container, SettingsList, Spacer, Text, type SettingItem, type SettingsListTheme } from "@earendil-works/pi-tui";
|
|
16
|
+
import os from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { envOverride, loadConfig, type PiCcConfig } from "./config.ts";
|
|
19
|
+
|
|
20
|
+
/** The part of Pi's theme the panel uses. */
|
|
21
|
+
export interface PanelTheme {
|
|
22
|
+
fg(color: string, text: string): string;
|
|
23
|
+
bold(text: string): string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SettingSpec {
|
|
27
|
+
field: keyof PiCcConfig;
|
|
28
|
+
label: string;
|
|
29
|
+
/** Display values for true and false. */
|
|
30
|
+
on: string;
|
|
31
|
+
off: string;
|
|
32
|
+
description: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SETTINGS: SettingSpec[] = [
|
|
36
|
+
{
|
|
37
|
+
field: "compactMessages",
|
|
38
|
+
label: "Message style",
|
|
39
|
+
on: "compact",
|
|
40
|
+
off: "card",
|
|
41
|
+
description:
|
|
42
|
+
"How scheduled fires and Monitor events are drawn. compact: \u273b header and indented body; card: the old padded card.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
field: "footer",
|
|
46
|
+
label: "Footer summary",
|
|
47
|
+
on: "on",
|
|
48
|
+
off: "off",
|
|
49
|
+
description: "One line in Pi's footer while loops or monitors run: loop 2 \u00b7 next 3m, monitor 2.",
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pi's getSettingsListTheme(), built from the theme the panel is given: the
|
|
55
|
+
* global theme that function reads is not shared with extensions in every
|
|
56
|
+
* Pi runtime (see the note on Pi's DynamicBorder).
|
|
57
|
+
*/
|
|
58
|
+
function listTheme(theme: PanelTheme): SettingsListTheme {
|
|
59
|
+
return {
|
|
60
|
+
label: (text, selected) => (selected ? theme.fg("accent", text) : text),
|
|
61
|
+
value: (text, selected) => (selected ? theme.fg("accent", text) : theme.fg("muted", text)),
|
|
62
|
+
description: (text) => theme.fg("dim", text),
|
|
63
|
+
cursor: theme.fg("accent", "\u2192 "),
|
|
64
|
+
hint: (text) => theme.fg("dim", text),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A path with the home directory shortened to ~. */
|
|
69
|
+
function displayPath(file: string): string {
|
|
70
|
+
const home = os.homedir();
|
|
71
|
+
return file === home || file.startsWith(home + path.sep) ? `~${file.slice(home.length)}` : file;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function item(spec: SettingSpec): SettingItem {
|
|
75
|
+
const value = loadConfig()[spec.field] ? spec.on : spec.off;
|
|
76
|
+
const override = envOverride(spec.field);
|
|
77
|
+
if (override) {
|
|
78
|
+
// No values: Enter/Space leaves it alone, and the description says why.
|
|
79
|
+
return {
|
|
80
|
+
id: spec.field,
|
|
81
|
+
label: spec.label,
|
|
82
|
+
currentValue: value,
|
|
83
|
+
description: `${spec.description}\n${override} overrides this while set; unset it to change it here.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return { id: spec.field, label: spec.label, currentValue: value, values: [spec.on, spec.off], description: spec.description };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface SettingsPanelOptions {
|
|
90
|
+
theme: PanelTheme;
|
|
91
|
+
/** Where the settings are saved, shown in the title. */
|
|
92
|
+
file: string;
|
|
93
|
+
/** Save one setting (and tell the other packages). */
|
|
94
|
+
onChange(field: keyof PiCcConfig, value: boolean): void;
|
|
95
|
+
onClose(): void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface SettingsPanel {
|
|
99
|
+
render(width: number): string[];
|
|
100
|
+
invalidate(): void;
|
|
101
|
+
handleInput(data: string): void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function settingsPanel(options: SettingsPanelOptions): SettingsPanel {
|
|
105
|
+
const { theme } = options;
|
|
106
|
+
const list = new SettingsList(
|
|
107
|
+
SETTINGS.map(item),
|
|
108
|
+
SETTINGS.length,
|
|
109
|
+
listTheme(theme),
|
|
110
|
+
(id, value) => {
|
|
111
|
+
const spec = SETTINGS.find((candidate) => candidate.field === id);
|
|
112
|
+
if (!spec) return;
|
|
113
|
+
options.onChange(spec.field, value === spec.on);
|
|
114
|
+
// Show what is in effect now.
|
|
115
|
+
list.updateValue(id, loadConfig()[spec.field] ? spec.on : spec.off);
|
|
116
|
+
},
|
|
117
|
+
() => options.onClose(),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const border = () => new DynamicBorder((text: string) => theme.fg("border", text));
|
|
121
|
+
const container = new Container();
|
|
122
|
+
container.addChild(border());
|
|
123
|
+
container.addChild(
|
|
124
|
+
new Text(`${theme.fg("accent", theme.bold("pi-cc settings"))}${theme.fg("dim", ` \u00b7 ${displayPath(options.file)}`)}`, 0, 0),
|
|
125
|
+
);
|
|
126
|
+
container.addChild(new Spacer(1));
|
|
127
|
+
container.addChild(list);
|
|
128
|
+
container.addChild(border());
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
render: (width) => container.render(width),
|
|
132
|
+
invalidate: () => container.invalidate(),
|
|
133
|
+
handleInput: (data) => list.handleInput(data),
|
|
134
|
+
};
|
|
135
|
+
}
|