claw-dashboard 1.8.4 → 2.0.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/.c8rc.json +38 -0
- package/.dockerignore +68 -0
- package/.github/dependabot.yml +45 -0
- package/.github/pull_request_template.md +39 -0
- package/.github/workflows/ci.yml +147 -0
- package/.github/workflows/release.yml +40 -0
- package/.github/workflows/security.yml +84 -0
- package/.husky/pre-commit +1 -0
- package/.planning/codebase/ARCHITECTURE.md +206 -0
- package/.planning/codebase/INTEGRATIONS.md +150 -0
- package/.planning/codebase/STACK.md +122 -0
- package/.planning/codebase/STRUCTURE.md +201 -0
- package/CHANGELOG.md +293 -0
- package/CONTRIBUTING.md +378 -0
- package/Dockerfile +54 -0
- package/FEATURES.md +195 -21
- package/README.md +83 -6
- package/TODO.md +28 -0
- package/build-cjs.js +127 -0
- package/cjs-shim.js +13 -0
- package/dist/clawdash +1729 -0
- package/dist/clawdash.meta.json +2236 -0
- package/dist/widgets.cjs +6511 -0
- package/docker-compose.yml +67 -0
- package/docs/API.md +1050 -0
- package/docs/PLUGINS.md +1504 -0
- package/esbuild.config.js +158 -0
- package/eslint.config.js +56 -0
- package/examples/plugins/README.md +122 -0
- package/examples/plugins/api-status/index.js +294 -0
- package/examples/plugins/api-status/plugin.json +19 -0
- package/examples/plugins/hello-world/index.js +104 -0
- package/examples/plugins/hello-world/plugin.json +15 -0
- package/examples/plugins/system-metrics-chart/index.js +339 -0
- package/examples/plugins/system-metrics-chart/plugin.json +18 -0
- package/examples/plugins/weather-widget/index.js +136 -0
- package/examples/plugins/weather-widget/plugin.json +19 -0
- package/index.cjs +23005 -0
- package/index.js +5331 -512
- package/jest.config.js +11 -0
- package/man/clawdash.1 +276 -0
- package/package.json +54 -5
- package/schemas/plugin-manifest.json +128 -0
- package/scripts/release.js +595 -0
- package/src/alerts.js +693 -0
- package/src/auto-save.js +584 -0
- package/src/cache.js +390 -0
- package/src/checksum.js +146 -0
- package/src/cli/args.js +110 -0
- package/src/cli/export-schedule.js +423 -0
- package/src/cli/export-snapshot.js +138 -0
- package/src/cli/help.js +69 -0
- package/src/cli/import-snapshot.js +230 -0
- package/src/cli/index.js +14 -0
- package/src/cli/list-templates.js +69 -0
- package/src/cli/validate-config.js +76 -0
- package/src/cli/validate-plugin.js +187 -0
- package/src/cli/version.js +15 -0
- package/src/config-validator.js +586 -0
- package/src/config-watcher.js +401 -0
- package/src/config.js +684 -0
- package/src/container-detector.js +499 -0
- package/src/database.js +734 -0
- package/src/differential-render.js +327 -0
- package/src/errors.js +169 -0
- package/src/export-scheduler.js +581 -0
- package/src/gateway-manager.js +685 -0
- package/src/hints.js +371 -0
- package/src/loading-states.js +509 -0
- package/src/logger.js +185 -0
- package/src/memory-pressure.js +467 -0
- package/src/performance-monitor.js +374 -0
- package/src/plugin-errors.js +586 -0
- package/src/plugin-manifest-validator.js +219 -0
- package/src/plugin-reload.js +481 -0
- package/src/plugin-scaffold.js +1941 -0
- package/src/plugin-validator.js +284 -0
- package/src/retry.js +218 -0
- package/src/security.js +860 -0
- package/src/snapshot.js +372 -0
- package/src/splash.js +115 -0
- package/src/theme-selector.js +411 -0
- package/src/themes.js +874 -0
- package/src/transitions.js +534 -0
- package/src/types.d.ts +174 -0
- package/src/validation.js +926 -0
- package/src/web-server.js +885 -0
- package/src/widgets/builtin-widgets.js +1057 -0
- package/src/widgets/config-processor.js +401 -0
- package/src/widgets/dependency-resolver.js +596 -0
- package/src/widgets/index.js +79 -0
- package/src/widgets/plugin-api.js +845 -0
- package/src/widgets/widget-error-boundary.js +773 -0
- package/src/widgets/widget-error-isolation.js +551 -0
- package/src/widgets/widget-loader.js +1437 -0
- package/src/workers/system-worker.js +130 -0
- package/src/workers/worker-pool.js +633 -0
- package/tests/alerts.test.js +437 -0
- package/tests/auto-save.test.js +529 -0
- package/tests/cache.test.js +317 -0
- package/tests/cli.test.js +351 -0
- package/tests/command-palette.test.js +320 -0
- package/tests/config-processor.test.js +452 -0
- package/tests/config-validator.test.js +710 -0
- package/tests/config-watcher.test.js +594 -0
- package/tests/config.test.js +755 -0
- package/tests/database.test.js +438 -0
- package/tests/errors.test.js +189 -0
- package/tests/example-plugins.test.js +624 -0
- package/tests/integration.test.js +1321 -0
- package/tests/loading-states.test.js +300 -0
- package/tests/manifest-validation-on-load.test.js +671 -0
- package/tests/performance-monitor.test.js +190 -0
- package/tests/plugin-api-rate-limit.test.js +302 -0
- package/tests/plugin-errors.test.js +311 -0
- package/tests/plugin-lifecycle-e2e.test.js +1036 -0
- package/tests/plugin-reload.test.js +764 -0
- package/tests/rate-limiter.test.js +539 -0
- package/tests/retry.test.js +308 -0
- package/tests/security.test.js +411 -0
- package/tests/settings-modal.test.js +226 -0
- package/tests/theme-selector.test.js +57 -0
- package/tests/utils.js +242 -0
- package/tests/utils.test.js +317 -0
- package/tests/validate-plugin-cli.test.js +359 -0
- package/tests/validation.test.js +837 -0
- package/tests/web-server.test.js +646 -0
- package/tests/widget-arrange-mode.test.js +183 -0
- package/tests/widget-config-hot-reload.test.js +465 -0
- package/tests/widget-dependency.test.js +591 -0
- package/tests/widget-error-boundary.test.js +749 -0
- package/tests/widget-error-isolation.test.js +469 -0
- package/tests/widget-integration.test.js +1147 -0
- package/tests/widget-refresh-intervals.test.js +284 -0
- package/tests/worker-pool.test.js +522 -0
package/src/themes.js
ADDED
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme definitions for the Claw Dashboard
|
|
3
|
+
* Contains multiple color themes for borders, text, gauges, charts, and alerts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import logger from './logger.js';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
|
|
11
|
+
// Settings path for theme persistence
|
|
12
|
+
const SETTINGS_PATH = process.env.HOME + '/.openclaw/dashboard-settings.json';
|
|
13
|
+
const THEME_KEY = 'theme';
|
|
14
|
+
|
|
15
|
+
// Theme change listeners
|
|
16
|
+
const themeChangeListeners = new Set();
|
|
17
|
+
let systemThemeWatcher = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Detect system-wide dark/light mode (macOS)
|
|
21
|
+
* Uses AppleInterfaceStyle which returns "Dark" or "" (empty for light)
|
|
22
|
+
* @returns {string|null} 'dark', 'light', or null if unable to detect
|
|
23
|
+
*/
|
|
24
|
+
function detectMacOSAppearance() {
|
|
25
|
+
try {
|
|
26
|
+
const result = execSync(
|
|
27
|
+
'defaults read -g AppleInterfaceStyle 2>/dev/null || echo "Light"',
|
|
28
|
+
{ encoding: 'utf8', timeout: 1000 }
|
|
29
|
+
);
|
|
30
|
+
const style = result.trim();
|
|
31
|
+
return style === 'Dark' ? 'dark' : 'light';
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Detect system theme on Linux using freedesktop settings
|
|
39
|
+
* @returns {string|null} 'dark', 'light', or null if unable to detect
|
|
40
|
+
*/
|
|
41
|
+
function detectLinuxAppearance() {
|
|
42
|
+
try {
|
|
43
|
+
// Try gsettings for GNOME/GTK-based desktops
|
|
44
|
+
const result = execSync(
|
|
45
|
+
'gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null || echo "default"',
|
|
46
|
+
{ encoding: 'utf8', timeout: 1000 }
|
|
47
|
+
);
|
|
48
|
+
const scheme = result.trim().replace(/'/g, '');
|
|
49
|
+
if (scheme === 'prefer-dark') return 'dark';
|
|
50
|
+
if (scheme === 'prefer-light') return 'light';
|
|
51
|
+
|
|
52
|
+
// Fallback: check GTK theme name
|
|
53
|
+
const themeResult = execSync(
|
|
54
|
+
'gsettings get org.gnome.desktop.interface gtk-theme 2>/dev/null || echo ""',
|
|
55
|
+
{ encoding: 'utf8', timeout: 1000 }
|
|
56
|
+
);
|
|
57
|
+
const theme = themeResult.trim().toLowerCase();
|
|
58
|
+
if (theme.includes('dark')) return 'dark';
|
|
59
|
+
if (theme.includes('light')) return 'light';
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Detect system theme using environment variables and COLORFGBG
|
|
69
|
+
* @returns {string|null} 'dark', 'light', or null
|
|
70
|
+
*/
|
|
71
|
+
function detectFromEnvironment() {
|
|
72
|
+
// Check COLORFGBG which some terminals set (e.g., "15;0" means white on black)
|
|
73
|
+
const colorFgBg = process.env.COLORFGBG;
|
|
74
|
+
if (colorFgBg) {
|
|
75
|
+
const parts = colorFgBg.split(';');
|
|
76
|
+
if (parts.length >= 2) {
|
|
77
|
+
// Standard colors: 0-7 are dark, 8-15 are light/bright
|
|
78
|
+
const bgColor = parseInt(parts[1], 10);
|
|
79
|
+
if (bgColor >= 0 && bgColor <= 7) return 'dark'; // Dark background
|
|
80
|
+
if (bgColor >= 8 && bgColor <= 15) return 'light'; // Light background
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check for explicit dark mode env vars
|
|
85
|
+
if (process.env.DARK_MODE === '1' || process.env.THEME === 'dark') {
|
|
86
|
+
return 'dark';
|
|
87
|
+
}
|
|
88
|
+
if (process.env.THEME === 'light') {
|
|
89
|
+
return 'light';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Detect system theme using multiple methods
|
|
97
|
+
* Falls back to terminal background detection if system detection fails
|
|
98
|
+
* @returns {string} 'light' or 'dark'
|
|
99
|
+
*/
|
|
100
|
+
function detectSystemTheme() {
|
|
101
|
+
let theme = null;
|
|
102
|
+
|
|
103
|
+
// Try platform-specific detection first
|
|
104
|
+
const platform = os.platform();
|
|
105
|
+
if (platform === 'darwin') {
|
|
106
|
+
theme = detectMacOSAppearance();
|
|
107
|
+
} else if (platform === 'linux') {
|
|
108
|
+
theme = detectLinuxAppearance();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Fall back to environment detection
|
|
112
|
+
if (!theme) {
|
|
113
|
+
theme = detectFromEnvironment();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Final fallback to terminal detection
|
|
117
|
+
if (!theme) {
|
|
118
|
+
theme = detectTerminalBackground();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return theme;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Start watching for system theme changes (macOS and Linux)
|
|
126
|
+
* macOS: Uses polling every 2 seconds via AppleInterfaceStyle
|
|
127
|
+
* Linux: Uses fs.watch on dconf database for efficient monitoring
|
|
128
|
+
* @param {Function} callback - Called when theme changes with new theme ('dark' or 'light')
|
|
129
|
+
* @returns {Object|null} Watcher object with stop() method, or null if not supported
|
|
130
|
+
*/
|
|
131
|
+
function startSystemThemeWatcher(callback) {
|
|
132
|
+
const platform = os.platform();
|
|
133
|
+
|
|
134
|
+
if (platform === 'darwin') {
|
|
135
|
+
return startMacOSThemeWatcher(callback);
|
|
136
|
+
} else if (platform === 'linux') {
|
|
137
|
+
return startLinuxThemeWatcher(callback);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
logger.debug('System theme watching not supported on this platform');
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* macOS theme watcher using polling
|
|
146
|
+
* @param {Function} callback - Called when theme changes
|
|
147
|
+
* @returns {Object} Watcher object with stop() method
|
|
148
|
+
*/
|
|
149
|
+
function startMacOSThemeWatcher(callback) {
|
|
150
|
+
let lastTheme = detectMacOSAppearance();
|
|
151
|
+
|
|
152
|
+
// Poll every 2 seconds for theme changes
|
|
153
|
+
const intervalId = setInterval(() => {
|
|
154
|
+
const currentTheme = detectMacOSAppearance();
|
|
155
|
+
if (currentTheme && currentTheme !== lastTheme) {
|
|
156
|
+
logger.info(`System theme changed: ${lastTheme} -> ${currentTheme}`);
|
|
157
|
+
lastTheme = currentTheme;
|
|
158
|
+
callback(currentTheme);
|
|
159
|
+
}
|
|
160
|
+
}, 2000);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
stop: () => {
|
|
164
|
+
clearInterval(intervalId);
|
|
165
|
+
logger.debug('macOS theme watcher stopped');
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Linux theme watcher using gsettings monitoring
|
|
172
|
+
* Uses fs.watch on the dconf database file for efficient change detection
|
|
173
|
+
* Falls back to polling if file watching fails
|
|
174
|
+
* @param {Function} callback - Called when theme changes
|
|
175
|
+
* @returns {Object|null} Watcher object with stop() method, or null if not supported
|
|
176
|
+
*/
|
|
177
|
+
function startLinuxThemeWatcher(callback) {
|
|
178
|
+
// Get the dconf database file path
|
|
179
|
+
const dconfPath = process.env.DCONF_PROFILE
|
|
180
|
+
? `/etc/dconf/db/${process.env.DCONF_PROFILE}`
|
|
181
|
+
: `${process.env.HOME}/.config/dconf/user`;
|
|
182
|
+
|
|
183
|
+
let lastTheme = detectLinuxAppearance();
|
|
184
|
+
let watcher = null;
|
|
185
|
+
let intervalId = null;
|
|
186
|
+
|
|
187
|
+
// Try to use file watching for efficient monitoring
|
|
188
|
+
if (fs.existsSync(dconfPath)) {
|
|
189
|
+
try {
|
|
190
|
+
// Watch the dconf database file for changes
|
|
191
|
+
watcher = fs.watch(dconfPath, (eventType) => {
|
|
192
|
+
if (eventType === 'change') {
|
|
193
|
+
const currentTheme = detectLinuxAppearance();
|
|
194
|
+
if (currentTheme && currentTheme !== lastTheme) {
|
|
195
|
+
logger.info(`System theme changed: ${lastTheme} -> ${currentTheme}`);
|
|
196
|
+
lastTheme = currentTheme;
|
|
197
|
+
callback(currentTheme);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
logger.debug(`Linux theme watcher started via fs.watch on ${dconfPath}`);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
logger.debug(`Failed to watch dconf file: ${err.message}, falling back to polling`);
|
|
205
|
+
watcher = null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Fallback to polling if file watching failed or file doesn't exist
|
|
210
|
+
if (!watcher) {
|
|
211
|
+
intervalId = setInterval(() => {
|
|
212
|
+
const currentTheme = detectLinuxAppearance();
|
|
213
|
+
if (currentTheme && currentTheme !== lastTheme) {
|
|
214
|
+
logger.info(`System theme changed: ${lastTheme} -> ${currentTheme}`);
|
|
215
|
+
lastTheme = currentTheme;
|
|
216
|
+
callback(currentTheme);
|
|
217
|
+
}
|
|
218
|
+
}, 3000); // Poll every 3 seconds as fallback
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
stop: () => {
|
|
223
|
+
if (watcher) {
|
|
224
|
+
watcher.close();
|
|
225
|
+
}
|
|
226
|
+
if (intervalId) {
|
|
227
|
+
clearInterval(intervalId);
|
|
228
|
+
}
|
|
229
|
+
logger.debug('Linux theme watcher stopped');
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Register a callback to be called when the theme changes
|
|
236
|
+
* @param {Function} callback - Function(themeName) called when theme changes
|
|
237
|
+
* @returns {Function} Unsubscribe function
|
|
238
|
+
*/
|
|
239
|
+
function onThemeChange(callback) {
|
|
240
|
+
themeChangeListeners.add(callback);
|
|
241
|
+
return () => themeChangeListeners.delete(callback);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Notify all registered listeners of a theme change
|
|
246
|
+
* @param {string} themeName - Name of the new theme
|
|
247
|
+
*/
|
|
248
|
+
function notifyThemeChange(themeName) {
|
|
249
|
+
themeChangeListeners.forEach(callback => {
|
|
250
|
+
try {
|
|
251
|
+
callback(themeName);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
logger.debug(`Theme change listener error: ${err.message}`);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Start automatic system theme detection and switching
|
|
260
|
+
* Only works when current theme is set to 'auto'
|
|
261
|
+
* @returns {Object|null} Watcher object or null if not started
|
|
262
|
+
*/
|
|
263
|
+
function startAutoThemeDetection() {
|
|
264
|
+
// Stop existing watcher if any
|
|
265
|
+
if (systemThemeWatcher) {
|
|
266
|
+
systemThemeWatcher.stop();
|
|
267
|
+
systemThemeWatcher = null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Only start if theme is set to 'auto'
|
|
271
|
+
if (currentThemeName !== 'auto') {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
systemThemeWatcher = startSystemThemeWatcher((newTheme) => {
|
|
276
|
+
// Re-detect and notify listeners
|
|
277
|
+
detectedBackground = newTheme;
|
|
278
|
+
notifyThemeChange('auto');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (systemThemeWatcher) {
|
|
282
|
+
logger.info('Auto theme detection started - watching for system theme changes');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return systemThemeWatcher;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Stop automatic system theme detection
|
|
290
|
+
*/
|
|
291
|
+
function stopAutoThemeDetection() {
|
|
292
|
+
if (systemThemeWatcher) {
|
|
293
|
+
systemThemeWatcher.stop();
|
|
294
|
+
systemThemeWatcher = null;
|
|
295
|
+
logger.debug('Auto theme detection stopped');
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Detect terminal background color (light or dark)
|
|
301
|
+
* @returns {string} 'light' or 'dark'
|
|
302
|
+
*/
|
|
303
|
+
function detectTerminalBackground() {
|
|
304
|
+
try {
|
|
305
|
+
// Check common terminal environment variables
|
|
306
|
+
const termProgram = process.env.TERM_PROGRAM || '';
|
|
307
|
+
const colorTerm = process.env.COLORTERM || '';
|
|
308
|
+
const term = process.env.TERM || '';
|
|
309
|
+
|
|
310
|
+
// iTerm2 on macOS
|
|
311
|
+
if (termProgram === 'iTerm.app' || termProgram === 'vscode') {
|
|
312
|
+
// Try to get background color from iTerm
|
|
313
|
+
if (termProgram === 'iTerm.app') {
|
|
314
|
+
try {
|
|
315
|
+
const itermBg = execSync(
|
|
316
|
+
'osascript -e \'tell app "System Events" to tell process "iTerm2" to get value of attribute "AXBackgroundColor" of window 1\'',
|
|
317
|
+
{ encoding: 'utf8', timeout: 1000 }
|
|
318
|
+
);
|
|
319
|
+
if (itermBg && itermBg.trim()) {
|
|
320
|
+
// Parse RGB values - if dark, first values will be low
|
|
321
|
+
const rgb = itermBg.match(/\d+/g);
|
|
322
|
+
if (rgb && rgb.length >= 3) {
|
|
323
|
+
const brightness = (parseInt(rgb[0]) + parseInt(rgb[1]) + parseInt(rgb[2])) / (255 * 3);
|
|
324
|
+
return brightness < 0.5 ? 'dark' : 'light';
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} catch {}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// VS Code terminal - assume dark
|
|
331
|
+
if (termProgram === 'vscode') {
|
|
332
|
+
return 'dark';
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Check for common light terminal indicators
|
|
337
|
+
const lightTermIndicators = ['-light', 'light'];
|
|
338
|
+
const isLightTerm = lightTermIndicators.some(ind =>
|
|
339
|
+
term.toLowerCase().includes(ind) || colorTerm.toLowerCase().includes(ind)
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
if (isLightTerm) {
|
|
343
|
+
return 'light';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Check for common dark terminal indicators
|
|
347
|
+
const darkTermIndicators = ['-256color', 'dark', 'truecolor'];
|
|
348
|
+
const isDarkTerm = darkTermIndicators.some(ind =>
|
|
349
|
+
term.toLowerCase().includes(ind)
|
|
350
|
+
) || termProgram !== '';
|
|
351
|
+
|
|
352
|
+
if (isDarkTerm) {
|
|
353
|
+
return 'dark';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Check for explicit background color setting in iTerm
|
|
357
|
+
if (process.env.TERM_SESSION_ID) {
|
|
358
|
+
try {
|
|
359
|
+
const profile = execSync(
|
|
360
|
+
'osascript -e \'tell app "iTerm2" to tell current session of current window to get background color\'',
|
|
361
|
+
{ encoding: 'utf8', timeout: 1000 }
|
|
362
|
+
);
|
|
363
|
+
if (profile) {
|
|
364
|
+
const rgb = profile.match(/\d+/g);
|
|
365
|
+
if (rgb && rgb.length >= 3) {
|
|
366
|
+
const brightness = (parseInt(rgb[0]) + parseInt(rgb[1]) + parseInt(rgb[2])) / (255 * 3);
|
|
367
|
+
return brightness < 0.5 ? 'dark' : 'light';
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
} catch {}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Check Apple Terminal
|
|
374
|
+
if (termProgram === 'Apple_Terminal') {
|
|
375
|
+
try {
|
|
376
|
+
const bgColor = execSync(
|
|
377
|
+
'osascript -e \'tell app "Terminal" to get background color of window 1\'',
|
|
378
|
+
{ encoding: 'utf8', timeout: 1000 }
|
|
379
|
+
);
|
|
380
|
+
if (bgColor) {
|
|
381
|
+
const rgb = bgColor.match(/\d+/g);
|
|
382
|
+
if (rgb && rgb.length >= 3) {
|
|
383
|
+
const brightness = (parseInt(rgb[0]) + parseInt(rgb[1]) + parseInt(rgb[2])) / (255 * 3);
|
|
384
|
+
return brightness < 0.5 ? 'dark' : 'light';
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
} catch {}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Default to dark for unknown terminals (more common for developers)
|
|
391
|
+
return 'dark';
|
|
392
|
+
} catch (err) {
|
|
393
|
+
logger.debug(`Background detection failed: ${err.message}`);
|
|
394
|
+
return 'dark'; // Default to dark
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Theme definitions
|
|
399
|
+
const themes = {
|
|
400
|
+
default: {
|
|
401
|
+
name: 'Default',
|
|
402
|
+
colors: {
|
|
403
|
+
// Borders
|
|
404
|
+
border: {
|
|
405
|
+
sessions: 'blue',
|
|
406
|
+
logs: 'cyan',
|
|
407
|
+
cpu: 'cyan',
|
|
408
|
+
memory: 'magenta',
|
|
409
|
+
gpu: 'yellow',
|
|
410
|
+
network: 'brightCyan',
|
|
411
|
+
disk: 'green',
|
|
412
|
+
system: 'gray',
|
|
413
|
+
uptime: 'brightMagenta',
|
|
414
|
+
gateway: 'cyan',
|
|
415
|
+
help: 'brightCyan',
|
|
416
|
+
settings: 'brightCyan',
|
|
417
|
+
modal: 'brightBlue'
|
|
418
|
+
},
|
|
419
|
+
// Text
|
|
420
|
+
text: {
|
|
421
|
+
primary: 'white',
|
|
422
|
+
secondary: 'gray',
|
|
423
|
+
bright: 'brightWhite',
|
|
424
|
+
header: 'brightWhite'
|
|
425
|
+
},
|
|
426
|
+
// Status indicators
|
|
427
|
+
status: {
|
|
428
|
+
active: 'green',
|
|
429
|
+
idle: 'yellow',
|
|
430
|
+
stale: 'gray',
|
|
431
|
+
error: 'red',
|
|
432
|
+
warning: 'yellow',
|
|
433
|
+
success: 'green'
|
|
434
|
+
},
|
|
435
|
+
// Gauges and values
|
|
436
|
+
gauge: {
|
|
437
|
+
low: 'green',
|
|
438
|
+
medium: 'yellow',
|
|
439
|
+
high: 'red',
|
|
440
|
+
critical: 'brightRed'
|
|
441
|
+
},
|
|
442
|
+
// Charts
|
|
443
|
+
chart: {
|
|
444
|
+
line: 'cyan',
|
|
445
|
+
fill: 'blue',
|
|
446
|
+
grid: 'gray'
|
|
447
|
+
},
|
|
448
|
+
// Alerts
|
|
449
|
+
alert: {
|
|
450
|
+
info: 'cyan',
|
|
451
|
+
warning: 'yellow',
|
|
452
|
+
error: 'red',
|
|
453
|
+
success: 'green'
|
|
454
|
+
},
|
|
455
|
+
// Logo and title
|
|
456
|
+
branding: {
|
|
457
|
+
logo: 'brightCyan',
|
|
458
|
+
title: 'brightWhite',
|
|
459
|
+
clock: 'brightCyan'
|
|
460
|
+
},
|
|
461
|
+
// Footer
|
|
462
|
+
footer: {
|
|
463
|
+
bg: 'black',
|
|
464
|
+
fg: 'gray'
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
dark: {
|
|
470
|
+
name: 'Dark',
|
|
471
|
+
colors: {
|
|
472
|
+
// Borders - muted tones
|
|
473
|
+
border: {
|
|
474
|
+
sessions: 'cyan',
|
|
475
|
+
logs: 'blue',
|
|
476
|
+
cpu: 'green',
|
|
477
|
+
memory: 'magenta',
|
|
478
|
+
gpu: 'yellow',
|
|
479
|
+
network: 'cyan',
|
|
480
|
+
disk: 'green',
|
|
481
|
+
system: 'gray',
|
|
482
|
+
uptime: 'magenta',
|
|
483
|
+
gateway: 'cyan',
|
|
484
|
+
help: 'cyan',
|
|
485
|
+
settings: 'cyan',
|
|
486
|
+
modal: 'cyan'
|
|
487
|
+
},
|
|
488
|
+
// Text
|
|
489
|
+
text: {
|
|
490
|
+
primary: 'white',
|
|
491
|
+
secondary: 'black',
|
|
492
|
+
bright: 'brightWhite',
|
|
493
|
+
header: 'brightWhite'
|
|
494
|
+
},
|
|
495
|
+
// Status indicators
|
|
496
|
+
status: {
|
|
497
|
+
active: 'green',
|
|
498
|
+
idle: 'yellow',
|
|
499
|
+
stale: 'black',
|
|
500
|
+
error: 'red',
|
|
501
|
+
warning: 'yellow',
|
|
502
|
+
success: 'green'
|
|
503
|
+
},
|
|
504
|
+
// Gauges and values
|
|
505
|
+
gauge: {
|
|
506
|
+
low: 'green',
|
|
507
|
+
medium: 'yellow',
|
|
508
|
+
high: 'red',
|
|
509
|
+
critical: 'brightRed'
|
|
510
|
+
},
|
|
511
|
+
// Charts
|
|
512
|
+
chart: {
|
|
513
|
+
line: 'green',
|
|
514
|
+
fill: 'cyan',
|
|
515
|
+
grid: 'black'
|
|
516
|
+
},
|
|
517
|
+
// Alerts
|
|
518
|
+
alert: {
|
|
519
|
+
info: 'cyan',
|
|
520
|
+
warning: 'yellow',
|
|
521
|
+
error: 'red',
|
|
522
|
+
success: 'green'
|
|
523
|
+
},
|
|
524
|
+
// Logo and title
|
|
525
|
+
branding: {
|
|
526
|
+
logo: 'green',
|
|
527
|
+
title: 'brightGreen',
|
|
528
|
+
clock: 'green'
|
|
529
|
+
},
|
|
530
|
+
// Footer
|
|
531
|
+
footer: {
|
|
532
|
+
bg: 'black',
|
|
533
|
+
fg: 'green'
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
'high-contrast': {
|
|
539
|
+
name: 'High Contrast',
|
|
540
|
+
colors: {
|
|
541
|
+
// Borders - bright on black
|
|
542
|
+
border: {
|
|
543
|
+
sessions: 'brightWhite',
|
|
544
|
+
logs: 'brightWhite',
|
|
545
|
+
cpu: 'brightWhite',
|
|
546
|
+
memory: 'brightWhite',
|
|
547
|
+
gpu: 'brightWhite',
|
|
548
|
+
network: 'brightWhite',
|
|
549
|
+
disk: 'brightWhite',
|
|
550
|
+
system: 'brightWhite',
|
|
551
|
+
uptime: 'brightWhite',
|
|
552
|
+
gateway: 'brightWhite',
|
|
553
|
+
help: 'brightWhite',
|
|
554
|
+
settings: 'brightWhite',
|
|
555
|
+
modal: 'brightWhite'
|
|
556
|
+
},
|
|
557
|
+
// Text
|
|
558
|
+
text: {
|
|
559
|
+
primary: 'white',
|
|
560
|
+
secondary: 'brightWhite',
|
|
561
|
+
bright: 'brightWhite',
|
|
562
|
+
header: 'brightWhite'
|
|
563
|
+
},
|
|
564
|
+
// Status indicators
|
|
565
|
+
status: {
|
|
566
|
+
active: 'brightGreen',
|
|
567
|
+
idle: 'brightYellow',
|
|
568
|
+
stale: 'brightWhite',
|
|
569
|
+
error: 'brightRed',
|
|
570
|
+
warning: 'brightYellow',
|
|
571
|
+
success: 'brightGreen'
|
|
572
|
+
},
|
|
573
|
+
// Gauges and values
|
|
574
|
+
gauge: {
|
|
575
|
+
low: 'brightGreen',
|
|
576
|
+
medium: 'brightYellow',
|
|
577
|
+
high: 'brightRed',
|
|
578
|
+
critical: 'brightRed'
|
|
579
|
+
},
|
|
580
|
+
// Charts
|
|
581
|
+
chart: {
|
|
582
|
+
line: 'brightWhite',
|
|
583
|
+
fill: 'brightWhite',
|
|
584
|
+
grid: 'brightWhite'
|
|
585
|
+
},
|
|
586
|
+
// Alerts
|
|
587
|
+
alert: {
|
|
588
|
+
info: 'brightCyan',
|
|
589
|
+
warning: 'brightYellow',
|
|
590
|
+
error: 'brightRed',
|
|
591
|
+
success: 'brightGreen'
|
|
592
|
+
},
|
|
593
|
+
// Logo and title
|
|
594
|
+
branding: {
|
|
595
|
+
logo: 'brightWhite',
|
|
596
|
+
title: 'brightWhite',
|
|
597
|
+
clock: 'brightWhite'
|
|
598
|
+
},
|
|
599
|
+
// Footer
|
|
600
|
+
footer: {
|
|
601
|
+
bg: 'white',
|
|
602
|
+
fg: 'black'
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
|
|
607
|
+
ocean: {
|
|
608
|
+
name: 'Ocean',
|
|
609
|
+
colors: {
|
|
610
|
+
// Borders - ocean blues
|
|
611
|
+
border: {
|
|
612
|
+
sessions: 'blue',
|
|
613
|
+
logs: 'cyan',
|
|
614
|
+
cpu: 'blue',
|
|
615
|
+
memory: 'cyan',
|
|
616
|
+
gpu: 'blue',
|
|
617
|
+
network: 'cyan',
|
|
618
|
+
disk: 'blue',
|
|
619
|
+
system: 'cyan',
|
|
620
|
+
uptime: 'blue',
|
|
621
|
+
gateway: 'cyan',
|
|
622
|
+
help: 'cyan',
|
|
623
|
+
settings: 'cyan',
|
|
624
|
+
modal: 'brightBlue'
|
|
625
|
+
},
|
|
626
|
+
// Text
|
|
627
|
+
text: {
|
|
628
|
+
primary: 'white',
|
|
629
|
+
secondary: 'blue',
|
|
630
|
+
bright: 'brightWhite',
|
|
631
|
+
header: 'brightCyan'
|
|
632
|
+
},
|
|
633
|
+
// Status indicators
|
|
634
|
+
status: {
|
|
635
|
+
active: 'cyan',
|
|
636
|
+
idle: 'blue',
|
|
637
|
+
stale: 'gray',
|
|
638
|
+
error: 'red',
|
|
639
|
+
warning: 'yellow',
|
|
640
|
+
success: 'green'
|
|
641
|
+
},
|
|
642
|
+
// Gauges and values
|
|
643
|
+
gauge: {
|
|
644
|
+
low: 'cyan',
|
|
645
|
+
medium: 'blue',
|
|
646
|
+
high: 'yellow',
|
|
647
|
+
critical: 'red'
|
|
648
|
+
},
|
|
649
|
+
// Charts
|
|
650
|
+
chart: {
|
|
651
|
+
line: 'cyan',
|
|
652
|
+
fill: 'blue',
|
|
653
|
+
grid: 'blue'
|
|
654
|
+
},
|
|
655
|
+
// Alerts
|
|
656
|
+
alert: {
|
|
657
|
+
info: 'cyan',
|
|
658
|
+
warning: 'yellow',
|
|
659
|
+
error: 'red',
|
|
660
|
+
success: 'green'
|
|
661
|
+
},
|
|
662
|
+
// Logo and title
|
|
663
|
+
branding: {
|
|
664
|
+
logo: 'cyan',
|
|
665
|
+
title: 'brightCyan',
|
|
666
|
+
clock: 'cyan'
|
|
667
|
+
},
|
|
668
|
+
// Footer
|
|
669
|
+
footer: {
|
|
670
|
+
bg: 'blue',
|
|
671
|
+
fg: 'cyan'
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
// Auto-detect theme - resolves to dark or light based on terminal background
|
|
677
|
+
auto: {
|
|
678
|
+
name: 'Auto-detect',
|
|
679
|
+
isAuto: true,
|
|
680
|
+
colors: null // Will be resolved dynamically
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
// Detected background state
|
|
685
|
+
let detectedBackground = null;
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Get the detected system/theme background
|
|
689
|
+
* Uses system theme detection with terminal fallback
|
|
690
|
+
* @returns {string} 'light' or 'dark'
|
|
691
|
+
*/
|
|
692
|
+
function getDetectedBackground() {
|
|
693
|
+
if (!detectedBackground) {
|
|
694
|
+
detectedBackground = detectSystemTheme();
|
|
695
|
+
logger.info(`Theme background detected: ${detectedBackground}`);
|
|
696
|
+
}
|
|
697
|
+
return detectedBackground;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Force re-detection of the system theme
|
|
702
|
+
* Useful after system theme changes
|
|
703
|
+
* @returns {string} Newly detected 'light' or 'dark'
|
|
704
|
+
*/
|
|
705
|
+
function refreshDetectedBackground() {
|
|
706
|
+
const oldTheme = detectedBackground;
|
|
707
|
+
detectedBackground = detectSystemTheme();
|
|
708
|
+
if (oldTheme !== detectedBackground) {
|
|
709
|
+
logger.info(`Theme background changed: ${oldTheme} -> ${detectedBackground}`);
|
|
710
|
+
}
|
|
711
|
+
return detectedBackground;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Resolve auto theme to actual theme based on detection
|
|
716
|
+
* @returns {object} Resolved theme object
|
|
717
|
+
*/
|
|
718
|
+
function resolveAutoTheme() {
|
|
719
|
+
const background = getDetectedBackground();
|
|
720
|
+
return background === 'light' ? themes.default : themes.dark;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Current theme state
|
|
724
|
+
let currentThemeName = 'default';
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Get the current theme
|
|
728
|
+
* @returns {object} Current theme object
|
|
729
|
+
*/
|
|
730
|
+
function getCurrentTheme() {
|
|
731
|
+
if (currentThemeName === 'auto') {
|
|
732
|
+
return resolveAutoTheme();
|
|
733
|
+
}
|
|
734
|
+
return themes[currentThemeName] || themes.default;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Get current theme name
|
|
739
|
+
* @returns {string} Current theme name
|
|
740
|
+
*/
|
|
741
|
+
function getThemeName() {
|
|
742
|
+
return currentThemeName;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Get theme by name
|
|
747
|
+
* @param {string} name - Theme name
|
|
748
|
+
* @returns {object} Theme object or null
|
|
749
|
+
*/
|
|
750
|
+
function getTheme(name) {
|
|
751
|
+
if (name === 'auto') {
|
|
752
|
+
return themes.auto;
|
|
753
|
+
}
|
|
754
|
+
return themes[name] || null;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Get all available theme names
|
|
759
|
+
* @returns {string[]} Array of theme names
|
|
760
|
+
*/
|
|
761
|
+
function getThemeNames() {
|
|
762
|
+
return Object.keys(themes);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Set the current theme
|
|
767
|
+
* @param {string} name - Theme name to set
|
|
768
|
+
* @returns {boolean} Success
|
|
769
|
+
*/
|
|
770
|
+
function setTheme(name) {
|
|
771
|
+
if (!themes[name]) {
|
|
772
|
+
logger.warn(`Theme '${name}' not found, keeping current theme`);
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
currentThemeName = name;
|
|
776
|
+
const displayName = name === 'auto' ? `Auto-detect (${getDetectedBackground()})` : themes[name].name;
|
|
777
|
+
logger.info(`Theme changed to: ${displayName}`);
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Cycle to the next theme
|
|
783
|
+
* @returns {string} New theme name
|
|
784
|
+
*/
|
|
785
|
+
function cycleTheme() {
|
|
786
|
+
const themeNames = Object.keys(themes);
|
|
787
|
+
const currentIndex = themeNames.indexOf(currentThemeName);
|
|
788
|
+
const nextIndex = (currentIndex + 1) % themeNames.length;
|
|
789
|
+
currentThemeName = themeNames[nextIndex];
|
|
790
|
+
const displayName = currentThemeName === 'auto' ? `Auto-detect (${getDetectedBackground()})` : themes[currentThemeName].name;
|
|
791
|
+
logger.info(`Theme cycled to: ${displayName}`);
|
|
792
|
+
return currentThemeName;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Load saved theme from settings
|
|
797
|
+
*/
|
|
798
|
+
function loadTheme() {
|
|
799
|
+
try {
|
|
800
|
+
const data = fs.readFileSync(SETTINGS_PATH, 'utf8');
|
|
801
|
+
const settings = JSON.parse(data);
|
|
802
|
+
if (settings[THEME_KEY] && themes[settings[THEME_KEY]]) {
|
|
803
|
+
currentThemeName = settings[THEME_KEY];
|
|
804
|
+
if (currentThemeName === 'auto') {
|
|
805
|
+
const bg = getDetectedBackground();
|
|
806
|
+
logger.info(`Loaded theme: Auto-detect (${bg} background)`);
|
|
807
|
+
} else {
|
|
808
|
+
logger.info(`Loaded theme: ${themes[currentThemeName].name}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
} catch {
|
|
812
|
+
// File doesn't exist or invalid JSON - use default
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Save current theme to settings
|
|
818
|
+
*/
|
|
819
|
+
function saveTheme() {
|
|
820
|
+
try {
|
|
821
|
+
let settings = {};
|
|
822
|
+
try {
|
|
823
|
+
const data = fs.readFileSync(SETTINGS_PATH, 'utf8');
|
|
824
|
+
settings = JSON.parse(data);
|
|
825
|
+
} catch {}
|
|
826
|
+
|
|
827
|
+
settings[THEME_KEY] = currentThemeName;
|
|
828
|
+
|
|
829
|
+
const dir = process.env.HOME + '/.openclaw';
|
|
830
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
831
|
+
fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
832
|
+
} catch (err) {
|
|
833
|
+
logger.warn(`Failed to save theme: ${err.message}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Get a color from the current theme
|
|
839
|
+
* @param {string} category - Color category (border, text, status, gauge, chart, alert, branding, footer)
|
|
840
|
+
* @param {string} key - Key within the category
|
|
841
|
+
* @returns {string} Color value
|
|
842
|
+
*/
|
|
843
|
+
function getColor(category, key) {
|
|
844
|
+
const theme = getCurrentTheme();
|
|
845
|
+
if (theme.colors[category] && theme.colors[category][key]) {
|
|
846
|
+
return theme.colors[category][key];
|
|
847
|
+
}
|
|
848
|
+
// Fallback to default theme
|
|
849
|
+
if (themes.default.colors[category] && themes.default.colors[category][key]) {
|
|
850
|
+
return themes.default.colors[category][key];
|
|
851
|
+
}
|
|
852
|
+
return 'white';
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
export default themes;
|
|
856
|
+
export {
|
|
857
|
+
themes,
|
|
858
|
+
getCurrentTheme,
|
|
859
|
+
getThemeName,
|
|
860
|
+
getTheme,
|
|
861
|
+
getThemeNames,
|
|
862
|
+
setTheme,
|
|
863
|
+
cycleTheme,
|
|
864
|
+
getColor,
|
|
865
|
+
loadTheme,
|
|
866
|
+
saveTheme,
|
|
867
|
+
getDetectedBackground,
|
|
868
|
+
refreshDetectedBackground,
|
|
869
|
+
detectSystemTheme,
|
|
870
|
+
startSystemThemeWatcher,
|
|
871
|
+
onThemeChange,
|
|
872
|
+
startAutoThemeDetection,
|
|
873
|
+
stopAutoThemeDetection
|
|
874
|
+
};
|