@alyibrahim/claude-statusline 1.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/README.md +57 -0
- package/bin/cli.js +41 -0
- package/package.json +17 -0
- package/scripts/config.js +26 -0
- package/scripts/postinstall.js +16 -0
- package/scripts/preuninstall.js +5 -0
- package/scripts/setup.js +47 -0
- package/scripts/uninstall.js +28 -0
- package/statusline.js +162 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @alyibrahim/claude-statusline
|
|
2
|
+
|
|
3
|
+
A zero-dependency statusline for [Claude Code](https://claude.ai/code). Shows model, git branch, context usage, subscription rate limits, and session cost — updating after every response.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- **Node.js >=16** — the only hard requirement (installed with npm)
|
|
8
|
+
- **git** — optional, used for branch display; gracefully absent if not installed
|
|
9
|
+
|
|
10
|
+
No `jq`, `bc`, `ccusage`, or other external tools needed.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g @alyibrahim/claude-statusline
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
That's it. The statusline is configured automatically. Restart Claude Code to see it.
|
|
19
|
+
|
|
20
|
+
**Manual setup** (if auto-setup failed):
|
|
21
|
+
```bash
|
|
22
|
+
claude-statusline setup
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## What it shows
|
|
26
|
+
|
|
27
|
+
**Line 1:** Model · Effort level · Active agents · Current task · Directory `[git branch]` · Context bar
|
|
28
|
+
|
|
29
|
+
**Line 2:** Weekly usage · 5h usage · Reset countdown *(subscription)* or Session cost *(API key)*
|
|
30
|
+
|
|
31
|
+
## Why this one
|
|
32
|
+
|
|
33
|
+
| | This package | Others |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| Zero dependencies | ✓ no `jq`, `bc`, etc. | Require external tools |
|
|
36
|
+
| No API calls | ✓ reads stdin directly | Poll OAuth endpoint, hit rate limits |
|
|
37
|
+
| Subscription vs API aware | ✓ | Show cost for everyone |
|
|
38
|
+
| Context bar normalized | ✓ usable % | Raw remaining % |
|
|
39
|
+
| Active agent counter | ✓ | — |
|
|
40
|
+
|
|
41
|
+
## Uninstall
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm uninstall -g @alyibrahim/claude-statusline
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> If using yarn/pnpm/bun, run `claude-statusline uninstall` **before** removing the package.
|
|
48
|
+
|
|
49
|
+
## Notes
|
|
50
|
+
|
|
51
|
+
- **Switched Node versions?** Re-run `claude-statusline setup` — the Node path is baked in at install time.
|
|
52
|
+
- Writes only the `statusLine` key in `~/.claude/settings.json` — all other settings are preserved.
|
|
53
|
+
- Respects `$CLAUDE_CONFIG_DIR` if set.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const { setup } = require('../scripts/setup');
|
|
4
|
+
const { uninstall } = require('../scripts/uninstall');
|
|
5
|
+
const { getSettingsPath } = require('../scripts/config');
|
|
6
|
+
|
|
7
|
+
const USAGE = `
|
|
8
|
+
claude-statusline <command>
|
|
9
|
+
|
|
10
|
+
Commands:
|
|
11
|
+
setup Configure ~/.claude/settings.json to use this statusline
|
|
12
|
+
uninstall Remove this statusline from ~/.claude/settings.json
|
|
13
|
+
`.trim();
|
|
14
|
+
|
|
15
|
+
const cmd = process.argv[2];
|
|
16
|
+
|
|
17
|
+
if (cmd === 'setup') {
|
|
18
|
+
const result = setup({ force: true });
|
|
19
|
+
if (!result.ok) {
|
|
20
|
+
console.error('Error:', result.error);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
console.log(`✓ Configured at ${result.settingsPath}. Restart Claude Code to see it.`);
|
|
24
|
+
|
|
25
|
+
} else if (cmd === 'uninstall') {
|
|
26
|
+
const result = uninstall();
|
|
27
|
+
if (!result.ok) {
|
|
28
|
+
console.error('Error:', result.error);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
console.log(`✓ Removed statusline from ${getSettingsPath()}`);
|
|
32
|
+
|
|
33
|
+
} else if (cmd === undefined) {
|
|
34
|
+
console.log(USAGE);
|
|
35
|
+
process.exit(0);
|
|
36
|
+
|
|
37
|
+
} else {
|
|
38
|
+
console.error(`Unknown command: ${cmd}`);
|
|
39
|
+
console.log('\n' + USAGE);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alyibrahim/claude-statusline",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A zero-dependency Claude Code statusline — reads rate limits from stdin, no API calls",
|
|
5
|
+
"engines": { "node": ">=16" },
|
|
6
|
+
"bin": { "claude-statusline": "bin/cli.js" },
|
|
7
|
+
"scripts": {
|
|
8
|
+
"postinstall": "node scripts/postinstall.js",
|
|
9
|
+
"preuninstall": "node scripts/preuninstall.js",
|
|
10
|
+
"test": "jest"
|
|
11
|
+
},
|
|
12
|
+
"files": ["statusline.js", "bin/", "scripts/", "README.md"],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"jest": "^29.0.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
function getSettingsPath() {
|
|
7
|
+
const configDir = process.env.CLAUDE_CONFIG_DIR;
|
|
8
|
+
const base = (configDir && configDir.trim())
|
|
9
|
+
? configDir
|
|
10
|
+
: path.join(os.homedir(), '.claude');
|
|
11
|
+
return path.join(base, 'settings.json');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function atomicWrite(filePath, obj) {
|
|
15
|
+
const tmpPath = filePath + '.tmp';
|
|
16
|
+
if (fs.existsSync(tmpPath)) fs.unlinkSync(tmpPath);
|
|
17
|
+
fs.writeFileSync(tmpPath, JSON.stringify(obj, null, 2));
|
|
18
|
+
try {
|
|
19
|
+
fs.renameSync(tmpPath, filePath);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
try { fs.unlinkSync(tmpPath); } catch (e) {}
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { getSettingsPath, atomicWrite };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const { setup } = require('./setup');
|
|
4
|
+
try {
|
|
5
|
+
const result = setup({ force: false });
|
|
6
|
+
if (result.settingsPath === null) process.exit(0); // non-global install, skip silently
|
|
7
|
+
if (!result.ok) {
|
|
8
|
+
console.warn('\n⚠ claude-statusline: auto-setup failed:', result.error);
|
|
9
|
+
console.warn(' Run manually: claude-statusline setup\n');
|
|
10
|
+
} else {
|
|
11
|
+
console.log('\n✓ claude-statusline configured. Restart Claude Code to see it.\n');
|
|
12
|
+
}
|
|
13
|
+
} catch (e) {
|
|
14
|
+
// Fully silent on any error — postinstall must never fail npm install
|
|
15
|
+
}
|
|
16
|
+
process.exit(0); // always exit 0 — never fail npm install
|
package/scripts/setup.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { getSettingsPath, atomicWrite } = require('./config');
|
|
5
|
+
|
|
6
|
+
const UNSAFE_CHARS = /["`$!()\\]/;
|
|
7
|
+
|
|
8
|
+
function setup({ force = false } = {}) {
|
|
9
|
+
// CI guard: skip during local/CI npm installs unless forced (e.g. from CLI)
|
|
10
|
+
if (!force && process.env.npm_config_global !== 'true') {
|
|
11
|
+
return { ok: true, settingsPath: null };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const scriptPath = path.resolve(__dirname, '../statusline.js');
|
|
15
|
+
if (!fs.existsSync(scriptPath)) {
|
|
16
|
+
return { ok: false, error: `Could not locate statusline.js at ${scriptPath}` };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (UNSAFE_CHARS.test(process.execPath) || UNSAFE_CHARS.test(scriptPath)) {
|
|
20
|
+
return { ok: false, error: 'Node.js path or install path contains unsupported characters.' };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const settingsPath = getSettingsPath();
|
|
24
|
+
let settings = {};
|
|
25
|
+
if (fs.existsSync(settingsPath)) {
|
|
26
|
+
try {
|
|
27
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return { ok: false, error: 'settings.json contains invalid JSON — fix manually then re-run.' };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const command = `"${process.execPath}" "${scriptPath}"`;
|
|
34
|
+
settings.statusLine = { type: 'command', command };
|
|
35
|
+
|
|
36
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
atomicWrite(settingsPath, settings);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return { ok: false, error: err.message };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { ok: true, settingsPath };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { setup };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { getSettingsPath, atomicWrite } = require('./config');
|
|
4
|
+
|
|
5
|
+
function uninstall() {
|
|
6
|
+
const settingsPath = getSettingsPath();
|
|
7
|
+
if (!fs.existsSync(settingsPath)) return { ok: true };
|
|
8
|
+
|
|
9
|
+
let settings;
|
|
10
|
+
try {
|
|
11
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
12
|
+
} catch (e) {
|
|
13
|
+
return { ok: false, error: 'settings.json contains invalid JSON — cannot safely modify.' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!settings.statusLine) return { ok: true };
|
|
17
|
+
delete settings.statusLine;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
atomicWrite(settingsPath, settings);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
return { ok: false, error: err.message };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return { ok: true };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { uninstall };
|
package/statusline.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code Statusline
|
|
3
|
+
// Shows: model | current task | directory | context usage
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
// Read JSON from stdin
|
|
11
|
+
let input = '';
|
|
12
|
+
// Timeout guard: if stdin doesn't close within 3s (e.g. pipe issues on
|
|
13
|
+
// Windows/Git Bash), exit silently instead of hanging.
|
|
14
|
+
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
|
15
|
+
process.stdin.setEncoding('utf8');
|
|
16
|
+
process.stdin.on('data', chunk => input += chunk);
|
|
17
|
+
process.stdin.on('end', () => {
|
|
18
|
+
clearTimeout(stdinTimeout);
|
|
19
|
+
try {
|
|
20
|
+
const sanitize = s => String(s).replace(/\x1b\[[0-9;]*[mGKHFABCDJ]/g, '');
|
|
21
|
+
const data = JSON.parse(input);
|
|
22
|
+
const model = sanitize(data.model?.display_name || 'Claude');
|
|
23
|
+
const dir = data.workspace?.current_dir || process.cwd();
|
|
24
|
+
const session = data.session_id || '';
|
|
25
|
+
const remaining = data.context_window?.remaining_percentage;
|
|
26
|
+
|
|
27
|
+
// Context window display (shows USED percentage scaled to usable context)
|
|
28
|
+
// Claude Code reserves ~16.5% for autocompact buffer, so usable context
|
|
29
|
+
// is 83.5% of the total window. We normalize to show 100% at that point.
|
|
30
|
+
const AUTO_COMPACT_BUFFER_PCT = 16.5;
|
|
31
|
+
let ctx = '';
|
|
32
|
+
if (remaining != null) {
|
|
33
|
+
// Normalize: subtract buffer from remaining, scale to usable range
|
|
34
|
+
const usableRemaining = Math.max(0, ((remaining - AUTO_COMPACT_BUFFER_PCT) / (100 - AUTO_COMPACT_BUFFER_PCT)) * 100);
|
|
35
|
+
const used = Math.max(0, Math.min(100, Math.round(100 - usableRemaining)));
|
|
36
|
+
|
|
37
|
+
// Build progress bar (10 segments)
|
|
38
|
+
const filled = Math.floor(used / 10);
|
|
39
|
+
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
|
|
40
|
+
|
|
41
|
+
// Color based on usable context thresholds
|
|
42
|
+
if (used < 50) {
|
|
43
|
+
ctx = ` \x1b[32m${bar} ${used}%\x1b[0m`;
|
|
44
|
+
} else if (used < 65) {
|
|
45
|
+
ctx = ` \x1b[33m${bar} ${used}%\x1b[0m`;
|
|
46
|
+
} else if (used < 80) {
|
|
47
|
+
ctx = ` \x1b[38;5;208m${bar} ${used}%\x1b[0m`;
|
|
48
|
+
} else {
|
|
49
|
+
ctx = ` \x1b[5;31m💀 ${bar} ${used}%\x1b[0m`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Current task from todos
|
|
54
|
+
let task = '';
|
|
55
|
+
let activeAgents = 0;
|
|
56
|
+
const homeDir = os.homedir();
|
|
57
|
+
const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(homeDir, '.claude');
|
|
58
|
+
const todosDir = path.join(claudeDir, 'todos');
|
|
59
|
+
if (session && fs.existsSync(todosDir)) {
|
|
60
|
+
try {
|
|
61
|
+
const files = fs.readdirSync(todosDir)
|
|
62
|
+
.filter(f => f.startsWith(session) && f.includes('-agent-') && f.endsWith('.json'))
|
|
63
|
+
.map(f => ({ name: f, mtime: fs.statSync(path.join(todosDir, f)).mtime }))
|
|
64
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
65
|
+
|
|
66
|
+
for (const f of files) {
|
|
67
|
+
try {
|
|
68
|
+
const todos = JSON.parse(fs.readFileSync(path.join(todosDir, f.name), 'utf8'));
|
|
69
|
+
const inProgress = todos.find(t => t.status === 'in_progress');
|
|
70
|
+
if (inProgress) {
|
|
71
|
+
activeAgents++;
|
|
72
|
+
if (!task) task = sanitize(inProgress.activeForm || '');
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {}
|
|
75
|
+
}
|
|
76
|
+
} catch (e) {
|
|
77
|
+
// Silently fail on file system errors - don't break statusline
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Session cost — only show for API key users; rate_limits presence means subscription
|
|
82
|
+
const isSubscription = data.rate_limits !== undefined;
|
|
83
|
+
const sessionCost = !isSubscription ? (data.cost?.total_cost_usd ?? null) : null;
|
|
84
|
+
|
|
85
|
+
// Usage limits — provided by Claude Code in stdin; no API call needed
|
|
86
|
+
const pct5h = data.rate_limits?.five_hour?.used_percentage ?? null;
|
|
87
|
+
const pctWeek = data.rate_limits?.seven_day?.used_percentage ?? null;
|
|
88
|
+
const resetsAt5h = data.rate_limits?.five_hour?.resets_at ?? null; // Unix epoch seconds
|
|
89
|
+
|
|
90
|
+
function usageLine(label, pct, suffix = '') {
|
|
91
|
+
if (pct === null) return '';
|
|
92
|
+
const p = Math.round(pct);
|
|
93
|
+
const color = p < 50 ? '\x1b[32m' : p < 75 ? '\x1b[33m' : '\x1b[31m';
|
|
94
|
+
return `\x1b[0m\x1b[97m${label}:\x1b[0m ${color}${p}%\x1b[0m${suffix}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let resetSuffix = '';
|
|
98
|
+
if (resetsAt5h) {
|
|
99
|
+
const resetDate = new Date(resetsAt5h * 1000); // stdin gives epoch seconds
|
|
100
|
+
if (!isNaN(resetDate)) {
|
|
101
|
+
const minsLeft = Math.max(0, Math.round((resetDate - Date.now()) / 60_000));
|
|
102
|
+
const h = Math.floor(minsLeft / 60), m = minsLeft % 60;
|
|
103
|
+
resetSuffix = ` \x1b[2m↺ ${h}h${String(m).padStart(2, '0')}m\x1b[0m`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Git branch (command is a fixed string; dir is passed as cwd, not interpolated — no injection risk)
|
|
108
|
+
let branch = '';
|
|
109
|
+
try {
|
|
110
|
+
branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: dir, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
111
|
+
} catch (e) {}
|
|
112
|
+
|
|
113
|
+
// Output
|
|
114
|
+
const dirname = sanitize(path.basename(dir));
|
|
115
|
+
const safeBranch = sanitize(branch);
|
|
116
|
+
// dirname is bright; branch stays cyan; surrounding prefix/suffix stay dim
|
|
117
|
+
const dirDisplay = safeBranch
|
|
118
|
+
? `\x1b[1m\x1b[97m${dirname}\x1b[0m\x1b[2m \x1b[36m[${safeBranch}]\x1b[0m`
|
|
119
|
+
: `\x1b[1m\x1b[97m${dirname}\x1b[0m`;
|
|
120
|
+
const u5h = usageLine('Current', pct5h, resetSuffix), u7d = usageLine('Weekly', pctWeek);
|
|
121
|
+
const costDisplay = sessionCost !== null
|
|
122
|
+
? ` \x1b[33m$${sessionCost < 0.01 ? sessionCost.toFixed(4) : sessionCost.toFixed(2)}\x1b[0m`
|
|
123
|
+
: '';
|
|
124
|
+
const usageContent = [u7d, u5h].filter(Boolean).join(' ');
|
|
125
|
+
const line2 = (usageContent || costDisplay)
|
|
126
|
+
? `\x1b[0m\x1b[32mUsage\x1b[0m \x1b[2m│\x1b[0m ${[usageContent, costDisplay].filter(Boolean).join(' ')}`
|
|
127
|
+
: '';
|
|
128
|
+
// Effort level: read from env var first, then settings.json, then fall back to
|
|
129
|
+
// model-based default (sonnet-4/opus-4 default to "medium" in Claude Code).
|
|
130
|
+
const effortLetters = { low: 'L', medium: 'M', high: 'H' };
|
|
131
|
+
let effortSuffix = '';
|
|
132
|
+
try {
|
|
133
|
+
let rawEffort = process.env.CLAUDE_CODE_EFFORT_LEVEL
|
|
134
|
+
|| JSON.parse(fs.readFileSync(path.join(claudeDir, 'settings.json'), 'utf8'))?.effortLevel
|
|
135
|
+
|| '';
|
|
136
|
+
if (!rawEffort) {
|
|
137
|
+
const m = model.toLowerCase();
|
|
138
|
+
if (m.includes('sonnet-4') || m.includes('opus-4')) rawEffort = 'medium';
|
|
139
|
+
}
|
|
140
|
+
const effortColors = { low: '\x1b[32m', medium: '\x1b[33m', high: '\x1b[38;5;208m', max: '\x1b[31m' };
|
|
141
|
+
const level = rawEffort?.toLowerCase();
|
|
142
|
+
const color = effortColors[level] || '';
|
|
143
|
+
if (level === 'max') {
|
|
144
|
+
effortSuffix = ` \x1b[0m${color}[MAXX]\x1b[0m`;
|
|
145
|
+
} else {
|
|
146
|
+
const letter = effortLetters[level];
|
|
147
|
+
if (letter) effortSuffix = ` \x1b[0m${color}[${letter}]\x1b[0m`;
|
|
148
|
+
}
|
|
149
|
+
} catch (e) {}
|
|
150
|
+
|
|
151
|
+
const agentDisplay = activeAgents > 0 ? ` \x1b[0m\x1b[36m↪ ${activeAgents}\x1b[0m` : '';
|
|
152
|
+
const modelDisplay = `\x1b[0m\x1b[94m${model}\x1b[0m` + effortSuffix + agentDisplay;
|
|
153
|
+
const line1 = task
|
|
154
|
+
? `${modelDisplay} \x1b[2m│\x1b[0m \x1b[1m${task}\x1b[0m \x1b[2m│\x1b[0m ${dirDisplay}${ctx}`
|
|
155
|
+
: `${modelDisplay} \x1b[2m│\x1b[0m ${dirDisplay}${ctx}`;
|
|
156
|
+
const visibleLen = line1.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
157
|
+
const sep = `\x1b[2m${'─'.repeat(visibleLen)}\x1b[0m`;
|
|
158
|
+
process.stdout.write(line2 ? `${line1}\n${sep}\n${line2}` : line1);
|
|
159
|
+
} catch (e) {
|
|
160
|
+
// Silent fail - don't break statusline on parse errors
|
|
161
|
+
}
|
|
162
|
+
});
|