ccigarette 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.
- package/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/app-N2XCI26M.js +140 -0
- package/dist/bin.js +133 -0
- package/dist/chunk-GN3HBMWB.js +383 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ivan Matveev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# 🚬 ccigarette
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/ccigarette)
|
|
4
|
+
[](#license)
|
|
5
|
+
[](#requirements)
|
|
6
|
+
|
|
7
|
+
> Smoking kills. Your context window.
|
|
8
|
+
|
|
9
|
+
A smouldering pixel cigarette that replaces your Claude Code status line. The cigarette **is** your context window: as the context fills up, the ember creeps toward the filter. Smoke curls on its own line above the ember while the agent is working. Burned down to the filter? Time to `/compact`.
|
|
10
|
+
|
|
11
|
+
<!-- Live status line: see docs/demo.tape (VHS) to regenerate. -->
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
The top line is smoke — a wisp `~` is born above the ember and, cooling into `°`, drifts toward the burned side, frame by frame. The burned tobacco settles as `▁` ash, so the cigarette keeps its full original length and you always see how much you've smoked.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- 🔥 **Burns with your context** — `context_window.used_percentage` drives the ember toward the filter. Reach the filter, reach for `/compact`.
|
|
19
|
+
- 💨 **Live smoke & flicker** while the agent is working; a steady ember when idle. The animation is fully stateless — derived from the wall clock, with zero state kept between invocations.
|
|
20
|
+
- 🚬 **Six brands**, each with its own filter, colors, and burnable length. Lucky Strike burns the longest; Беломорканал burns out the fastest.
|
|
21
|
+
- 🎛 **Interactive TUI configurator** with a live animated preview — `npx ccigarette`, pick a brand, install in one keystroke.
|
|
22
|
+
- 🎨 **Truecolor** with an automatic 256-color downsample, a mirrorable orientation, and an optional dark backdrop for light terminals.
|
|
23
|
+
- 🛟 **Never crashes the status line** — broken JSON renders an unlit cigarette; any render error prints a bare `🚬` and exits `0`.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx ccigarette
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This opens a terminal configurator: pick your brand (live animated preview), flip the orientation, toggle the label/percent/model, then hit **Install** — it writes the `statusLine` block into `~/.claude/settings.json` (honoring `CLAUDE_CONFIG_DIR`) and you're smoking.
|
|
32
|
+
|
|
33
|
+
Manual install, if you prefer editing settings yourself:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"statusLine": {
|
|
38
|
+
"type": "command",
|
|
39
|
+
"command": "npx -y ccigarette@0.1.0",
|
|
40
|
+
"padding": 0,
|
|
41
|
+
"refreshInterval": 1
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The installed command pins the version you set up: it runs from the npx cache (works offline after the first run) and never auto-updates. To upgrade, run `npx ccigarette` again. Prefer scripts? `npx ccigarette install` / `npx ccigarette uninstall` do the same non-interactively.
|
|
47
|
+
|
|
48
|
+
## Requirements
|
|
49
|
+
|
|
50
|
+
- **[Claude Code](https://claude.com/claude-code)** — `ccigarette` is a `statusLine` provider.
|
|
51
|
+
- **Node.js ≥ 22**
|
|
52
|
+
- A terminal with Unicode glyph support (any modern terminal). Truecolor is recommended; a 256-color fallback is included.
|
|
53
|
+
|
|
54
|
+
## Brands
|
|
55
|
+
|
|
56
|
+
Each brand has its own filter, colors, and — importantly — its own burnable length. Your context feels different depending on what you smoke.
|
|
57
|
+
|
|
58
|
+
| Brand | Character |
|
|
59
|
+
|---|---|
|
|
60
|
+
| **Marlboro** | Cork filter with speckle, red band. The classic. |
|
|
61
|
+
| **KENT** | White filter, blue band. Strict. |
|
|
62
|
+
| **Camel** | Ochre, double-speckled cork. |
|
|
63
|
+
| **Parliament** | Recessed filter — you can see the hollow. |
|
|
64
|
+
| **LUCKY STRIKE** | No filter, red ring at the butt. Longest burn of the pack. |
|
|
65
|
+
| **Беломорканал (Belomorkanal)** | A papirosa. Hollow paper mouthpiece, barely any tobacco — burns out fast. Honest. |
|
|
66
|
+
|
|
67
|
+
## How it works
|
|
68
|
+
|
|
69
|
+
- Claude Code pipes status JSON to the command every second (`refreshInterval: 1`); burn progress comes from `context_window.used_percentage`.
|
|
70
|
+
- Animation is stateless: the frame is derived from the wall clock, so smoke sways and the ember flickers with zero state between invocations.
|
|
71
|
+
- Smoke and flicker only happen while the agent is actually working (the transcript file's mtime is under 10 s old). An idle cigarette rests with a steady ember. No `transcript_path` → treated as active.
|
|
72
|
+
- At 0% the cigarette is unlit. Past `warnAt` (default 90%) the percent turns red. After `/compact` the percentage drops — fresh cigarette, on the house.
|
|
73
|
+
- The status line **never crashes**: broken JSON renders an unlit cigarette; a render error prints a bare `🚬` and exits `0`.
|
|
74
|
+
|
|
75
|
+
## Config
|
|
76
|
+
|
|
77
|
+
Lives at `~/.config/ccigarette/config.json` (respects `XDG_CONFIG_HOME`). Managed by the TUI, editable by hand:
|
|
78
|
+
|
|
79
|
+
| Field | Default | Meaning |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| `brand` | `"marlboro"` | `marlboro` · `kent` · `camel` · `parliament` · `lucky` · `belomor` |
|
|
82
|
+
| `direction` | `"right"` | `right` = filter on the right, burns leftward; `left` = mirrored |
|
|
83
|
+
| `showLabel` | `true` | Brand name after the art |
|
|
84
|
+
| `showPercent` | `true` | Context percentage |
|
|
85
|
+
| `showModel` | `true` | Model display name |
|
|
86
|
+
| `smoke` | `true` | Smoke wisp drifting above the ember (one line) |
|
|
87
|
+
| `backdrop` | `false` | Render a dark card behind the art — readable on light terminals, matches the mockup |
|
|
88
|
+
| `warnAt` | `90` | Percent turns red at this threshold |
|
|
89
|
+
| `colorMode` | `"truecolor"` | `truecolor` or `ansi256` (nearest-color downsample) |
|
|
90
|
+
|
|
91
|
+
## Беломорканал
|
|
92
|
+
|
|
93
|
+
Отдельное слово про Беломор. Это папироса, а не сигарета: длинная полая бумажная гильза-мундштук `▐══════▌`, которая не сгорает никогда, и совсем немного табака. Поэтому у Беломора самая короткая сгораемая часть в паке — контекст выгорает стремительно, зато мундштук вечен. Мы считаем, что это самая честная метафора работы с большим контекстом из всех доступных.
|
|
94
|
+
|
|
95
|
+
## Uninstall
|
|
96
|
+
|
|
97
|
+
Run `npx ccigarette`, go to the Install screen, hit Enter. It removes only its own `statusLine` block and never touches the rest of your settings. If ccigarette replaced another statusLine on install, uninstall puts the original back. Non-interactive: `npx ccigarette uninstall`.
|
|
98
|
+
|
|
99
|
+
## Contributing
|
|
100
|
+
|
|
101
|
+
PRs are welcome — it's a small, well-tested codebase. MIT-licensed.
|
|
102
|
+
|
|
103
|
+
### Develop
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git clone https://github.com/blackbalancef/ccigarette.git
|
|
107
|
+
cd ccigarette
|
|
108
|
+
npm install
|
|
109
|
+
npm run build # tsup → dist/
|
|
110
|
+
npm test # vitest
|
|
111
|
+
npm run typecheck # tsc --noEmit
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Preview every brand at several burn stages:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
node design/preview.mjs # all brands, 0–100%
|
|
118
|
+
node design/preview.mjs --animate marlboro # live burn animation
|
|
119
|
+
node design/preview.mjs --flip # filter on the left
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Dry-run the status line exactly like Claude Code calls it:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
echo '{"context_window":{"used_percentage":40},"model":{"display_name":"Sonnet 4.5"}}' | node dist/bin.js
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Disclaimer
|
|
129
|
+
|
|
130
|
+
`ccigarette` is a joke — a pixel cigarette that visualizes your context window. It is **not advertising** and does **not promote, endorse, or encourage** tobacco use. Smoking is a leading cause of preventable death; if you smoke, please consider quitting — let the agent smoke instead. (See [WHO — tobacco](https://www.who.int/health-topics/tobacco).)
|
|
131
|
+
|
|
132
|
+
The brand names and colors shown (Marlboro, KENT, Camel, Parliament, Lucky Strike, Беломорканал) are trademarks of their respective owners, used here only as cultural references and satire. This project is not affiliated with, sponsored by, or endorsed by any of them.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT. Smoking (this status line) is encouraged; the real thing kills you *and* doesn't show context usage.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
BRANDS,
|
|
4
|
+
BRAND_ORDER,
|
|
5
|
+
hasForeignStatusLine,
|
|
6
|
+
install,
|
|
7
|
+
isInstalled,
|
|
8
|
+
loadConfig,
|
|
9
|
+
renderCigarette,
|
|
10
|
+
saveConfig,
|
|
11
|
+
uninstall
|
|
12
|
+
} from "./chunk-GN3HBMWB.js";
|
|
13
|
+
|
|
14
|
+
// src/tui/app.tsx
|
|
15
|
+
import { render } from "ink";
|
|
16
|
+
|
|
17
|
+
// src/tui/ui.tsx
|
|
18
|
+
import { useState, useEffect } from "react";
|
|
19
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
20
|
+
|
|
21
|
+
// src/tui/preview.ts
|
|
22
|
+
var PREVIEW_PCT = 45;
|
|
23
|
+
function previewLines(config, frame) {
|
|
24
|
+
return renderCigarette({ config, pct: PREVIEW_PCT, frame, active: true, model: "Fable 5" });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/tui/ui.tsx
|
|
28
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
29
|
+
function App() {
|
|
30
|
+
const { exit } = useApp();
|
|
31
|
+
const [config, setConfig] = useState(() => loadConfig());
|
|
32
|
+
const [screen, setScreen] = useState("brand");
|
|
33
|
+
const [frame, setFrame] = useState(0);
|
|
34
|
+
const [cursor, setCursor] = useState(0);
|
|
35
|
+
const [installed, setInstalled] = useState(() => isInstalled());
|
|
36
|
+
const [foreign, setForeign] = useState(() => hasForeignStatusLine());
|
|
37
|
+
const [error, setError] = useState(null);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const t = setInterval(() => setFrame((f) => f + 1), 400);
|
|
40
|
+
return () => clearInterval(t);
|
|
41
|
+
}, []);
|
|
42
|
+
const update = (patch) => {
|
|
43
|
+
const next = { ...config, ...patch };
|
|
44
|
+
setConfig(next);
|
|
45
|
+
saveConfig(next);
|
|
46
|
+
};
|
|
47
|
+
const brandIdx = BRAND_ORDER.indexOf(config.brand);
|
|
48
|
+
const toggles = [
|
|
49
|
+
{ key: "direction", label: "Orientation (right/left)" },
|
|
50
|
+
{ key: "showLabel", label: "Brand label" },
|
|
51
|
+
{ key: "showPercent", label: "Percent" },
|
|
52
|
+
{ key: "showModel", label: "Model" },
|
|
53
|
+
{ key: "smoke", label: "Smoke" },
|
|
54
|
+
{ key: "colorMode", label: "Color (truecolor/ansi256)" },
|
|
55
|
+
{ key: "backdrop", label: "Backdrop (card background)" }
|
|
56
|
+
];
|
|
57
|
+
useInput((input, key) => {
|
|
58
|
+
if (input === "q") {
|
|
59
|
+
exit();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (key.tab) {
|
|
63
|
+
setScreen((s) => s === "brand" ? "settings" : s === "settings" ? "install" : "brand");
|
|
64
|
+
setCursor(0);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (screen === "brand") {
|
|
68
|
+
if (key.leftArrow || key.upArrow) update({ brand: BRAND_ORDER[(brandIdx + BRAND_ORDER.length - 1) % BRAND_ORDER.length] });
|
|
69
|
+
if (key.rightArrow || key.downArrow) update({ brand: BRAND_ORDER[(brandIdx + 1) % BRAND_ORDER.length] });
|
|
70
|
+
} else if (screen === "settings") {
|
|
71
|
+
if (key.upArrow) setCursor((c) => (c + toggles.length - 1) % toggles.length);
|
|
72
|
+
if (key.downArrow) setCursor((c) => (c + 1) % toggles.length);
|
|
73
|
+
if (key.return || input === " ") {
|
|
74
|
+
const t = toggles[cursor].key;
|
|
75
|
+
if (t === "direction") update({ direction: config.direction === "right" ? "left" : "right" });
|
|
76
|
+
else if (t === "colorMode") update({ colorMode: config.colorMode === "truecolor" ? "ansi256" : "truecolor" });
|
|
77
|
+
else update({ [t]: !config[t] });
|
|
78
|
+
}
|
|
79
|
+
} else if (screen === "install") {
|
|
80
|
+
if (key.return || input === " ") {
|
|
81
|
+
try {
|
|
82
|
+
if (installed) {
|
|
83
|
+
uninstall();
|
|
84
|
+
setInstalled(false);
|
|
85
|
+
} else {
|
|
86
|
+
install();
|
|
87
|
+
setInstalled(true);
|
|
88
|
+
setForeign(false);
|
|
89
|
+
}
|
|
90
|
+
setError(null);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
const { top, bottom } = previewLines(config, frame);
|
|
98
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
|
|
99
|
+
/* @__PURE__ */ jsx(Text, { bold: true, children: "\u{1F6AC} ccigarette" }),
|
|
100
|
+
/* @__PURE__ */ jsxs(Box, { marginY: 1, flexDirection: "column", children: [
|
|
101
|
+
/* @__PURE__ */ jsx(Text, { children: top }),
|
|
102
|
+
/* @__PURE__ */ jsx(Text, { children: bottom })
|
|
103
|
+
] }),
|
|
104
|
+
screen === "brand" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
105
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2190 \u2192 pick brand \xB7 Tab next \xB7 q quit" }),
|
|
106
|
+
/* @__PURE__ */ jsx(Text, { children: BRAND_ORDER.map((k) => k === config.brand ? `[${BRANDS[k].label}]` : ` ${BRANDS[k].label} `).join(" ") })
|
|
107
|
+
] }),
|
|
108
|
+
screen === "settings" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
109
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191 \u2193 select \xB7 Enter/Space toggle \xB7 Tab next \xB7 q quit" }),
|
|
110
|
+
toggles.map((t, i) => {
|
|
111
|
+
const v = config[t.key];
|
|
112
|
+
const shown = typeof v === "boolean" ? v ? "on" : "off" : String(v);
|
|
113
|
+
return /* @__PURE__ */ jsx(Text, { inverse: i === cursor, children: `${t.label}: ${shown}` }, t.key);
|
|
114
|
+
})
|
|
115
|
+
] }),
|
|
116
|
+
screen === "install" && /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
117
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
118
|
+
"Enter \u2014 ",
|
|
119
|
+
installed ? "remove from Claude Code" : "install into Claude Code",
|
|
120
|
+
" \xB7 Tab next \xB7 q quit"
|
|
121
|
+
] }),
|
|
122
|
+
/* @__PURE__ */ jsx(Text, { children: installed ? "\u2713 installed (statusLine \u2192 ccigarette)" : "\u2717 not installed" }),
|
|
123
|
+
!installed && foreign && /* @__PURE__ */ jsx(Text, { color: "yellow", children: "\u26A0 another statusLine already in settings.json \u2014 install will replace it" }),
|
|
124
|
+
error && /* @__PURE__ */ jsxs(Text, { color: "red", children: [
|
|
125
|
+
"\u2717 ",
|
|
126
|
+
error
|
|
127
|
+
] })
|
|
128
|
+
] })
|
|
129
|
+
] });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/tui/app.tsx
|
|
133
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
134
|
+
async function run() {
|
|
135
|
+
const instance = render(/* @__PURE__ */ jsx2(App, {}));
|
|
136
|
+
await instance.waitUntilExit();
|
|
137
|
+
}
|
|
138
|
+
export {
|
|
139
|
+
run
|
|
140
|
+
};
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
SettingsError,
|
|
4
|
+
VERSION,
|
|
5
|
+
install,
|
|
6
|
+
loadConfig,
|
|
7
|
+
renderCigarette,
|
|
8
|
+
settingsPath,
|
|
9
|
+
uninstall
|
|
10
|
+
} from "./chunk-GN3HBMWB.js";
|
|
11
|
+
|
|
12
|
+
// src/statusline.ts
|
|
13
|
+
import { statSync } from "fs";
|
|
14
|
+
var ACTIVE_WINDOW_MS = 1e4;
|
|
15
|
+
function parseStdin(raw) {
|
|
16
|
+
try {
|
|
17
|
+
const j = JSON.parse(raw);
|
|
18
|
+
const up = j?.context_window?.used_percentage;
|
|
19
|
+
const dn = j?.model?.display_name;
|
|
20
|
+
const tp = j?.transcript_path;
|
|
21
|
+
return {
|
|
22
|
+
usedPercentage: typeof up === "number" ? up : null,
|
|
23
|
+
modelDisplayName: typeof dn === "string" ? dn : null,
|
|
24
|
+
transcriptPath: typeof tp === "string" ? tp : null
|
|
25
|
+
};
|
|
26
|
+
} catch {
|
|
27
|
+
return { usedPercentage: null, modelDisplayName: null, transcriptPath: null };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function isActive(transcriptPath, now) {
|
|
31
|
+
if (!transcriptPath) return true;
|
|
32
|
+
try {
|
|
33
|
+
return now - statSync(transcriptPath).mtimeMs < ACTIVE_WINDOW_MS;
|
|
34
|
+
} catch {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function currentFrame(now) {
|
|
39
|
+
return Math.floor(now / 1e3) % 4;
|
|
40
|
+
}
|
|
41
|
+
function runStatusline(raw, now) {
|
|
42
|
+
try {
|
|
43
|
+
const input = parseStdin(raw);
|
|
44
|
+
const config = loadConfig();
|
|
45
|
+
const { top, bottom } = renderCigarette({
|
|
46
|
+
config,
|
|
47
|
+
pct: input.usedPercentage ?? 0,
|
|
48
|
+
frame: currentFrame(now),
|
|
49
|
+
active: isActive(input.transcriptPath, now),
|
|
50
|
+
model: input.modelDisplayName ?? ""
|
|
51
|
+
});
|
|
52
|
+
return `${top}
|
|
53
|
+
${bottom}`;
|
|
54
|
+
} catch {
|
|
55
|
+
return "\u{1F6AC}";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/cli.ts
|
|
60
|
+
var USAGE = `ccigarette ${VERSION} \u2014 a pixel cigarette for your Claude Code status line
|
|
61
|
+
|
|
62
|
+
Usage:
|
|
63
|
+
ccigarette interactive TUI configurator (run in a terminal)
|
|
64
|
+
ccigarette install write the statusLine block into Claude Code settings
|
|
65
|
+
ccigarette uninstall remove it (restores a previously replaced statusLine)
|
|
66
|
+
ccigarette --version print the version
|
|
67
|
+
ccigarette --help show this help
|
|
68
|
+
|
|
69
|
+
Claude Code pipes status JSON on stdin \u2014 that renders the status line itself.`;
|
|
70
|
+
function runCommand(argv) {
|
|
71
|
+
const [cmd] = argv;
|
|
72
|
+
try {
|
|
73
|
+
switch (cmd) {
|
|
74
|
+
case "--version":
|
|
75
|
+
case "-v":
|
|
76
|
+
process.stdout.write(`${VERSION}
|
|
77
|
+
`);
|
|
78
|
+
return;
|
|
79
|
+
case "--help":
|
|
80
|
+
case "-h":
|
|
81
|
+
process.stdout.write(`${USAGE}
|
|
82
|
+
`);
|
|
83
|
+
return;
|
|
84
|
+
case "install":
|
|
85
|
+
install();
|
|
86
|
+
process.stdout.write(`\u2713 statusLine installed \u2192 ${settingsPath()}
|
|
87
|
+
`);
|
|
88
|
+
return;
|
|
89
|
+
case "uninstall":
|
|
90
|
+
process.stdout.write(uninstall() ? `\u2713 statusLine removed from ${settingsPath()}
|
|
91
|
+
` : `nothing to remove \u2014 no ccigarette statusLine in ${settingsPath()}
|
|
92
|
+
`);
|
|
93
|
+
return;
|
|
94
|
+
default:
|
|
95
|
+
process.stderr.write(`unknown argument: ${cmd}
|
|
96
|
+
|
|
97
|
+
${USAGE}
|
|
98
|
+
`);
|
|
99
|
+
process.exitCode = 1;
|
|
100
|
+
}
|
|
101
|
+
} catch (e) {
|
|
102
|
+
process.stderr.write(`${e instanceof SettingsError ? e.message : String(e)}
|
|
103
|
+
`);
|
|
104
|
+
process.exitCode = 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function readStdin(stream) {
|
|
108
|
+
const chunks = [];
|
|
109
|
+
for await (const c of stream) chunks.push(Buffer.from(c));
|
|
110
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
111
|
+
}
|
|
112
|
+
async function main() {
|
|
113
|
+
const argv = process.argv.slice(2);
|
|
114
|
+
if (argv.length > 0) {
|
|
115
|
+
runCommand(argv);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (process.stdin.isTTY) {
|
|
119
|
+
const { run } = await import("./app-N2XCI26M.js");
|
|
120
|
+
await run();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const raw = await readStdin(process.stdin);
|
|
124
|
+
process.stdout.write(`${runStatusline(raw, Date.now())}
|
|
125
|
+
`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/bin.ts
|
|
129
|
+
process.stdout.on("error", () => process.exit(0));
|
|
130
|
+
main().catch(() => {
|
|
131
|
+
process.stdout.write("\u{1F6AC}\n");
|
|
132
|
+
process.exit(0);
|
|
133
|
+
});
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync } from "fs";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
import { dirname, join } from "path";
|
|
7
|
+
var DEFAULT_CONFIG = {
|
|
8
|
+
brand: "marlboro",
|
|
9
|
+
direction: "right",
|
|
10
|
+
showLabel: true,
|
|
11
|
+
showPercent: true,
|
|
12
|
+
showModel: true,
|
|
13
|
+
smoke: true,
|
|
14
|
+
backdrop: false,
|
|
15
|
+
// transparent terminal background; true renders a dark card like in the mockup
|
|
16
|
+
warnAt: 90,
|
|
17
|
+
colorMode: "truecolor"
|
|
18
|
+
};
|
|
19
|
+
var BRANDS = ["marlboro", "kent", "camel", "parliament", "lucky", "belomor"];
|
|
20
|
+
var DIRECTIONS = ["right", "left"];
|
|
21
|
+
var COLOR_MODES = ["truecolor", "ansi256"];
|
|
22
|
+
function configPath() {
|
|
23
|
+
const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
24
|
+
return join(base, "ccigarette", "config.json");
|
|
25
|
+
}
|
|
26
|
+
var oneOf = (v, allowed, def) => allowed.includes(v) ? v : def;
|
|
27
|
+
var bool = (v, def) => typeof v === "boolean" ? v : def;
|
|
28
|
+
function normalizeConfig(partial) {
|
|
29
|
+
const p = partial ?? {};
|
|
30
|
+
const warn = typeof p.warnAt === "number" && Number.isFinite(p.warnAt) ? Math.max(0, Math.min(100, Math.round(p.warnAt))) : DEFAULT_CONFIG.warnAt;
|
|
31
|
+
return {
|
|
32
|
+
brand: oneOf(p.brand, BRANDS, DEFAULT_CONFIG.brand),
|
|
33
|
+
direction: oneOf(p.direction, DIRECTIONS, DEFAULT_CONFIG.direction),
|
|
34
|
+
showLabel: bool(p.showLabel, DEFAULT_CONFIG.showLabel),
|
|
35
|
+
showPercent: bool(p.showPercent, DEFAULT_CONFIG.showPercent),
|
|
36
|
+
showModel: bool(p.showModel, DEFAULT_CONFIG.showModel),
|
|
37
|
+
smoke: bool(p.smoke, DEFAULT_CONFIG.smoke),
|
|
38
|
+
backdrop: bool(p.backdrop, DEFAULT_CONFIG.backdrop),
|
|
39
|
+
warnAt: warn,
|
|
40
|
+
colorMode: oneOf(p.colorMode, COLOR_MODES, DEFAULT_CONFIG.colorMode)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function loadConfig() {
|
|
44
|
+
try {
|
|
45
|
+
return normalizeConfig(JSON.parse(readFileSync(configPath(), "utf8")));
|
|
46
|
+
} catch {
|
|
47
|
+
return { ...DEFAULT_CONFIG };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function saveConfig(config) {
|
|
51
|
+
const p = configPath();
|
|
52
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
53
|
+
const tmp = `${p}.tmp`;
|
|
54
|
+
writeFileSync(tmp, JSON.stringify(normalizeConfig(config), null, 2));
|
|
55
|
+
renameSync(tmp, p);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/render/palette.ts
|
|
59
|
+
var PALETTE = {
|
|
60
|
+
paper: "#F2F0EB",
|
|
61
|
+
// paper from the mockup: readable thanks to the dark backdrop (termBg)
|
|
62
|
+
termBg: "#0C0C10",
|
|
63
|
+
// background of the terminal card from the mockup — backdrop behind the art
|
|
64
|
+
ashTip: "#B0AFAA",
|
|
65
|
+
emberHot: "#FFB03A",
|
|
66
|
+
emberCore: "#FF6B35",
|
|
67
|
+
emberDeep: "#E8491D",
|
|
68
|
+
unlit: "#4A4A4A",
|
|
69
|
+
smokeNear: "#9E9E9E",
|
|
70
|
+
smokeFar: "#5E5E5E",
|
|
71
|
+
meta: "#6E6E6E",
|
|
72
|
+
warn: "#E8491D"
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/render/brands.ts
|
|
76
|
+
var ART_WIDTH = 20;
|
|
77
|
+
var BRANDS2 = {
|
|
78
|
+
marlboro: {
|
|
79
|
+
label: "Marlboro",
|
|
80
|
+
labelColor: "#D42A30",
|
|
81
|
+
body: 11,
|
|
82
|
+
band: [["\u2588", "#D42A30"]],
|
|
83
|
+
filter: [["\u2590", "#C8813C"], ["\u2588", "#C8813C"], ["\u2592", "#A9662B"], ["\u2588", "#C8813C"], ["\u258C", "#C8813C"]]
|
|
84
|
+
},
|
|
85
|
+
kent: {
|
|
86
|
+
label: "KENT",
|
|
87
|
+
labelColor: "#1B4F9C",
|
|
88
|
+
body: 12,
|
|
89
|
+
band: [["\u2588", "#1B4F9C"]],
|
|
90
|
+
filter: [["\u2590", "#EDEDE6"], ["\u2588", "#EDEDE6"], ["\u2588", "#EDEDE6"], ["\u258C", "#EDEDE6"]]
|
|
91
|
+
},
|
|
92
|
+
camel: {
|
|
93
|
+
label: "Camel",
|
|
94
|
+
labelColor: "#B8804A",
|
|
95
|
+
body: 11,
|
|
96
|
+
band: [["\u2588", "#E8A33D"]],
|
|
97
|
+
filter: [["\u2590", "#C8813C"], ["\u2592", "#A9662B"], ["\u2588", "#C8813C"], ["\u2592", "#A9662B"], ["\u258C", "#C8813C"]]
|
|
98
|
+
},
|
|
99
|
+
parliament: {
|
|
100
|
+
label: "Parliament",
|
|
101
|
+
labelColor: "#4A7BC4",
|
|
102
|
+
body: 11,
|
|
103
|
+
band: [["\u2588", "#24549C"]],
|
|
104
|
+
filter: [["\u2590", "#EDEDE6"], ["\u2588", "#EDEDE6"], ["\u2593", "#9FA8B8"], [" ", ""], ["\u258C", "#EDEDE6"]]
|
|
105
|
+
},
|
|
106
|
+
lucky: {
|
|
107
|
+
label: "LUCKY STRIKE",
|
|
108
|
+
labelColor: "#CE1126",
|
|
109
|
+
body: 15,
|
|
110
|
+
band: [],
|
|
111
|
+
filter: [["\u2588", "#CE1126"], ["\u258C", "#F2F0EB"]]
|
|
112
|
+
},
|
|
113
|
+
belomor: {
|
|
114
|
+
label: "Belomorkanal",
|
|
115
|
+
labelColor: "#6B99BC",
|
|
116
|
+
body: 9,
|
|
117
|
+
paper: "#EFE9DC",
|
|
118
|
+
band: [],
|
|
119
|
+
filter: [
|
|
120
|
+
["\u2590", "#D9CBAA"],
|
|
121
|
+
["\u2550", "#D9CBAA"],
|
|
122
|
+
["\u2550", "#D9CBAA"],
|
|
123
|
+
["\u2550", "#D9CBAA"],
|
|
124
|
+
["\u2550", "#D9CBAA"],
|
|
125
|
+
["\u2550", "#D9CBAA"],
|
|
126
|
+
["\u2550", "#D9CBAA"],
|
|
127
|
+
["\u258C", "#D9CBAA"]
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
var BRAND_ORDER = ["marlboro", "kent", "camel", "parliament", "lucky", "belomor"];
|
|
132
|
+
var SMOKE_FRAMES = [
|
|
133
|
+
[[-1, "~", PALETTE.smokeNear]],
|
|
134
|
+
[[-2, "\xB0", PALETTE.smokeFar], [-1, "~", PALETTE.smokeNear]],
|
|
135
|
+
[[-3, "\xB0", PALETTE.smokeFar], [-2, "~", PALETTE.smokeNear]],
|
|
136
|
+
[[-3, "\xB0", PALETTE.smokeFar]]
|
|
137
|
+
];
|
|
138
|
+
var MIRROR = { "\u2590": "\u258C", "\u258C": "\u2590" };
|
|
139
|
+
|
|
140
|
+
// package.json
|
|
141
|
+
var package_default = {
|
|
142
|
+
name: "ccigarette",
|
|
143
|
+
version: "0.1.0",
|
|
144
|
+
description: "A smouldering pixel cigarette that burns down as your Claude Code context window fills up.",
|
|
145
|
+
type: "module",
|
|
146
|
+
license: "MIT",
|
|
147
|
+
author: "Ivan Matveev",
|
|
148
|
+
repository: { type: "git", url: "git+https://github.com/blackbalancef/ccigarette.git" },
|
|
149
|
+
homepage: "https://github.com/blackbalancef/ccigarette#readme",
|
|
150
|
+
bugs: "https://github.com/blackbalancef/ccigarette/issues",
|
|
151
|
+
exports: {},
|
|
152
|
+
keywords: [
|
|
153
|
+
"claude-code",
|
|
154
|
+
"statusline",
|
|
155
|
+
"status-line",
|
|
156
|
+
"cli",
|
|
157
|
+
"terminal",
|
|
158
|
+
"cigarette",
|
|
159
|
+
"context-window",
|
|
160
|
+
"ansi",
|
|
161
|
+
"tui"
|
|
162
|
+
],
|
|
163
|
+
bin: { ccigarette: "dist/bin.js" },
|
|
164
|
+
files: ["dist", "README.md", "LICENSE"],
|
|
165
|
+
engines: { node: ">=22" },
|
|
166
|
+
scripts: {
|
|
167
|
+
build: "tsup",
|
|
168
|
+
test: "vitest run",
|
|
169
|
+
"test:watch": "vitest",
|
|
170
|
+
typecheck: "tsc --noEmit",
|
|
171
|
+
prepublishOnly: "npm run build",
|
|
172
|
+
"demo:record": "vhs docs/demo.tape",
|
|
173
|
+
"demo:crop": 'ffmpeg -y -i docs/demo.raw.gif -filter_complex "[0:v]crop=840:120:0:8[c];[c]split[a][b];[a]palettegen=max_colors=64[p];[b][p]paletteuse=dither=bayer:bayer_scale=5" docs/demo.gif',
|
|
174
|
+
demo: "npm run build && npm run demo:record && npm run demo:crop"
|
|
175
|
+
},
|
|
176
|
+
dependencies: {
|
|
177
|
+
ink: "^7.1.0",
|
|
178
|
+
react: "^19.2.0"
|
|
179
|
+
},
|
|
180
|
+
devDependencies: {
|
|
181
|
+
"@types/node": "^22.0.0",
|
|
182
|
+
"@types/react": "^19.2.0",
|
|
183
|
+
"ink-testing-library": "^4.0.0",
|
|
184
|
+
tsup: "^8.5.0",
|
|
185
|
+
typescript: "^6.0.0",
|
|
186
|
+
vitest: "^4.1.0"
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/version.ts
|
|
191
|
+
var VERSION = package_default.version;
|
|
192
|
+
|
|
193
|
+
// src/install.ts
|
|
194
|
+
import { copyFileSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
195
|
+
import { homedir as homedir2 } from "os";
|
|
196
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
197
|
+
var COMMAND = `npx -y ccigarette@${VERSION}`;
|
|
198
|
+
var STATUSLINE_BLOCK = { type: "command", command: COMMAND, padding: 0, refreshInterval: 1 };
|
|
199
|
+
var SettingsError = class extends Error {
|
|
200
|
+
};
|
|
201
|
+
function settingsPath() {
|
|
202
|
+
const dir = process.env.CLAUDE_CONFIG_DIR || join2(homedir2(), ".claude");
|
|
203
|
+
return join2(dir, "settings.json");
|
|
204
|
+
}
|
|
205
|
+
function backupPath() {
|
|
206
|
+
return join2(dirname2(settingsPath()), "ccigarette.statusline-backup.json");
|
|
207
|
+
}
|
|
208
|
+
function readSettings() {
|
|
209
|
+
let raw;
|
|
210
|
+
try {
|
|
211
|
+
raw = readFileSync2(settingsPath(), "utf8");
|
|
212
|
+
} catch (e) {
|
|
213
|
+
if (e.code === "ENOENT") return {};
|
|
214
|
+
throw new SettingsError(`cannot read ${settingsPath()}: ${e.message}`);
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(raw);
|
|
218
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
219
|
+
} catch {
|
|
220
|
+
throw new SettingsError(`${settingsPath()} is not valid JSON \u2014 fix or remove it, then retry`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function readSettingsSafe() {
|
|
224
|
+
try {
|
|
225
|
+
return readSettings();
|
|
226
|
+
} catch {
|
|
227
|
+
return {};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function writeSettings(settings) {
|
|
231
|
+
const p = settingsPath();
|
|
232
|
+
mkdirSync2(dirname2(p), { recursive: true });
|
|
233
|
+
if (existsSync(p)) copyFileSync(p, `${p}.bak`);
|
|
234
|
+
const tmp = `${p}.tmp`;
|
|
235
|
+
writeFileSync2(tmp, JSON.stringify(settings, null, 2));
|
|
236
|
+
renameSync2(tmp, p);
|
|
237
|
+
}
|
|
238
|
+
function isOurs(statusLine) {
|
|
239
|
+
return !!statusLine && typeof statusLine === "object" && typeof statusLine.command === "string" && statusLine.command.includes("ccigarette");
|
|
240
|
+
}
|
|
241
|
+
function isInstalled() {
|
|
242
|
+
return isOurs(readSettingsSafe().statusLine);
|
|
243
|
+
}
|
|
244
|
+
function hasForeignStatusLine() {
|
|
245
|
+
const sl = readSettingsSafe().statusLine;
|
|
246
|
+
return sl !== void 0 && !isOurs(sl);
|
|
247
|
+
}
|
|
248
|
+
function install() {
|
|
249
|
+
const s = readSettings();
|
|
250
|
+
if (s.statusLine !== void 0 && !isOurs(s.statusLine)) {
|
|
251
|
+
writeFileSync2(backupPath(), JSON.stringify(s.statusLine, null, 2));
|
|
252
|
+
}
|
|
253
|
+
s.statusLine = { ...STATUSLINE_BLOCK };
|
|
254
|
+
writeSettings(s);
|
|
255
|
+
}
|
|
256
|
+
function uninstall() {
|
|
257
|
+
const s = readSettings();
|
|
258
|
+
if (!isOurs(s.statusLine)) return false;
|
|
259
|
+
let previous;
|
|
260
|
+
try {
|
|
261
|
+
previous = JSON.parse(readFileSync2(backupPath(), "utf8"));
|
|
262
|
+
} catch {
|
|
263
|
+
previous = void 0;
|
|
264
|
+
}
|
|
265
|
+
if (previous !== void 0) {
|
|
266
|
+
s.statusLine = previous;
|
|
267
|
+
writeSettings(s);
|
|
268
|
+
rmSync(backupPath(), { force: true });
|
|
269
|
+
} else {
|
|
270
|
+
delete s.statusLine;
|
|
271
|
+
writeSettings(s);
|
|
272
|
+
}
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/render/ansi.ts
|
|
277
|
+
var RESET = "\x1B[0m";
|
|
278
|
+
var BOLD = "\x1B[1m";
|
|
279
|
+
function hexToRgb(hex) {
|
|
280
|
+
const int = parseInt(hex.slice(1), 16);
|
|
281
|
+
return [int >> 16 & 255, int >> 8 & 255, int & 255];
|
|
282
|
+
}
|
|
283
|
+
function toAnsi256(hex) {
|
|
284
|
+
const [r, g, b] = hexToRgb(hex);
|
|
285
|
+
if (r === g && g === b) {
|
|
286
|
+
if (r < 8) return 16;
|
|
287
|
+
if (r > 248) return 231;
|
|
288
|
+
return 232 + Math.round((r - 8) / 247 * 24);
|
|
289
|
+
}
|
|
290
|
+
const toLevel = (v) => Math.round(v / 255 * 5);
|
|
291
|
+
return 16 + 36 * toLevel(r) + 6 * toLevel(g) + toLevel(b);
|
|
292
|
+
}
|
|
293
|
+
function sgr(layer, hex, colorMode) {
|
|
294
|
+
if (colorMode === "ansi256") return `\x1B[${layer};5;${toAnsi256(hex)}m`;
|
|
295
|
+
const [r, g, b] = hexToRgb(hex);
|
|
296
|
+
return `\x1B[${layer};2;${r};${g};${b}m`;
|
|
297
|
+
}
|
|
298
|
+
function paint(text, hex, colorMode, bold = false, bgHex = "") {
|
|
299
|
+
const fg = hex ? sgr(38, hex, colorMode) : "";
|
|
300
|
+
const bg = bgHex ? sgr(48, bgHex, colorMode) : "";
|
|
301
|
+
if (!fg && !bg) return text;
|
|
302
|
+
return (bold ? BOLD : "") + fg + bg + text + RESET;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/render/cigarette.ts
|
|
306
|
+
function renderCigarette(input) {
|
|
307
|
+
const { config, frame, active, model } = input;
|
|
308
|
+
const b = BRANDS2[config.brand];
|
|
309
|
+
const paperColor = b.paper ?? PALETTE.paper;
|
|
310
|
+
const pct = Number.isFinite(input.pct) ? Math.max(0, Math.min(100, input.pct)) : 0;
|
|
311
|
+
const cm = config.colorMode;
|
|
312
|
+
const flip = config.direction === "left";
|
|
313
|
+
const burnt = Math.round(pct / 100 * b.body);
|
|
314
|
+
const rest = b.body - burnt;
|
|
315
|
+
const lit = pct > 0;
|
|
316
|
+
const cells = [];
|
|
317
|
+
for (let i = 0; i < burnt; i++) cells.push(["\u2581", PALETTE.ashTip]);
|
|
318
|
+
cells.push([lit && burnt > 0 ? "\u2592" : " ", PALETTE.ashTip]);
|
|
319
|
+
if (lit) {
|
|
320
|
+
const hot = active ? frame % 2 === 0 : true;
|
|
321
|
+
cells.push(["\u2593", hot ? PALETTE.emberDeep : PALETTE.emberCore]);
|
|
322
|
+
cells.push(["\u2588", hot ? PALETTE.emberCore : PALETTE.emberHot]);
|
|
323
|
+
} else {
|
|
324
|
+
cells.push(["\u2593", PALETTE.unlit], ["\u2593", PALETTE.unlit]);
|
|
325
|
+
}
|
|
326
|
+
for (let i = 0; i < rest; i++) cells.push(["\u2588", paperColor]);
|
|
327
|
+
cells.push(...b.band, ...b.filter);
|
|
328
|
+
const smoke = Array.from({ length: ART_WIDTH }, () => null);
|
|
329
|
+
if (lit && active && config.smoke) {
|
|
330
|
+
const anchor = 1 + burnt;
|
|
331
|
+
const fi = (frame % SMOKE_FRAMES.length + SMOKE_FRAMES.length) % SMOKE_FRAMES.length;
|
|
332
|
+
for (const [dx, ch, col] of SMOKE_FRAMES[fi]) {
|
|
333
|
+
const x = flip ? ART_WIDTH - 1 - (anchor + dx) : anchor + dx;
|
|
334
|
+
if (x >= 0 && x < ART_WIDTH) smoke[x] = [ch, col];
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const bottomCells = flip ? cells.slice().reverse().map(([ch, col]) => [MIRROR[ch] ?? ch, col]) : cells;
|
|
338
|
+
const bg = config.backdrop ? PALETTE.termBg : "";
|
|
339
|
+
const top = smoke.map((c) => c ? paint(c[0], c[1], cm, false, bg) : paint(" ", "", cm, false, bg)).join("");
|
|
340
|
+
const art = bottomCells.map(([ch, col]) => paint(ch, col, cm, false, bg)).join("");
|
|
341
|
+
let tailPlain = "";
|
|
342
|
+
const tail = [];
|
|
343
|
+
if (config.showLabel) {
|
|
344
|
+
const label = b.label.padEnd(13);
|
|
345
|
+
tailPlain += label;
|
|
346
|
+
tail.push(paint(label, b.labelColor, cm, true, bg));
|
|
347
|
+
}
|
|
348
|
+
const metaPlain = [];
|
|
349
|
+
const meta = [];
|
|
350
|
+
if (config.showPercent) {
|
|
351
|
+
const warn = pct >= config.warnAt;
|
|
352
|
+
const pctText = `${String(Math.round(pct)).padStart(3)}%`;
|
|
353
|
+
metaPlain.push(pctText);
|
|
354
|
+
meta.push(paint(pctText, warn ? PALETTE.warn : PALETTE.meta, cm, warn, bg));
|
|
355
|
+
}
|
|
356
|
+
if (config.showModel && model) {
|
|
357
|
+
const shown = model.length > 24 ? `${model.slice(0, 23)}\u2026` : model;
|
|
358
|
+
metaPlain.push(shown);
|
|
359
|
+
meta.push(paint(shown, PALETTE.meta, cm, false, bg));
|
|
360
|
+
}
|
|
361
|
+
if (meta.length) {
|
|
362
|
+
tailPlain += metaPlain.join(" \xB7 ");
|
|
363
|
+
tail.push(meta.join(paint(" \xB7 ", PALETTE.meta, cm, false, bg)));
|
|
364
|
+
}
|
|
365
|
+
if (!tail.length) return { top, bottom: art };
|
|
366
|
+
const pad = paint(" ".repeat(2 + tailPlain.length), "", cm, false, bg);
|
|
367
|
+
return { top: top + pad, bottom: art + paint(" ", "", cm, false, bg) + tail.join("") };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export {
|
|
371
|
+
loadConfig,
|
|
372
|
+
saveConfig,
|
|
373
|
+
BRANDS2 as BRANDS,
|
|
374
|
+
BRAND_ORDER,
|
|
375
|
+
renderCigarette,
|
|
376
|
+
VERSION,
|
|
377
|
+
SettingsError,
|
|
378
|
+
settingsPath,
|
|
379
|
+
isInstalled,
|
|
380
|
+
hasForeignStatusLine,
|
|
381
|
+
install,
|
|
382
|
+
uninstall
|
|
383
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ccigarette",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A smouldering pixel cigarette that burns down as your Claude Code context window fills up.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Ivan Matveev",
|
|
8
|
+
"repository": { "type": "git", "url": "git+https://github.com/blackbalancef/ccigarette.git" },
|
|
9
|
+
"homepage": "https://github.com/blackbalancef/ccigarette#readme",
|
|
10
|
+
"bugs": "https://github.com/blackbalancef/ccigarette/issues",
|
|
11
|
+
"exports": {},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"claude-code",
|
|
14
|
+
"statusline",
|
|
15
|
+
"status-line",
|
|
16
|
+
"cli",
|
|
17
|
+
"terminal",
|
|
18
|
+
"cigarette",
|
|
19
|
+
"context-window",
|
|
20
|
+
"ansi",
|
|
21
|
+
"tui"
|
|
22
|
+
],
|
|
23
|
+
"bin": { "ccigarette": "dist/bin.js" },
|
|
24
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
25
|
+
"engines": { "node": ">=22" },
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsup",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"test:watch": "vitest",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"prepublishOnly": "npm run build",
|
|
32
|
+
"demo:record": "vhs docs/demo.tape",
|
|
33
|
+
"demo:crop": "ffmpeg -y -i docs/demo.raw.gif -filter_complex \"[0:v]crop=840:120:0:8[c];[c]split[a][b];[a]palettegen=max_colors=64[p];[b][p]paletteuse=dither=bayer:bayer_scale=5\" docs/demo.gif",
|
|
34
|
+
"demo": "npm run build && npm run demo:record && npm run demo:crop"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"ink": "^7.1.0",
|
|
38
|
+
"react": "^19.2.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^22.0.0",
|
|
42
|
+
"@types/react": "^19.2.0",
|
|
43
|
+
"ink-testing-library": "^4.0.0",
|
|
44
|
+
"tsup": "^8.5.0",
|
|
45
|
+
"typescript": "^6.0.0",
|
|
46
|
+
"vitest": "^4.1.0"
|
|
47
|
+
}
|
|
48
|
+
}
|