opencode-tweaks 0.0.0-stage → 0.4.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.
@@ -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
+ }