@npow/oh-my-claude 0.1.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.
Files changed (57) hide show
  1. package/README.md +317 -0
  2. package/bin/omc.js +403 -0
  3. package/docs/architecture.md +198 -0
  4. package/docs/segment-contract.md +186 -0
  5. package/docs/theme-format.md +156 -0
  6. package/package.json +35 -0
  7. package/src/cache.js +102 -0
  8. package/src/color.js +105 -0
  9. package/src/compositor.js +163 -0
  10. package/src/config.js +146 -0
  11. package/src/plugins.js +72 -0
  12. package/src/runner.js +160 -0
  13. package/src/segments/achievement.js +68 -0
  14. package/src/segments/api-timer.js +55 -0
  15. package/src/segments/battle-log.js +55 -0
  16. package/src/segments/cat.js +89 -0
  17. package/src/segments/coffee-cup.js +81 -0
  18. package/src/segments/commit-msg.js +95 -0
  19. package/src/segments/context-bar.js +50 -0
  20. package/src/segments/context-percent.js +40 -0
  21. package/src/segments/context-tokens.js +52 -0
  22. package/src/segments/cost-budget.js +43 -0
  23. package/src/segments/coworker.js +137 -0
  24. package/src/segments/custom-text.js +25 -0
  25. package/src/segments/directory.js +75 -0
  26. package/src/segments/emoji-story.js +99 -0
  27. package/src/segments/flex-space.js +25 -0
  28. package/src/segments/fortune-cookie.js +131 -0
  29. package/src/segments/garden.js +57 -0
  30. package/src/segments/git-branch.js +36 -0
  31. package/src/segments/git-status.js +56 -0
  32. package/src/segments/horoscope.js +134 -0
  33. package/src/segments/index.js +65 -0
  34. package/src/segments/lines-changed.js +29 -0
  35. package/src/segments/model-name.js +28 -0
  36. package/src/segments/narrator.js +129 -0
  37. package/src/segments/output-style.js +25 -0
  38. package/src/segments/rpg-stats.js +119 -0
  39. package/src/segments/separator-arrow.js +22 -0
  40. package/src/segments/separator-pipe.js +22 -0
  41. package/src/segments/separator-space.js +22 -0
  42. package/src/segments/session-cost.js +72 -0
  43. package/src/segments/session-timer.js +53 -0
  44. package/src/segments/smart-nudge.js +97 -0
  45. package/src/segments/soundtrack.js +133 -0
  46. package/src/segments/speedrun.js +94 -0
  47. package/src/segments/stock-ticker.js +71 -0
  48. package/src/segments/streak.js +131 -0
  49. package/src/segments/tamagotchi.js +95 -0
  50. package/src/segments/token-sparkline.js +73 -0
  51. package/src/segments/version.js +27 -0
  52. package/src/segments/vibe-check.js +109 -0
  53. package/src/segments/vim-mode.js +29 -0
  54. package/src/segments/weather-report.js +88 -0
  55. package/themes/default.json +59 -0
  56. package/themes/minimal.json +37 -0
  57. package/themes/powerline.json +73 -0
package/bin/omc.js ADDED
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env node
2
+
3
+ // bin/omc.js — oh-my-claude CLI
4
+ // Usage: npx oh-my-claude [command]
5
+ // Commands: install, themes, uninstall, list, validate, create
6
+
7
+ import { readFileSync, writeFileSync, mkdirSync, cpSync, existsSync, readdirSync } from 'node:fs';
8
+ import { join, dirname } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { homedir } from 'node:os';
11
+ import { createInterface } from 'node:readline';
12
+
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ const PACKAGE_ROOT = join(__dirname, '..');
15
+ const OMC_DIR = join(homedir(), '.claude', 'oh-my-claude');
16
+ const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
17
+ const CONFIG_PATH = join(OMC_DIR, 'config.json');
18
+ const PLUGINS_DIR = join(OMC_DIR, 'plugins');
19
+
20
+ // ─── Colors ──────────────────────────────────────
21
+
22
+ const C = {
23
+ reset: '\x1b[0m',
24
+ bold: '\x1b[1m',
25
+ dim: '\x1b[2m',
26
+ cyan: '\x1b[36m',
27
+ green: '\x1b[32m',
28
+ yellow: '\x1b[33m',
29
+ red: '\x1b[31m',
30
+ magenta: '\x1b[35m',
31
+ };
32
+
33
+ function log(msg = '') { console.log(msg); }
34
+ function info(msg) { log(`${C.cyan}${msg}${C.reset}`); }
35
+ function success(msg) { log(`${C.green}✓${C.reset} ${msg}`); }
36
+ function warn(msg) { log(`${C.yellow}!${C.reset} ${msg}`); }
37
+
38
+ // ─── Prompts ─────────────────────────────────────
39
+
40
+ function ask(question, options) {
41
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
42
+ return new Promise((resolve) => {
43
+ if (options) {
44
+ log(`\n${C.bold}${question}${C.reset}`);
45
+ options.forEach((opt, i) => log(` ${C.cyan}${i + 1}${C.reset}) ${opt}`));
46
+ rl.question(`\nChoose [1-${options.length}]: `, (answer) => {
47
+ rl.close();
48
+ const idx = parseInt(answer, 10) - 1;
49
+ resolve(idx >= 0 && idx < options.length ? options[idx] : options[0]);
50
+ });
51
+ } else {
52
+ rl.question(`${question} `, (answer) => {
53
+ rl.close();
54
+ resolve(answer.trim());
55
+ });
56
+ }
57
+ });
58
+ }
59
+
60
+ // ─── Install ─────────────────────────────────────
61
+
62
+ async function install() {
63
+ log(`\n${C.bold}${C.cyan} oh-my-claude${C.reset} installer\n`);
64
+ log(`${C.dim}The framework for Claude Code statuslines.${C.reset}\n`);
65
+
66
+ // 1. Choose theme
67
+ const themes = readdirSync(join(PACKAGE_ROOT, 'themes'))
68
+ .filter(f => f.endsWith('.json'))
69
+ .map(f => f.replace('.json', ''));
70
+
71
+ const theme = await ask('Which theme?', themes);
72
+ log(`${C.dim} → ${theme}${C.reset}`);
73
+
74
+ // 2. Budget
75
+ const budgetStr = await ask(`\n${C.bold}Monthly cost budget in USD?${C.reset} (default: 10) `);
76
+ const budget = parseFloat(budgetStr) || 10;
77
+ log(`${C.dim} → $${budget}${C.reset}`);
78
+
79
+ // 3. Create directories
80
+ mkdirSync(OMC_DIR, { recursive: true });
81
+ mkdirSync(join(OMC_DIR, 'plugins'), { recursive: true });
82
+
83
+ // 4. Copy framework files
84
+ const srcDest = join(OMC_DIR, 'src');
85
+ const themesDest = join(OMC_DIR, 'themes');
86
+ cpSync(join(PACKAGE_ROOT, 'src'), srcDest, { recursive: true });
87
+ cpSync(join(PACKAGE_ROOT, 'themes'), themesDest, { recursive: true });
88
+ cpSync(join(PACKAGE_ROOT, 'package.json'), join(OMC_DIR, 'package.json'));
89
+
90
+ // 5. Write user config
91
+ const config = {
92
+ theme,
93
+ segments: {
94
+ 'cost-budget': { budget },
95
+ },
96
+ };
97
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
98
+ success('Config written to ' + CONFIG_PATH);
99
+
100
+ // 6. Update Claude Code settings.json
101
+ let settings = {};
102
+ if (existsSync(SETTINGS_PATH)) {
103
+ try {
104
+ settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'));
105
+ } catch {
106
+ settings = {};
107
+ }
108
+ } else {
109
+ mkdirSync(dirname(SETTINGS_PATH), { recursive: true });
110
+ }
111
+
112
+ settings.statusLine = {
113
+ type: 'command',
114
+ command: `node ${join(OMC_DIR, 'src', 'runner.js')}`,
115
+ };
116
+
117
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
118
+ success('Updated ' + SETTINGS_PATH);
119
+
120
+ // 7. Show preview
121
+ log(`\n${C.bold}Preview:${C.reset}\n`);
122
+
123
+ const mockData = JSON.stringify({
124
+ model: { display_name: 'Opus', id: 'claude-opus-4-6' },
125
+ context_window: { used_percentage: 35, context_window_size: 200000, total_input_tokens: 70000, total_output_tokens: 10000 },
126
+ cost: { total_cost_usd: 2.45, total_duration_ms: 900000, total_api_duration_ms: 220000, total_lines_added: 83, total_lines_removed: 21 },
127
+ workspace: { current_dir: process.cwd(), project_dir: process.cwd() },
128
+ session_id: 'preview', version: '2.1.34',
129
+ });
130
+
131
+ const { execSync } = await import('node:child_process');
132
+ try {
133
+ const preview = execSync(`echo '${mockData}' | node ${join(OMC_DIR, 'src', 'runner.js')}`, { encoding: 'utf8' });
134
+ log(preview);
135
+ } catch {
136
+ log(`${C.dim}(preview unavailable)${C.reset}`);
137
+ }
138
+
139
+ log(`\n${C.green}${C.bold}Done!${C.reset} Restart Claude Code to see your new statusline.`);
140
+ log(`${C.dim}Config: ${CONFIG_PATH}`);
141
+ log(`Themes: omc themes`);
142
+ log(`Uninstall: omc uninstall${C.reset}\n`);
143
+ }
144
+
145
+ // ─── Themes ──────────────────────────────────────
146
+
147
+ function listThemes() {
148
+ log(`\n${C.bold}Available themes:${C.reset}\n`);
149
+ const themesDir = existsSync(join(OMC_DIR, 'themes')) ? join(OMC_DIR, 'themes') : join(PACKAGE_ROOT, 'themes');
150
+ const themes = readdirSync(themesDir).filter(f => f.endsWith('.json'));
151
+
152
+ for (const file of themes) {
153
+ try {
154
+ const theme = JSON.parse(readFileSync(join(themesDir, file), 'utf8'));
155
+ const name = file.replace('.json', '');
156
+
157
+ // Check if active
158
+ let active = false;
159
+ if (existsSync(CONFIG_PATH)) {
160
+ try {
161
+ const cfg = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
162
+ active = cfg.theme === name;
163
+ } catch {}
164
+ }
165
+
166
+ const marker = active ? `${C.green} ●${C.reset}` : ' ';
167
+ log(`${marker} ${C.bold}${name}${C.reset} — ${C.dim}${theme.description || ''}${C.reset}`);
168
+ log(` ${C.dim}Lines: ${theme.lines?.length || 0} | Segments: ${[...(theme.lines || [])].flatMap(l => [...(l.left || []), ...(l.right || [])]).length}${C.reset}`);
169
+ } catch {}
170
+ }
171
+ log('');
172
+ }
173
+
174
+ // ─── List Segments ───────────────────────────────
175
+
176
+ async function listSegments() {
177
+ log(`\n${C.bold}Built-in segments:${C.reset}\n`);
178
+ const segDir = existsSync(join(OMC_DIR, 'src', 'segments'))
179
+ ? join(OMC_DIR, 'src', 'segments')
180
+ : join(PACKAGE_ROOT, 'src', 'segments');
181
+
182
+ const files = readdirSync(segDir).filter(f => f.endsWith('.js') && f !== 'index.js').sort();
183
+
184
+ for (const file of files) {
185
+ try {
186
+ const mod = await import(join(segDir, file));
187
+ const name = mod.meta?.name || file.replace('.js', '');
188
+ const desc = mod.meta?.description || '';
189
+ const requires = mod.meta?.requires?.length ? ` ${C.yellow}[${mod.meta.requires.join(', ')}]${C.reset}` : '';
190
+ log(` ${C.cyan}${name}${C.reset} — ${desc}${requires}`);
191
+ } catch {}
192
+ }
193
+ log(`\n${C.dim}${files.length} segments available${C.reset}`);
194
+
195
+ // List plugins
196
+ let pluginCount = 0;
197
+ if (existsSync(PLUGINS_DIR)) {
198
+ try {
199
+ const pluginEntries = readdirSync(PLUGINS_DIR).sort();
200
+ const validPlugins = [];
201
+
202
+ for (const entry of pluginEntries) {
203
+ const entryPath = join(PLUGINS_DIR, entry);
204
+ const segmentPath = join(entryPath, 'segment.js');
205
+ try {
206
+ const { statSync: statSyncFs } = await import('node:fs');
207
+ const stat = statSyncFs(entryPath);
208
+ if (!stat.isDirectory()) continue;
209
+ if (!existsSync(segmentPath)) continue;
210
+
211
+ const { pathToFileURL } = await import('node:url');
212
+ const mod = await import(pathToFileURL(segmentPath).href);
213
+ if (!mod.meta || typeof mod.meta.name !== 'string' || typeof mod.render !== 'function') {
214
+ validPlugins.push({
215
+ name: entry,
216
+ desc: `${C.red}(invalid: missing meta.name or render)${C.reset}`,
217
+ path: segmentPath,
218
+ });
219
+ } else {
220
+ validPlugins.push({
221
+ name: mod.meta.name,
222
+ desc: mod.meta.description || '',
223
+ path: segmentPath,
224
+ });
225
+ }
226
+ } catch (err) {
227
+ validPlugins.push({
228
+ name: entry,
229
+ desc: `${C.red}(error: ${err.message})${C.reset}`,
230
+ path: segmentPath,
231
+ });
232
+ }
233
+ }
234
+
235
+ if (validPlugins.length > 0) {
236
+ log(`\n${C.bold}Plugins:${C.reset}\n`);
237
+ for (const p of validPlugins) {
238
+ log(` ${C.magenta}${p.name}${C.reset} — ${p.desc}`);
239
+ log(` ${C.dim}${p.path}${C.reset}`);
240
+ }
241
+ pluginCount = validPlugins.length;
242
+ log(`\n${C.dim}${pluginCount} plugin(s) found${C.reset}`);
243
+ }
244
+ } catch {}
245
+ }
246
+
247
+ if (pluginCount === 0) {
248
+ log(`\n${C.dim}No plugins installed. Run ${C.reset}${C.cyan}omc create <name>${C.reset}${C.dim} to create one.${C.reset}`);
249
+ }
250
+
251
+ log('');
252
+ }
253
+
254
+ // ─── Create Plugin ───────────────────────────────
255
+
256
+ function createPlugin(name) {
257
+ if (!name || typeof name !== 'string') {
258
+ warn('Usage: omc create <segment-name>');
259
+ log(`\n${C.dim}Example: omc create my-segment${C.reset}\n`);
260
+ process.exit(1);
261
+ }
262
+
263
+ // Validate segment name: lowercase letters, numbers, hyphens only
264
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
265
+ warn(`Invalid segment name: "${name}"`);
266
+ log(`\n${C.dim}Names must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens.${C.reset}`);
267
+ log(`${C.dim}Example: my-segment, cpu-usage, weather-v2${C.reset}\n`);
268
+ process.exit(1);
269
+ }
270
+
271
+ const pluginDir = join(PLUGINS_DIR, name);
272
+ const segmentPath = join(pluginDir, 'segment.js');
273
+
274
+ // Check if plugin already exists
275
+ if (existsSync(segmentPath)) {
276
+ warn(`Plugin "${name}" already exists at:`);
277
+ log(` ${C.dim}${segmentPath}${C.reset}\n`);
278
+ process.exit(1);
279
+ }
280
+
281
+ // Create plugin directory and segment file
282
+ mkdirSync(pluginDir, { recursive: true });
283
+
284
+ const template = `// ${segmentPath}
285
+ // Custom segment for oh-my-claude.
286
+ // Add "${name}" to your theme's lines array to use it.
287
+
288
+ export const meta = {
289
+ name: '${name}',
290
+ description: 'My custom segment',
291
+ requires: [],
292
+ defaultConfig: {},
293
+ };
294
+
295
+ /**
296
+ * Render this segment.
297
+ *
298
+ * @param {object} data - JSON from Claude Code (use optional chaining: data?.model?.display_name)
299
+ * @param {object} config - Per-segment config from your theme/config.json
300
+ * @returns {{ text: string, style: string } | null} Return { text, style } or null to hide
301
+ */
302
+ export function render(data, config) {
303
+ // Example: show a greeting. Replace this with your own logic.
304
+ // Available data fields:
305
+ // data?.model?.display_name - "Opus", "Sonnet", etc.
306
+ // data?.model?.id - "claude-opus-4-6", etc.
307
+ // data?.context_window?.used_percentage
308
+ // data?.cost?.total_cost_usd
309
+ // data?.workspace?.current_dir
310
+ // data?.session_id
311
+ // data?.version
312
+ //
313
+ // Return null to hide the segment when data is unavailable.
314
+ // Never throw — return null instead.
315
+
316
+ return { text: 'Hello from ${name}!', style: 'cyan' };
317
+ }
318
+ `;
319
+
320
+ writeFileSync(segmentPath, template);
321
+
322
+ log(`\n${C.green}${C.bold}Plugin created!${C.reset}\n`);
323
+ success(`File: ${segmentPath}`);
324
+ log(`\n${C.bold}To use it:${C.reset}\n`);
325
+ log(` 1. Edit the segment file to customize it:`);
326
+ log(` ${C.dim}${segmentPath}${C.reset}\n`);
327
+ log(` 2. Add ${C.cyan}"${name}"${C.reset} to a theme's lines array in your config:`);
328
+ log(` ${C.dim}${CONFIG_PATH}${C.reset}\n`);
329
+ log(` Example config.json snippet:`);
330
+ log(` ${C.dim}{`);
331
+ log(` "lines": [`);
332
+ log(` { "left": ["model-name", "${name}"], "right": ["session-cost"] }`);
333
+ log(` ]`);
334
+ log(` }${C.reset}\n`);
335
+ log(` 3. Restart Claude Code to see your segment.\n`);
336
+ }
337
+
338
+ // ─── Uninstall ───────────────────────────────────
339
+
340
+ function uninstall() {
341
+ log(`\n${C.bold}Uninstalling oh-my-claude...${C.reset}\n`);
342
+
343
+ // Remove statusLine from settings
344
+ if (existsSync(SETTINGS_PATH)) {
345
+ try {
346
+ const settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'));
347
+ delete settings.statusLine;
348
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
349
+ success('Removed statusLine from ' + SETTINGS_PATH);
350
+ } catch {
351
+ warn('Could not update settings.json');
352
+ }
353
+ }
354
+
355
+ log(`\n${C.dim}To fully remove, delete: ${OMC_DIR}${C.reset}`);
356
+ log(`${C.green}Done.${C.reset}\n`);
357
+ }
358
+
359
+ // ─── Main ────────────────────────────────────────
360
+
361
+ const command = process.argv[2] || 'install';
362
+
363
+ switch (command) {
364
+ case 'install':
365
+ install().catch(err => { console.error(err); process.exit(1); });
366
+ break;
367
+ case 'themes':
368
+ case 'theme':
369
+ listThemes();
370
+ break;
371
+ case 'list':
372
+ case 'segments':
373
+ listSegments().catch(err => { console.error(err); process.exit(1); });
374
+ break;
375
+ case 'create':
376
+ createPlugin(process.argv[3]);
377
+ break;
378
+ case 'uninstall':
379
+ case 'remove':
380
+ uninstall();
381
+ break;
382
+ case 'validate':
383
+ import(join(PACKAGE_ROOT, 'scripts', 'validate.js'));
384
+ break;
385
+ case '--help':
386
+ case '-h':
387
+ case 'help':
388
+ log(`\n${C.bold}oh-my-claude${C.reset} — The framework for Claude Code statuslines\n`);
389
+ log(`${C.bold}Usage:${C.reset} omc <command>\n`);
390
+ log(`${C.bold}Commands:${C.reset}`);
391
+ log(` ${C.cyan}install${C.reset} Install oh-my-claude (interactive wizard)`);
392
+ log(` ${C.cyan}themes${C.reset} List available themes`);
393
+ log(` ${C.cyan}list${C.reset} List all available segments`);
394
+ log(` ${C.cyan}create${C.reset} Create a new plugin segment (omc create <name>)`);
395
+ log(` ${C.cyan}validate${C.reset} Run the segment contract validator`);
396
+ log(` ${C.cyan}uninstall${C.reset} Remove oh-my-claude from Claude Code`);
397
+ log(` ${C.cyan}help${C.reset} Show this help message\n`);
398
+ break;
399
+ default:
400
+ warn(`Unknown command: ${command}`);
401
+ log(`Run ${C.cyan}omc help${C.reset} for available commands.`);
402
+ process.exit(1);
403
+ }
@@ -0,0 +1,198 @@
1
+ # Architecture
2
+
3
+ How oh-my-claude transforms Claude Code's JSON status data into a styled, multi-line statusline.
4
+
5
+ ## Pipeline
6
+
7
+ ```
8
+ Claude Code (stdin JSON)
9
+ |
10
+ v
11
+ runner.js Parse stdin, orchestrate pipeline
12
+ |
13
+ v
14
+ config.js Load theme + user overrides, resolve per-segment config
15
+ |
16
+ v
17
+ segments/*.js Each segment's render(data, config) called
18
+ |
19
+ v
20
+ compositor.js Arrange segments into lines with left/right alignment
21
+ |
22
+ v
23
+ color.js Apply ANSI escape codes from style strings
24
+ |
25
+ v
26
+ stdout Final styled string printed to terminal
27
+ ```
28
+
29
+ ## runner.js
30
+
31
+ The entry point. Responsibilities:
32
+
33
+ 1. Read stdin to completion (Claude Code pipes a single JSON blob).
34
+ 2. Parse JSON. If parsing fails, output an empty line and exit.
35
+ 3. Call `config.js` to resolve the active theme and user overrides.
36
+ 4. Discover all segment modules in `src/segments/`.
37
+ 5. Check each segment's `meta.requires` against available system commands. Skip segments with missing dependencies.
38
+ 6. For each line defined in the theme, call each segment's `render(data, config)`.
39
+ 7. Pass rendered segments to `compositor.js` to produce final output.
40
+ 8. Write to stdout.
41
+
42
+ All segment render calls are synchronous. The statusline must produce output quickly and exit.
43
+
44
+ ## config.js
45
+
46
+ Resolves configuration through a three-layer stack:
47
+
48
+ ```
49
+ Layer 1 (lowest priority): themes/default.json
50
+ Layer 2: themes/<active-theme>.json
51
+ Layer 3 (highest priority): ~/.claude/oh-my-claude/config.json
52
+ ```
53
+
54
+ The user config file at `~/.claude/oh-my-claude/config.json` can specify:
55
+
56
+ ```json
57
+ {
58
+ "theme": "minimal",
59
+ "segments": {
60
+ "cost": { "precision": 4, "warnAt": 10.0 }
61
+ }
62
+ }
63
+ ```
64
+
65
+ Config resolution for a segment:
66
+
67
+ 1. Start with the segment's `meta.defaultConfig`.
68
+ 2. Merge the theme's `segments["name"]` on top.
69
+ 3. Merge the user's `segments["name"]` on top.
70
+ 4. Pass the result as `config` to `render()`.
71
+
72
+ Merging is shallow `Object.assign` -- not deep merge. Keep segment configs flat.
73
+
74
+ ## compositor.js
75
+
76
+ Takes an array of line definitions (each with rendered left and right segments) and produces the final output string.
77
+
78
+ ### Alignment
79
+
80
+ Each line has a `left` array and a `right` array of rendered segments. The compositor:
81
+
82
+ 1. Joins left segments with the theme's separator (default: `" "`).
83
+ 2. Joins right segments with the separator.
84
+ 3. Detects terminal width from `process.stdout.columns` (falls back to 80).
85
+ 4. Pads the space between left and right groups to fill the terminal width.
86
+ 5. If the combined width exceeds the terminal, truncates the right group first, then the left group. Truncation adds `...` as an indicator.
87
+
88
+ ### Multi-line Output
89
+
90
+ Themes can define multiple lines. The compositor outputs each line separated by `\n`. Claude Code's statusline feature supports multi-line output.
91
+
92
+ ### Separator
93
+
94
+ The theme's `separator` field controls the string placed between adjacent segments on the same side. Default is a single space. Themes can use Unicode characters like `|`, `//`, or powerline glyphs.
95
+
96
+ ## color.js
97
+
98
+ Parses style strings into ANSI escape sequences.
99
+
100
+ Input: A space-separated string like `"bold cyan"` or `"bg:red white"`.
101
+
102
+ Output: An object with `open` and `close` ANSI strings that wrap the segment text.
103
+
104
+ ### Parsing Rules
105
+
106
+ 1. Split style string on spaces.
107
+ 2. For each token:
108
+ - Modifiers (`bold`, `dim`, `italic`, `underline`, `inverse`, `hidden`, `strikethrough`): map to ANSI SGR codes.
109
+ - Foreground colors (`black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`): map to ANSI 30-37.
110
+ - Background colors (`bg:black`, `bg:red`, etc.): map to ANSI 40-47.
111
+ 3. Combine all codes into a single `\x1b[...m` open sequence.
112
+ 4. Close sequence is always `\x1b[0m` (reset).
113
+
114
+ No 256-color or truecolor support in v1. Standard 8 colors cover the common case.
115
+
116
+ ## cache.js
117
+
118
+ Provides a TTL-based cache for shell command output. Segments that run external commands (git, system utilities) MUST use this to avoid re-executing commands on every statusline update.
119
+
120
+ ### API
121
+
122
+ ```js
123
+ import { cachedExec } from "../cache.js";
124
+
125
+ // Returns stdout as a string, or null if the command fails
126
+ const result = cachedExec(key, command, ttlMs);
127
+ ```
128
+
129
+ | Parameter | Type | Description |
130
+ |-----------|------|-------------|
131
+ | `key` | `string` | Cache key. Use a descriptive name like `"git-branch"`. |
132
+ | `command` | `string` | Shell command to execute. |
133
+ | `ttlMs` | `number` | Time-to-live in milliseconds. Cached result is reused within this window. |
134
+
135
+ ### Implementation Details
136
+
137
+ - Uses `child_process.execSync` with a timeout (default: 2000ms).
138
+ - Cache storage: files in `/tmp/omc-cache/`. Each key maps to a file containing `{ timestamp, stdout }` as JSON.
139
+ - On cache hit (file exists and age < TTL): returns cached stdout without executing.
140
+ - On cache miss or expired: executes command, writes result, returns stdout.
141
+ - On command failure (non-zero exit, timeout): returns `null`. Does not cache failures.
142
+ - File-based cache survives across statusline invocations within the same shell session.
143
+
144
+ ### Why File-Based
145
+
146
+ The statusline runs as a short-lived process on every update. In-memory caches die with the process. File-based caching in `/tmp` persists across invocations and is automatically cleaned by the OS.
147
+
148
+ ## External Plugins
149
+
150
+ Users can add custom segments without modifying the framework.
151
+
152
+ ### Location
153
+
154
+ ```
155
+ ~/.claude/oh-my-claude/plugins/<name>/segment.sh
156
+ ```
157
+
158
+ ### Contract
159
+
160
+ - The script receives the full stdin JSON on stdin.
161
+ - It must output exactly one line to stdout: the segment text.
162
+ - Exit code 0: segment is shown. Non-zero: segment is hidden.
163
+ - The theme references external plugins by name (e.g., `"my-plugin"`).
164
+ - Styling for external plugins is configured in the theme's `segments` block:
165
+
166
+ ```json
167
+ {
168
+ "segments": {
169
+ "my-plugin": { "style": "bold cyan" }
170
+ }
171
+ }
172
+ ```
173
+
174
+ External plugins do not support the `meta` object. The framework assigns reasonable defaults (no requirements, no default config).
175
+
176
+ ### Performance Note
177
+
178
+ External plugins fork a child process on every invocation. They are inherently slower than built-in JS segments. The framework enforces a 2-second timeout on plugin execution.
179
+
180
+ ## Dependency Checking
181
+
182
+ When a segment declares `meta.requires = ["git"]`, the framework checks at startup:
183
+
184
+ 1. Run `which <dep>` for each dependency.
185
+ 2. Cache the result for the lifetime of the process.
186
+ 3. If any dependency is missing, skip the segment entirely (treat as null).
187
+
188
+ This prevents error spam from segments that depend on tools not installed on the user's system.
189
+
190
+ ## Error Isolation
191
+
192
+ Every segment render call is wrapped in a try/catch. If a segment throws:
193
+
194
+ 1. The exception is caught.
195
+ 2. The segment is treated as returning `null` (hidden).
196
+ 3. No error output contaminates stdout. The statusline is a display-only surface.
197
+
198
+ This guarantee means a single broken segment can never take down the entire statusline.