opencode-tweaks 0.0.0-stage → 0.4.1
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/LICENSE +21 -0
- package/README.md +222 -2
- package/package.json +62 -4
- package/src/index.ts +20 -0
- package/src/quota/format.ts +97 -0
- package/src/quota/pace.ts +180 -0
- package/src/quota/parse.ts +101 -0
- package/src/quota/service.ts +224 -0
- package/src/rpc.ts +84 -0
- package/src/tui.tsx +66 -0
- package/src/tweaks/menu-dialog.tsx +424 -0
- package/src/tweaks/menu-model.ts +55 -0
- package/src/tweaks/menu.ts +1 -0
- package/src/tweaks/quota-panel.tsx +514 -0
- package/src/tweaks/quota.ts +163 -0
- package/src/tweaks/registry.ts +10 -0
- package/src/tweaks/settings.ts +73 -0
- package/src/tweaks/token-inject.ts +724 -0
- package/src/tweaks/token-slots.tsx +327 -0
- package/src/tweaks/tokens-model.ts +213 -0
- package/src/tweaks/tokens.ts +113 -0
- package/src/tweaks/types.ts +187 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/** A value a tweak setting can hold. */
|
|
2
|
+
export type SettingValue = boolean | number | string
|
|
3
|
+
|
|
4
|
+
export type SettingKind = "boolean" | "number" | "select"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One row in the `/tweaks` menu.
|
|
8
|
+
*
|
|
9
|
+
* The menu renders settings from this definition, so a tweak never ships its
|
|
10
|
+
* own settings UI. Add a setting here and it appears in the menu with the right
|
|
11
|
+
* picker, validation, and persistence.
|
|
12
|
+
*/
|
|
13
|
+
export interface TweakSetting {
|
|
14
|
+
readonly id: string
|
|
15
|
+
readonly title: string
|
|
16
|
+
readonly description: string
|
|
17
|
+
readonly category: string
|
|
18
|
+
readonly kind: SettingKind
|
|
19
|
+
readonly default: SettingValue
|
|
20
|
+
/** Accepted values for a `select` setting. */
|
|
21
|
+
readonly values?: readonly SettingValue[]
|
|
22
|
+
/** Display labels, aligned with `values` or with `false`/`true`. */
|
|
23
|
+
readonly labels?: readonly string[]
|
|
24
|
+
/** Inclusive bounds for a `number` setting. */
|
|
25
|
+
readonly min?: number
|
|
26
|
+
readonly max?: number
|
|
27
|
+
/** Picker increment for a `number` setting. */
|
|
28
|
+
readonly step?: number
|
|
29
|
+
/** Overrides the text shown in the menu footer. */
|
|
30
|
+
readonly format?: (value: SettingValue) => string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Tweak {
|
|
34
|
+
readonly id: string
|
|
35
|
+
readonly title: string
|
|
36
|
+
readonly description: string
|
|
37
|
+
/** Group shown in the `/tweaks` list. */
|
|
38
|
+
readonly category: string
|
|
39
|
+
readonly settings: readonly TweakSetting[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Settings changed from the menu, keyed by tweak and setting id. */
|
|
43
|
+
export type StoredSettings = Record<string, Record<string, SettingValue>>
|
|
44
|
+
|
|
45
|
+
export interface ResolvedSetting {
|
|
46
|
+
readonly value: SettingValue
|
|
47
|
+
readonly source: "user" | "config" | "default"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SettingChoice {
|
|
51
|
+
readonly value: SettingValue
|
|
52
|
+
readonly label: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Checks a definition before it reaches the menu. A bad definition fails when
|
|
57
|
+
* the plugin loads, which is easier to trace than a broken row at runtime.
|
|
58
|
+
*/
|
|
59
|
+
export function defineTweak(tweak: Tweak): Tweak {
|
|
60
|
+
const ids = new Set<string>()
|
|
61
|
+
|
|
62
|
+
for (const setting of tweak.settings) {
|
|
63
|
+
if (ids.has(setting.id)) {
|
|
64
|
+
throw new Error(`Duplicate setting "${setting.id}" in tweak "${tweak.id}"`)
|
|
65
|
+
}
|
|
66
|
+
ids.add(setting.id)
|
|
67
|
+
|
|
68
|
+
switch (setting.kind) {
|
|
69
|
+
case "boolean":
|
|
70
|
+
if (typeof setting.default !== "boolean") {
|
|
71
|
+
throw new Error(`Setting "${tweak.id}.${setting.id}" needs a boolean default`)
|
|
72
|
+
}
|
|
73
|
+
break
|
|
74
|
+
|
|
75
|
+
case "select":
|
|
76
|
+
if (!setting.values?.length) {
|
|
77
|
+
throw new Error(`Setting "${tweak.id}.${setting.id}" needs at least one value`)
|
|
78
|
+
}
|
|
79
|
+
if (setting.labels && setting.labels.length !== setting.values.length) {
|
|
80
|
+
throw new Error(`Setting "${tweak.id}.${setting.id}" has mismatched labels`)
|
|
81
|
+
}
|
|
82
|
+
if (!setting.values.includes(setting.default)) {
|
|
83
|
+
throw new Error(`Setting "${tweak.id}.${setting.id}" has a default outside its values`)
|
|
84
|
+
}
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
case "number":
|
|
88
|
+
if (
|
|
89
|
+
setting.min === undefined ||
|
|
90
|
+
setting.max === undefined ||
|
|
91
|
+
setting.step === undefined ||
|
|
92
|
+
setting.min > setting.max ||
|
|
93
|
+
setting.step <= 0
|
|
94
|
+
) {
|
|
95
|
+
throw new Error(`Setting "${tweak.id}.${setting.id}" needs valid min, max, and step`)
|
|
96
|
+
}
|
|
97
|
+
if (
|
|
98
|
+
typeof setting.default !== "number" ||
|
|
99
|
+
setting.default < setting.min ||
|
|
100
|
+
setting.default > setting.max
|
|
101
|
+
) {
|
|
102
|
+
throw new Error(`Setting "${tweak.id}.${setting.id}" has a default outside its range`)
|
|
103
|
+
}
|
|
104
|
+
break
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return tweak
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Whether a stored or configured value is valid for a setting. */
|
|
112
|
+
export function isSettingValue(setting: TweakSetting, value: unknown): value is SettingValue {
|
|
113
|
+
switch (setting.kind) {
|
|
114
|
+
case "boolean":
|
|
115
|
+
return typeof value === "boolean"
|
|
116
|
+
|
|
117
|
+
case "select":
|
|
118
|
+
return setting.values?.some((choice) => choice === value) ?? false
|
|
119
|
+
|
|
120
|
+
case "number":
|
|
121
|
+
return (
|
|
122
|
+
typeof value === "number" &&
|
|
123
|
+
Number.isFinite(value) &&
|
|
124
|
+
value >= (setting.min ?? Number.NEGATIVE_INFINITY) &&
|
|
125
|
+
value <= (setting.max ?? Number.POSITIVE_INFINITY)
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The text shown next to a setting or in a picker. */
|
|
131
|
+
export function displayValue(setting: TweakSetting, value: SettingValue): string {
|
|
132
|
+
if (setting.format) return setting.format(value)
|
|
133
|
+
|
|
134
|
+
if (setting.kind === "boolean") {
|
|
135
|
+
return setting.labels?.[value ? 1 : 0] ?? (value ? "on" : "off")
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (setting.kind === "select") {
|
|
139
|
+
const index = setting.values?.indexOf(value) ?? -1
|
|
140
|
+
if (index >= 0) return setting.labels?.[index] ?? String(value)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return String(value)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Every value a picker offers for a setting. */
|
|
147
|
+
export function choicesFor(setting: TweakSetting): SettingChoice[] {
|
|
148
|
+
if (setting.kind === "boolean") {
|
|
149
|
+
return [false, true].map((value, index) => ({
|
|
150
|
+
value,
|
|
151
|
+
label: setting.labels?.[index] ?? (value ? "on" : "off"),
|
|
152
|
+
}))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (setting.kind === "select") {
|
|
156
|
+
return (setting.values ?? []).map((value, index) => ({
|
|
157
|
+
value,
|
|
158
|
+
label: setting.labels?.[index] ?? String(value),
|
|
159
|
+
}))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const min = setting.min ?? 0
|
|
163
|
+
const max = setting.max ?? 0
|
|
164
|
+
const step = setting.step ?? 1
|
|
165
|
+
const count = Math.floor((max - min) / step + 1e-9) + 1
|
|
166
|
+
const choices: SettingChoice[] = []
|
|
167
|
+
|
|
168
|
+
for (let index = 0; index < count; index++) {
|
|
169
|
+
const value = Number((min + index * step).toFixed(6))
|
|
170
|
+
choices.push({ value, label: displayValue(setting, value) })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return choices
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The value after moving one step through a setting's choices, wrapping at both ends. */
|
|
177
|
+
export function nextValue(
|
|
178
|
+
setting: TweakSetting,
|
|
179
|
+
current: SettingValue,
|
|
180
|
+
direction: 1 | -1,
|
|
181
|
+
): SettingValue | undefined {
|
|
182
|
+
const choices = choicesFor(setting)
|
|
183
|
+
if (choices.length === 0) return undefined
|
|
184
|
+
|
|
185
|
+
const index = choices.findIndex((choice) => choice.value === current)
|
|
186
|
+
return choices[(index + direction + choices.length) % choices.length]?.value
|
|
187
|
+
}
|