komado 0.1.2 → 0.1.4

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 CHANGED
@@ -28,7 +28,7 @@ with [Ink](https://github.com/vadimdemedes/ink).
28
28
  - **Reading progress.** Where you left off is saved per manga and resumable from
29
29
  *Continue reading*.
30
30
  - **Sign in to MangaDex (optional).** Log in to browse your **followed library**, see
31
- which chapters you've already read, and **sync progress back** finishing a chapter
31
+ which chapters you've already read, and **sync progress back** - finishing a chapter
32
32
  marks it read on MangaDex. Reading itself needs no account.
33
33
  - **Offline-friendly.** Point it at folders of `.cbz`/`.zip` or loose images.
34
34
 
@@ -41,7 +41,7 @@ with [Ink](https://github.com/vadimdemedes/ink).
41
41
 
42
42
  ## Install
43
43
 
44
- **macOS / Linux** one line fetches the latest, builds it, and drops a `komado`
44
+ **macOS / Linux** - one line fetches the latest, builds it, and drops a `komado`
45
45
  launcher on your PATH:
46
46
 
47
47
  ```bash
@@ -52,24 +52,33 @@ Then just type **`komado`**. Re-run that same command any time to update. It nee
52
52
  `git`, **Node ≥ 20**, and `npm`; it installs to `~/.local/share/komado` with the
53
53
  launcher in `~/.local/bin` (override via `KOMADO_APP_DIR` / `KOMADO_BIN_DIR`).
54
54
 
55
- **Windows** the PowerShell one-liner installs Node.js for you (via `winget`) if it's
56
- missing, then komado, leaving a native `komado` command on your `PATH`:
55
+ **Windows** - one line installs Node.js for you (via `winget`) if it's missing,
56
+ then komado, leaving a native `komado` command on your `PATH`. Works from **both
57
+ CMD and PowerShell**, and ignores the execution policy either way:
57
58
 
58
- ```powershell
59
- irm https://raw.githubusercontent.com/RyuPrad/komado/main/install.ps1 | iex
59
+ ```
60
+ powershell -NoProfile -ExecutionPolicy Bypass -c "irm https://raw.githubusercontent.com/RyuPrad/komado/main/install.ps1 | iex"
60
61
  ```
61
62
 
62
- Already have **Node 20**? Install straight from PowerShell or CMD instead:
63
+ Prefer the native syntax? In **PowerShell**:
63
64
 
64
65
  ```powershell
65
- npm i -g komado
66
+ irm https://raw.githubusercontent.com/RyuPrad/komado/main/install.ps1 | iex
66
67
  ```
67
68
 
68
- Don't use the `curl | bash` line on Windows under **Git Bash** or **WSL** it
69
+ Already have **Node 20**? `npm i -g komado` works from either shell - CMD runs
70
+ npm's `.cmd` shim directly, and PowerShell does too if your execution policy
71
+ allows scripts.
72
+
73
+ > Note: the short `irm … | iex` form is PowerShell-only - in **CMD** it prints
74
+ > `'irm' is not recognized`. Use the longer one-liner above (which works in both),
75
+ > or run `npm i -g komado` directly.
76
+
77
+ Don't use the `curl … | bash` line on Windows - under **Git Bash** or **WSL** it
69
78
  installs a launcher that only runs inside that shell, never from CMD/PowerShell.
70
79
 
71
- Uninstall from inside the app **Settings → Uninstall komado…** (type `uninstall` to
72
- confirm) removes the app, its launcher, and `~/.komado` (config, reading progress,
80
+ Uninstall from inside the app - **Settings → Uninstall komado…** (type `uninstall` to
81
+ confirm) - removes the app, its launcher, and `~/.komado` (config, reading progress,
73
82
  MangaDex login). By hand that's
74
83
  `rm -rf ~/.local/share/komado ~/.local/bin/komado ~/.komado`.
75
84
 
@@ -87,7 +96,7 @@ terminal supports and where state is stored:
87
96
  npm run doctor
88
97
  ```
89
98
 
90
- Render a single image (path or URL) at the best fidelity your terminal allows handy
99
+ Render a single image (path or URL) at the best fidelity your terminal allows - handy
91
100
  for testing sixel/kitty terminals:
92
101
 
93
102
  ```bash
@@ -118,7 +127,7 @@ Readable text needs real pixels. At startup the app probes your terminal (run
118
127
  full-width with vertical pan by default; press `f` to toggle whole-page fit.
119
128
  Keys: `←`/`→` or `a`/`d` page · `↑`/`↓` pan · `n`/`p` chapter · `f` fit · `q` back.
120
129
  - **neither** → the in-Ink **cell reader** is used (Unicode half-blocks, or
121
- chafa symbols when available). Fine for art, coarse for small lettering
130
+ chafa symbols when available). Fine for art, coarse for small lettering -
122
131
  that's the hard ceiling of character-cell rendering.
123
132
 
124
133
  Terminals with pixel support include kitty, WezTerm, Ghostty, foot, recent
@@ -128,9 +137,9 @@ Windows Terminal (≥ 1.22), and VS Code's terminal (with image support enabled)
128
137
 
129
138
  Reading from MangaDex needs **no account**. Signing in only adds personalization:
130
139
 
131
- - **My Library** browse the manga you follow on MangaDex.
132
- - **Read-markers** chapters you've already read are marked in the chapter list.
133
- - **Progress sync** finishing a chapter pushes a read-marker back to MangaDex.
140
+ - **My Library** - browse the manga you follow on MangaDex.
141
+ - **Read-markers** - chapters you've already read are marked in the chapter list.
142
+ - **Progress sync** - finishing a chapter pushes a read-marker back to MangaDex.
134
143
 
135
144
  MangaDex uses OAuth2 **personal clients**, so logging in is a one-time setup:
136
145
 
@@ -140,7 +149,7 @@ MangaDex uses OAuth2 **personal clients**, so logging in is a one-time setup:
140
149
  2. In the app: **Settings → Log in to MangaDex…**, then enter the client id/secret plus
141
150
  your MangaDex username and password.
142
151
 
143
- The session is **durable** it requests an `offline_access` token and persists it, so
152
+ The session is **durable** - it requests an `offline_access` token and persists it, so
144
153
  you stay logged in across restarts until you explicitly **log out** (Settings). Only the
145
154
  client id/secret + refresh token are stored, in `~/.komado/credentials.json` (mode
146
155
  `600`); your password is never written to disk. Toggle write-back any time with
@@ -166,10 +175,10 @@ archives are supported (RAR via the WASM `node-unrar-js`, no system binary neede
166
175
 
167
176
  Everything is self-contained under `~/.komado/` (override with `KOMADO_HOME`):
168
177
 
169
- - `config.json` preferences + library paths
170
- - `progress.json` reading progress
171
- - `credentials.json` MangaDex login (client id/secret + refresh token; mode `600`)
172
- - `cache/` scratch space for the renderer
178
+ - `config.json` - preferences + library paths
179
+ - `progress.json` - reading progress
180
+ - `credentials.json` - MangaDex login (client id/secret + refresh token; mode `600`)
181
+ - `cache/` - scratch space for the renderer
173
182
 
174
183
  ## Architecture
175
184
 
@@ -180,23 +189,23 @@ cli.js → app.js (screen stack) → screens → hooks/state → sources/* → H
180
189
  └──────────────→ render/* (image → terminal)
181
190
  ```
182
191
 
183
- - **`src/sources/*`** each source (`mangadex`, `local`) implements the same interface
192
+ - **`src/sources/*`** - each source (`mangadex`, `local`) implements the same interface
184
193
  (`search`, `getManga`, `listChapters`, `getPages`, `loadPageBuffer`) and returns the
185
194
  unified `{ data, pagination, meta }` envelope. The only source-specific seam is
186
195
  `loadPageBuffer`, which resolves raw bytes.
187
- - **`src/domain/shape.js`** the unified `Manga`/`Chapter` contract.
188
- - **`src/render/*`** capability detection + half-block (`sharp`) and `chafa` backends
196
+ - **`src/domain/shape.js`** - the unified `Manga`/`Chapter` contract.
197
+ - **`src/render/*`** - capability detection + half-block (`sharp`) and `chafa` backends
189
198
  behind one `renderInline()` dispatcher, with half-block as the always-works fallback.
190
- - **`src/lib/*`** cross-cutting utilities ported from a server toolkit:
199
+ - **`src/lib/*`** - cross-cutting utilities ported from a server toolkit:
191
200
  `fetchWithBackoff` (retries 429/5xx + timeout), `createCache` (TTL + negative caching
192
201
  + stampede protection), `AppError`/typed errors, the response `envelope`.
193
- - **`src/components/*` + `src/hooks/*`** Ink UI. Effects use a cancelled-flag/abort
202
+ - **`src/components/*` + `src/hooks/*`** - Ink UI. Effects use a cancelled-flag/abort
194
203
  guard against out-of-order responses, and reading progress writes are debounced.
195
204
 
196
205
  ## Development
197
206
 
198
207
  UI is plain JSX in `.js` files, transpiled to `dist/` by esbuild (a small
199
- `scripts/build.mjs` does a transpile-only, structure-preserving build no bundling,
208
+ `scripts/build.mjs` does a transpile-only, structure-preserving build - no bundling,
200
209
  so package imports stay external).
201
210
 
202
211
  ```bash
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { jsx } from "react/jsx-runtime";
3
3
  import process from "node:process";
4
4
  function printHelp() {
5
- console.log(`komado \u2014 a terminal manga reader (MangaDex + local files)
5
+ console.log(`komado - a terminal manga reader (MangaDex + local files)
6
6
 
7
7
  Usage:
8
8
  komado launch the interactive reader
@@ -40,8 +40,8 @@ async function doctor() {
40
40
  console.log(` cell size: ${probe.cellW ?? "?"}x${probe.cellH ?? "?"} px`);
41
41
  }
42
42
  } else {
43
- console.log(` kitty graphics: ${caps.kitty} (env guess \u2014 run in a real terminal to probe)`);
44
- console.log(` sixel: ${caps.sixel} (env guess \u2014 run in a real terminal to probe)`);
43
+ console.log(` kitty graphics: ${caps.kitty} (env guess - run in a real terminal to probe)`);
44
+ console.log(` sixel: ${caps.sixel} (env guess - run in a real terminal to probe)`);
45
45
  }
46
46
  console.log(` chafa: ${caps.chafa ? caps.chafaVersion : "not installed"}`);
47
47
  console.log(` inline backend: ${caps.chafa ? "chafa-symbols" : "half-block"} (config.renderer=${cfg.renderer})`);
@@ -50,7 +50,7 @@ async function doctor() {
50
50
  console.log(
51
51
  protos.length ? `
52
52
  \u2192 Pixel graphics available (${protos.join(", ")}). Crisp rendering is possible:
53
- test it with node dist/cli.js render <some-image>` : "\n \u2192 No pixel protocol detected \u2014 rendering is limited to character cells."
53
+ test it with node dist/cli.js render <some-image>` : "\n \u2192 No pixel protocol detected - rendering is limited to character cells."
54
54
  );
55
55
  }
56
56
  console.log("\nPaths:");
@@ -63,7 +63,7 @@ async function doctor() {
63
63
  console.log(` dataSaver: ${cfg.dataSaver}`);
64
64
  console.log(` renderer: ${cfg.renderer}`);
65
65
  console.log(` contentRating: ${cfg.contentRating.join(", ")}`);
66
- console.log(` localLibraryPaths: ${cfg.localLibraryPaths.length ? cfg.localLibraryPaths.join(", ") : "(none \u2014 add in Settings)"}`);
66
+ console.log(` localLibraryPaths: ${cfg.localLibraryPaths.length ? cfg.localLibraryPaths.join(", ") : "(none - add in Settings)"}`);
67
67
  }
68
68
  async function renderCmd(target, rest) {
69
69
  if (!target) {
@@ -136,7 +136,7 @@ ${formatUninstallSummary(results)}
136
136
  /* @__PURE__ */ jsx(Text, { children: "This permanently deletes:" }),
137
137
  uninstallTargets().map((t) => /* @__PURE__ */ jsxs(Text, { color: "red", children: [
138
138
  ` \u2022 ${displayPath(t.path)}`,
139
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` \u2014 ${t.label}` })
139
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ` - ${t.label}` })
140
140
  ] }, t.path)),
141
141
  /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
142
142
  /* @__PURE__ */ jsx(Text, { children: "Type " }),
package/dist/config.js CHANGED
@@ -29,7 +29,7 @@ const DEFAULT_CONFIG = {
29
29
  // preferred MangaDex translatedLanguage
30
30
  contentRating: ["safe", "suggestive"],
31
31
  dataSaver: true,
32
- // smaller MangaDex page images ideal for a terminal
32
+ // smaller MangaDex page images - ideal for a terminal
33
33
  renderer: "auto",
34
34
  // auto | halfblock | chafa
35
35
  theme: "default",
@@ -34,7 +34,7 @@ function makeChapter(p) {
34
34
  }
35
35
  function chapterLabel(ch) {
36
36
  const head = ch.number != null && ch.number !== "" ? `Ch. ${ch.number}${ch.volume != null && ch.volume !== "" ? ` (Vol. ${ch.volume})` : ""}` : "Oneshot";
37
- return ch.title ? `${head} \u2014 ${ch.title}` : head;
37
+ return ch.title ? `${head} - ${ch.title}` : head;
38
38
  }
39
39
  export {
40
40
  chapterLabel,
@@ -76,7 +76,7 @@ async function getAccessToken({ signal, force = false } = {}) {
76
76
  return access.token;
77
77
  } catch (err) {
78
78
  if (err.oauthError === "invalid_grant") {
79
- logger.warn("MangaDex refresh token expired/revoked \u2014 logging out", err);
79
+ logger.warn("MangaDex refresh token expired/revoked - logging out", err);
80
80
  logout();
81
81
  }
82
82
  throw err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "komado",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "A terminal manga reader (MangaDex + local files) rendered with Ink.",
5
5
  "keywords": [
6
6
  "manga",
package/src/app.js CHANGED
@@ -39,7 +39,7 @@ export function App({ caps = {}, onViewer, initialRoute = null }) {
39
39
  exit();
40
40
  }, [exit]);
41
41
 
42
- // Global keys suppressed while a text input is focused (`typing`).
42
+ // Global keys - suppressed while a text input is focused (`typing`).
43
43
  useInput((input, key) => {
44
44
  if (typing) return;
45
45
  if (input === 'q') quit();
package/src/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import process from 'node:process';
3
3
 
4
4
  function printHelp() {
5
- console.log(`komado a terminal manga reader (MangaDex + local files)
5
+ console.log(`komado - a terminal manga reader (MangaDex + local files)
6
6
 
7
7
  Usage:
8
8
  komado launch the interactive reader
@@ -42,8 +42,8 @@ async function doctor() {
42
42
  console.log(` cell size: ${probe.cellW ?? '?'}x${probe.cellH ?? '?'} px`);
43
43
  }
44
44
  } else {
45
- console.log(` kitty graphics: ${caps.kitty} (env guess run in a real terminal to probe)`);
46
- console.log(` sixel: ${caps.sixel} (env guess run in a real terminal to probe)`);
45
+ console.log(` kitty graphics: ${caps.kitty} (env guess - run in a real terminal to probe)`);
46
+ console.log(` sixel: ${caps.sixel} (env guess - run in a real terminal to probe)`);
47
47
  }
48
48
  console.log(` chafa: ${caps.chafa ? caps.chafaVersion : 'not installed'}`);
49
49
  console.log(` inline backend: ${caps.chafa ? 'chafa-symbols' : 'half-block'} (config.renderer=${cfg.renderer})`);
@@ -52,7 +52,7 @@ async function doctor() {
52
52
  console.log(
53
53
  protos.length
54
54
  ? `\n → Pixel graphics available (${protos.join(', ')}). Crisp rendering is possible:\n test it with node dist/cli.js render <some-image>`
55
- : '\n → No pixel protocol detected rendering is limited to character cells.',
55
+ : '\n → No pixel protocol detected - rendering is limited to character cells.',
56
56
  );
57
57
  }
58
58
  console.log('\nPaths:');
@@ -65,7 +65,7 @@ async function doctor() {
65
65
  console.log(` dataSaver: ${cfg.dataSaver}`);
66
66
  console.log(` renderer: ${cfg.renderer}`);
67
67
  console.log(` contentRating: ${cfg.contentRating.join(', ')}`);
68
- console.log(` localLibraryPaths: ${cfg.localLibraryPaths.length ? cfg.localLibraryPaths.join(', ') : '(none add in Settings)'}`);
68
+ console.log(` localLibraryPaths: ${cfg.localLibraryPaths.length ? cfg.localLibraryPaths.join(', ') : '(none - add in Settings)'}`);
69
69
  }
70
70
 
71
71
  async function renderCmd(target, rest) {
@@ -20,7 +20,7 @@ export function List({
20
20
  const selected = count ? Math.min(index, count - 1) : 0;
21
21
 
22
22
  // Notify the parent of the highlighted item when the selection (or list size)
23
- // changes. Keyed on the index + count (primitives) NOT the `items` array,
23
+ // changes. Keyed on the index + count (primitives) - NOT the `items` array,
24
24
  // whose reference changes every render in callers that rebuild it inline. If
25
25
  // `items` were a dep, a parent whose onHighlight calls setState would loop:
26
26
  // render → new items ref → effect → setState → render → … (max update depth).
@@ -9,7 +9,7 @@ import { truncate } from '../../lib/text.js';
9
9
  const PAGE = 32;
10
10
 
11
11
  // The signed-in user's MangaDex follows. Same browse/paginate shape as the
12
- // search screen, minus the query box getFollows is just another envelope.
12
+ // search screen, minus the query box - getFollows is just another envelope.
13
13
  export function LibraryScreen({ params }) {
14
14
  const sourceId = params?.sourceId || 'mangadex';
15
15
  const ui = useUI();
@@ -22,7 +22,7 @@ export function LoginScreen() {
22
22
  const [busy, setBusy] = useState(false);
23
23
  const [error, setError] = useState(null);
24
24
 
25
- // The whole screen is a form keep global keys (q / Esc) suppressed.
25
+ // The whole screen is a form - keep global keys (q / Esc) suppressed.
26
26
  useEffect(() => {
27
27
  ui.setTyping(true);
28
28
  return () => ui.setTyping(false);
@@ -39,7 +39,7 @@ export function MangaScreen({ params }) {
39
39
  if (isLoggedIn() && source.getReadMarkers) {
40
40
  source.getReadMarkers(initial.id, { signal: ctrl.signal })
41
41
  .then((ids) => { if (!cancelled) setReadSet(new Set(ids)); })
42
- .catch(() => {}); // decorative never block the screen on this
42
+ .catch(() => {}); // decorative - never block the screen on this
43
43
  }
44
44
  } catch (err) {
45
45
  if (!cancelled) setError(err);
@@ -75,7 +75,7 @@ export function ReaderScreen({ params }) {
75
75
  return out;
76
76
  };
77
77
 
78
- // Background prefetch (own controller survives page turns, aborts on unmount).
78
+ // Background prefetch (own controller - survives page turns, aborts on unmount).
79
79
  const prefetchers = useRef(new Set());
80
80
  useEffect(() => () => {
81
81
  for (const c of prefetchers.current) c.abort();
@@ -136,7 +136,7 @@ export function SettingsScreen() {
136
136
  {uninstallTargets().map((t) => (
137
137
  <Text key={t.path} color="red">
138
138
  {` • ${displayPath(t.path)}`}
139
- <Text dimColor>{` ${t.label}`}</Text>
139
+ <Text dimColor>{` - ${t.label}`}</Text>
140
140
  </Text>
141
141
  ))}
142
142
  <Box marginTop={1}>
@@ -3,7 +3,7 @@ import { Box, Text } from 'ink';
3
3
 
4
4
  const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
5
5
 
6
- // Hand-rolled spinner dependency-light, matching your preference for not
6
+ // Hand-rolled spinner - dependency-light, matching your preference for not
7
7
  // pulling a package for something this small.
8
8
  export function Spinner({ label = 'Loading' }) {
9
9
  const [frame, setFrame] = useState(0);
package/src/config.js CHANGED
@@ -7,7 +7,7 @@ const usingDefaultHome = !process.env.KOMADO_HOME;
7
7
  const HOME = usingDefaultHome
8
8
  ? path.join(os.homedir(), '.komado')
9
9
  : path.resolve(process.env.KOMADO_HOME);
10
- // The pre-rename home (the project used to be "manga-tui") migrated once on
10
+ // The pre-rename home (the project used to be "manga-tui") - migrated once on
11
11
  // first run so existing config / reading progress / MangaDex login carry over.
12
12
  const LEGACY_HOME = path.join(os.homedir(), '.manga-tui');
13
13
 
@@ -34,7 +34,7 @@ export const DEFAULT_CONFIG = {
34
34
  localLibraryPaths: [], // directories scanned by the local source
35
35
  language: 'en', // preferred MangaDex translatedLanguage
36
36
  contentRating: ['safe', 'suggestive'],
37
- dataSaver: true, // smaller MangaDex page images ideal for a terminal
37
+ dataSaver: true, // smaller MangaDex page images - ideal for a terminal
38
38
  renderer: 'auto', // auto | halfblock | chafa
39
39
  theme: 'default',
40
40
  syncProgress: true, // push read-markers to MangaDex while logged in
@@ -43,5 +43,5 @@ export function chapterLabel(ch) {
43
43
  const head = ch.number != null && ch.number !== ''
44
44
  ? `Ch. ${ch.number}${ch.volume != null && ch.volume !== '' ? ` (Vol. ${ch.volume})` : ''}`
45
45
  : 'Oneshot';
46
- return ch.title ? `${head} ${ch.title}` : head;
46
+ return ch.title ? `${head} - ${ch.title}` : head;
47
47
  }
@@ -1,4 +1,4 @@
1
- // Typed, operational errors same idea as your server AppError, minus HTTP plumbing.
1
+ // Typed, operational errors - same idea as your server AppError, minus HTTP plumbing.
2
2
  // statusCode is kept for parity/log triage; the TUI maps these to friendly messages.
3
3
  export class AppError extends Error {
4
4
  constructor(message, statusCode = 500, opts = {}) {
package/src/lib/cache.js CHANGED
@@ -1,4 +1,4 @@
1
- // In-memory cache with TTL, negative caching, and stampede protection
1
+ // In-memory cache with TTL, negative caching, and stampede protection -
2
2
  // a port of your createCache. `wrap` shares a single in-flight promise per key
3
3
  // so concurrent callers (e.g. two screens requesting the same chapter) collapse
4
4
  // into one upstream request.
@@ -48,7 +48,7 @@ export async function fetchWithBackoff(url, options = {}) {
48
48
  }
49
49
  return res;
50
50
  } catch (err) {
51
- // Caller cancelled propagate without retrying.
51
+ // Caller cancelled - propagate without retrying.
52
52
  if (extSignal?.aborted) throw err;
53
53
  if (attempt < retries) {
54
54
  const delay = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt) + Math.random() * 200;
package/src/lib/logger.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import { paths } from '../config.js';
3
3
 
4
- // A TUI owns stdout console.log would corrupt the Ink render. So debug output
4
+ // A TUI owns stdout - console.log would corrupt the Ink render. So debug output
5
5
  // goes to a file, only when KOMADO_DEBUG is set. Tail it with:
6
6
  // tail -f ~/.komado/komado.log
7
7
  const enabled = !!process.env.KOMADO_DEBUG;
@@ -35,7 +35,7 @@ export function detectCapabilities() {
35
35
  chafa = true;
36
36
  chafaVersion = (out.match(/version\s+([\d.]+)/i) || [])[1] || 'unknown';
37
37
  } catch {
38
- /* chafa not on PATH half-block fallback */
38
+ /* chafa not on PATH - half-block fallback */
39
39
  }
40
40
 
41
41
  cached = { term, termProgram, kitty, sixel, truecolor, chafa, chafaVersion };
@@ -5,7 +5,7 @@ const RESET = `${ESC}[0m`;
5
5
 
6
6
  // Render an image buffer to terminal lines using the upper-half-block glyph (▀):
7
7
  // the glyph's foreground paints the TOP half of the cell, the background the
8
- // BOTTOM half so each character encodes two vertical pixels at 24-bit colour.
8
+ // BOTTOM half - so each character encodes two vertical pixels at 24-bit colour.
9
9
  // One column == one pixel wide, one row == two pixels tall.
10
10
  export async function renderHalfBlock(buffer, { cols = 80 } = {}) {
11
11
  const targetWidth = Math.max(1, Math.min(Math.floor(cols), 400));
@@ -34,7 +34,7 @@ export async function renderHalfBlock(buffer, { cols = 80 } = {}) {
34
34
  const [br, bg, bb] = y + 1 < height ? px(x, y + 1) : [tr, tg, tb];
35
35
  const fg = `${tr};${tg};${tb}`;
36
36
  const bgc = `${br};${bg};${bb}`;
37
- // Emit colour codes only when they change keeps lines compact.
37
+ // Emit colour codes only when they change - keeps lines compact.
38
38
  if (fg !== lastFg) { line += `${ESC}[38;2;${fg}m`; lastFg = fg; }
39
39
  if (bgc !== lastBg) { line += `${ESC}[48;2;${bgc}m`; lastBg = bgc; }
40
40
  line += '▀';
@@ -9,7 +9,7 @@ export async function imageSize(buffer) {
9
9
  }
10
10
 
11
11
  // Dispatch a buffer to the chosen inline backend, producing { lines, cols, rows }.
12
- // Always falls back to half-block so a chafa hiccup never blanks the reader
12
+ // Always falls back to half-block so a chafa hiccup never blanks the reader -
13
13
  // the same "graceful degradation" idea as your SSR→static fallback.
14
14
  export async function renderInline(buffer, { cols = 80, backend = 'halfblock' } = {}) {
15
15
  if (backend === 'chafa-symbols') {
@@ -11,7 +11,7 @@ const DEFAULT_CELL_H = 20;
11
11
 
12
12
  // Encode an already-correctly-sized image to sixel at its NATIVE pixel
13
13
  // resolution. --exact-size + font-ratio 1/1 makes chafa emit ~1 image px → 1
14
- // sixel px, so the displayed size is exactly what we sized the image to no
14
+ // sixel px, so the displayed size is exactly what we sized the image to - no
15
15
  // dependence on chafa guessing the terminal's cell size through a pipe.
16
16
  export async function encodePixels(buffer, { format = 'sixel' } = {}) {
17
17
  const { stdout } = await withTempImage(buffer, (file) =>
@@ -40,7 +40,7 @@ export async function scalePage(buffer, { cols, cellW }) {
40
40
  // mode 'width' → full terminal width, vertical window at cell offset `scroll`
41
41
  // Pass a pre-scaled page (`scaled` from scalePage) in 'width' mode to skip the
42
42
  // per-scroll resize; without it the page is scaled inline.
43
- // Returns { buffer, maxScroll, scroll, imageRows } scroll clamped to range,
43
+ // Returns { buffer, maxScroll, scroll, imageRows } - scroll clamped to range,
44
44
  // imageRows = the rendered image's height in cells (so the caller can erase any
45
45
  // rows below it instead of clearing the whole screen).
46
46
  export async function prepareImage(buffer, { mode, cols, rows, scroll = 0, cellW, cellH, scaled = null }) {
@@ -80,7 +80,7 @@ export async function prepareImage(buffer, { mode, cols, rows, scroll = 0, cellW
80
80
  // chafa + ~50ms of sharp per frame). But sixel is laid out as a fixed palette
81
81
  // defined UP FRONT followed by independent 6px-tall bands, each of which
82
82
  // re-selects its own colours. So we encode the whole page to sixel ONCE, then
83
- // window the pre-encoded bands per frame a string slice, not a re-encode.
83
+ // window the pre-encoded bands per frame - a string slice, not a re-encode.
84
84
  // (Verified against chafa 1.14: every palette entry precedes the first band and
85
85
  // every band starts with a colour select, so any band range stands alone.)
86
86
  const BAND_PX = 6;
@@ -109,7 +109,7 @@ export function parseSixelPage(raw) {
109
109
  // chafa omits a band's leading colour select when it equals the previous
110
110
  // band's last colour (the register carries across '-'). For a band to be the
111
111
  // TOP of a sliced window it must re-select that colour itself, so prepend the
112
- // carried register wherever it's missing making every band self-contained.
112
+ // carried register wherever it's missing - making every band self-contained.
113
113
  let carry = '0'; // sixel's default colour register
114
114
  const bands = rawBands.map((b) => {
115
115
  const fixed = b.startsWith('#') ? b : `#${carry}${b}`;
@@ -7,7 +7,7 @@ import { logger } from './lib/logger.js';
7
7
  const ESC = '\x1b';
8
8
 
9
9
  // Synchronized-update markers (DEC private mode 2026). Wrapping a frame makes a
10
- // supporting terminal present it atomically so the strip-scroll's "scroll the
10
+ // supporting terminal present it atomically - so the strip-scroll's "scroll the
11
11
  // region, then repaint the freed strip" can't flash a blank strip at the leading
12
12
  // edge. Ignored (harmless) on terminals that don't implement it.
13
13
  const SYNC_BEGIN = Buffer.from(`${ESC}[?2026h`, 'latin1');
@@ -30,7 +30,7 @@ export async function runViewer({ sourceId, manga, chapters, chapterIndex, start
30
30
 
31
31
  // Cell/band geometry for the optional strip-scroll. Sixel bands are 6px tall;
32
32
  // the terminal scrolls by whole cells. A "slot" = LCM(6, cellH)px is the
33
- // smallest shift that's whole in BOTH grids scrolling by slots lets us move
33
+ // smallest shift that's whole in BOTH grids - scrolling by slots lets us move
34
34
  // the on-screen pixels with the terminal and repaint only the newly-exposed
35
35
  // strip (seam-free), instead of re-sending the entire viewport every step.
36
36
  // Opt-in (KOMADO_SCROLL_DELTA=1) because it relies on the terminal scrolling
@@ -45,7 +45,7 @@ export async function runViewer({ sourceId, manga, chapters, chapterIndex, start
45
45
  let shownSig = null; // page + geometry the on-screen frame was drawn for
46
46
 
47
47
  // Per-page caches. draw() runs on every keypress, but the page bytes and the
48
- // full-width scale are constant within a page re-doing them per scroll step
48
+ // full-width scale are constant within a page - re-doing them per scroll step
49
49
  // (a network round-trip per step for remote sources) is what made scrolling
50
50
  // crawl. Cache both, keyed by page (and width for the scale).
51
51
  let rawKey = null;
@@ -200,7 +200,7 @@ export async function runViewer({ sourceId, manga, chapters, chapterIndex, start
200
200
  });
201
201
  // Last page reached → push a read-marker to MangaDex (self-guarded/deduped).
202
202
  // `pages &&`: a rapid n/p runs changeChapter() (pages = null) during one of
203
- // this draw's awaits, after it started guard before reading .length, or the
203
+ // this draw's awaits, after it started - guard before reading .length, or the
204
204
  // stale tail throws "Cannot read properties of null (reading 'length')".
205
205
  if (pages && pi === pages.length - 1 && source.syncChapterRead) {
206
206
  source.syncChapterRead(manga.id, chapters[ci].id);
@@ -212,7 +212,7 @@ export async function runViewer({ sourceId, manga, chapters, chapterIndex, start
212
212
  }
213
213
  }
214
214
 
215
- // Whole-viewport frame: home + overwrite (no full ESC[2J each step that
215
+ // Whole-viewport frame: home + overwrite (no full ESC[2J each step - that
216
216
  // clear-to-blank is what made scrolling blink), erase only the rows a shorter
217
217
  // image leaves below, then the status bar. One atomic write avoids partial
218
218
  // frames and extra syscalls.
@@ -224,7 +224,7 @@ export async function runViewer({ sourceId, manga, chapters, chapterIndex, start
224
224
  }
225
225
 
226
226
  // Strip-scroll frame (opt-in): scroll the image area with the terminal and
227
- // repaint only the slot of sixel that scrolled into view far less data than
227
+ // repaint only the slot of sixel that scrolled into view - far less data than
228
228
  // re-sending the whole viewport. Valid only for slot-sized shifts (whole cells
229
229
  // AND whole bands), so the moved pixels stay band-aligned and there's no seam.
230
230
  // Uses LF/RI inside a DECSTBM region (the most widely supported scroll path).
@@ -248,7 +248,7 @@ export async function runViewer({ sourceId, manga, chapters, chapterIndex, start
248
248
  }
249
249
 
250
250
  // Coalescing scheduler: while a draw runs, extra requests collapse into a
251
- // single follow-up at the latest state input is never dropped and draws
251
+ // single follow-up at the latest state - input is never dropped and draws
252
252
  // never stack up behind a slow encode.
253
253
  function schedule({ fullClear = false } = {}) {
254
254
  if (fullClear) pendingFullClear = true;
@@ -290,7 +290,7 @@ export async function runViewer({ sourceId, manga, chapters, chapterIndex, start
290
290
  const prevRaw = stdin.isRaw;
291
291
  let onKey;
292
292
  // Ink leaves stdin unref'd after unmount, so a bare `await`-for-keypress won't
293
- // keep the process alive it would exit the moment the first page is drawn.
293
+ // keep the process alive - it would exit the moment the first page is drawn.
294
294
  // A ref'd timer holds the event loop open until we're done.
295
295
  const keepAlive = setInterval(() => {}, 1 << 30);
296
296
  // Re-render on terminal resize so the page tracks the window size. Full clear:
@@ -333,13 +333,13 @@ export async function runViewer({ sourceId, manga, chapters, chapterIndex, start
333
333
  } else {
334
334
  return; // ignore other keys without redrawing
335
335
  }
336
- // Update state synchronously, then coalesce the redraw rapid repeats
336
+ // Update state synchronously, then coalesce the redraw - rapid repeats
337
337
  // collapse into the fewest draws instead of being dropped mid-draw.
338
338
  inputSeq += 1;
339
339
  schedule();
340
340
  };
341
341
 
342
- // Enter raw mode *after* the first draw Ink's unmount restores cooked
342
+ // Enter raw mode *after* the first draw - Ink's unmount restores cooked
343
343
  // mode on a deferred tick, which would otherwise leave stdin line-buffered
344
344
  // (so single keypresses never arrive).
345
345
  stdin.removeAllListeners('data');
@@ -32,7 +32,7 @@ function readZipEntry(filePath, entryName) {
32
32
  return entry.getData();
33
33
  }
34
34
 
35
- // ---- RAR / CBR (node-unrar-js WASM, loaded lazily on first use) ----
35
+ // ---- RAR / CBR (node-unrar-js - WASM, loaded lazily on first use) ----
36
36
  let unrarPromise = null;
37
37
  const getUnrar = () => (unrarPromise ??= import('node-unrar-js'));
38
38
 
@@ -27,7 +27,7 @@ const cleanName = (name) => name.replace(/\.(cbz|zip|cbr|rar)$/i, '');
27
27
  const expandHome = (p) => (p.startsWith('~') ? path.join(os.homedir(), p.slice(1)) : p);
28
28
 
29
29
  // A directory is a manga. Its chapters are: image subfolders, then archive
30
- // files; or if neither the loose images in the folder become one chapter.
30
+ // files; or - if neither - the loose images in the folder become one chapter.
31
31
  function buildMangaFromDir(dirPath) {
32
32
  const entries = listDir(dirPath);
33
33
  const chapters = [];
@@ -7,7 +7,7 @@ import { logger } from '../../lib/logger.js';
7
7
  // MangaDex auth is OAuth2 "personal clients" (Keycloak). The user registers a
8
8
  // client at mangadex.org/settings, then we exchange client id/secret + their
9
9
  // username/password for a short-lived (15-min) access token and a rotating
10
- // refresh token. Reading manga needs none of this login only unlocks the
10
+ // refresh token. Reading manga needs none of this - login only unlocks the
11
11
  // user's follows/library and read-marker sync.
12
12
 
13
13
  // The access token lives in memory only; the refresh token is the durable
@@ -57,7 +57,7 @@ function applyTokens(creds, json) {
57
57
  token: json.access_token,
58
58
  expiresAt: Date.now() + (Number(json.expires_in) || 900) * 1000,
59
59
  };
60
- // The refresh token rotates on each use persist the latest one.
60
+ // The refresh token rotates on each use - persist the latest one.
61
61
  setCredentials({ ...creds, refreshToken: json.refresh_token || creds.refreshToken });
62
62
  }
63
63
 
@@ -101,10 +101,10 @@ export async function getAccessToken({ signal, force = false } = {}) {
101
101
  } catch (err) {
102
102
  // Only drop the saved session when the refresh token is DEFINITIVELY
103
103
  // dead (expired/revoked → invalid_grant). Transient or unexpected
104
- // failures keep the credentials so the next call or next launch can
104
+ // failures keep the credentials so the next call - or next launch - can
105
105
  // recover, instead of silently logging the user out.
106
106
  if (err.oauthError === 'invalid_grant') {
107
- logger.warn('MangaDex refresh token expired/revoked logging out', err);
107
+ logger.warn('MangaDex refresh token expired/revoked - logging out', err);
108
108
  logout();
109
109
  }
110
110
  throw err;
@@ -121,7 +121,7 @@ export async function loadPageBuffer(page, { signal } = {}) {
121
121
 
122
122
  // The signed-in user's followed manga, normalized like search results.
123
123
  export async function getFollows({ offset = 0, limit = MANGADEX.pageLimit, signal } = {}) {
124
- // /user/follows/manga rejects contentRating[] (HTTP 400) it returns ALL of
124
+ // /user/follows/manga rejects contentRating[] (HTTP 400) - it returns ALL of
125
125
  // the user's follows regardless of rating, which is what we want here anyway.
126
126
  const res = await mdGet('/user/follows/manga', {
127
127
  limit,
@@ -13,7 +13,7 @@ function readJson(file, fallback) {
13
13
 
14
14
  // While the app is uninstalling itself, every writer no-ops so a pending debounced
15
15
  // save (or the flushProgress() that quit() runs) can't recreate the data directory
16
- // we just deleted. Set once, never unset the process is on its way out.
16
+ // we just deleted. Set once, never unset - the process is on its way out.
17
17
  let persistenceDisabled = false;
18
18
  export function disablePersistence() {
19
19
  persistenceDisabled = true;
@@ -49,7 +49,7 @@ function loadProgress() {
49
49
  return progress;
50
50
  }
51
51
 
52
- // Debounced save the reader updates progress on every page turn, so coalesce.
52
+ // Debounced save - the reader updates progress on every page turn, so coalesce.
53
53
  let saveTimer = null;
54
54
  function scheduleSave() {
55
55
  if (persistenceDisabled) return;
package/src/uninstall.js CHANGED
@@ -6,7 +6,7 @@ import { disablePersistence } from './state/store.js';
6
6
 
7
7
  // Where install.sh put things. Mirrors the installer's own defaults + env
8
8
  // overrides (KOMADO_APP_DIR / KOMADO_BIN_DIR) so an in-app uninstall targets
9
- // exactly what was installed and never a dev checkout, which lives elsewhere.
9
+ // exactly what was installed - and never a dev checkout, which lives elsewhere.
10
10
  function appDir() {
11
11
  if (process.env.KOMADO_APP_DIR) return path.resolve(process.env.KOMADO_APP_DIR);
12
12
  const share = process.env.XDG_DATA_HOME || path.join(os.homedir(), '.local', 'share');
@@ -16,13 +16,13 @@ function launcherFile() {
16
16
  const bin = process.env.KOMADO_BIN_DIR
17
17
  ? path.resolve(process.env.KOMADO_BIN_DIR)
18
18
  : path.join(os.homedir(), '.local', 'bin');
19
- // The launcher FILE only never the whole bin dir, which is shared with other tools.
19
+ // The launcher FILE only - never the whole bin dir, which is shared with other tools.
20
20
  return path.join(bin, 'komado');
21
21
  }
22
22
 
23
23
  // The three things a full install creates. `paths.home` is the runtime data dir
24
24
  // (~/.komado or KOMADO_HOME) and holds config, reading progress, the MangaDex
25
- // login, the page cache, and the log all under one dir, so one delete clears it.
25
+ // login, the page cache, and the log - all under one dir, so one delete clears it.
26
26
  export function uninstallTargets() {
27
27
  return [
28
28
  { label: 'application files', path: appDir() },
@@ -47,7 +47,7 @@ function assertSafe(p) {
47
47
  }
48
48
  }
49
49
 
50
- // Remove the installed app, its launcher, and all runtime data. Synchronous a
50
+ // Remove the installed app, its launcher, and all runtime data. Synchronous - a
51
51
  // one-shot teardown run right before exit. It first switches off the store's
52
52
  // persistence so a pending debounced save (or the flushProgress() that quit()
53
53
  // runs) can't recreate the data dir we're deleting. One failing target never