@dunkinfrunkin/mdcat 0.1.0 → 0.1.1
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 +59 -108
- package/package.json +1 -1
- package/src/cli.js +91 -15
- package/src/tui.js +57 -12
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Frank Chan
|
|
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
CHANGED
|
@@ -1,141 +1,92 @@
|
|
|
1
|
-
# mdcat
|
|
1
|
+
# mdcat /\(o.o)/\
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@dunkinfrunkin/mdcat)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](package.json)
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
[](LICENSE)
|
|
7
|
+
**Terminal pager for Markdown.** Full colour, syntax highlighting, incremental search, mouse support — zero config.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## What it looks like
|
|
13
|
-
|
|
14
|
-
- **H1** headings render in a purple Unicode box
|
|
15
|
-
- **H2** headings render in bold blue with an underline bar
|
|
16
|
-
- **H3–H6** headings use green → yellow → cyan → dim
|
|
17
|
-
- **Code blocks** get a bordered box with a language tag and syntax highlighting
|
|
18
|
-
- **Blockquotes** get an amber `▌` bar with dim italic text
|
|
19
|
-
- **Lists** use `●` / `○` / `‣` bullets; task lists use `☑` / `☐`
|
|
20
|
-
- **Tables** render with full Unicode box-drawing borders
|
|
21
|
-
- **Links** are blue and underlined, clickable in iTerm2 / Kitty / WezTerm
|
|
22
|
-
|
|
23
|
-
The top chrome bar shows the filename and file type. The bottom bar shows key hints and scroll percentage. Press `/` to search — matching lines get a gold `▶` gutter marker.
|
|
9
|
+
```sh
|
|
10
|
+
npx @dunkinfrunkin/mdcat README.md
|
|
11
|
+
```
|
|
24
12
|
|
|
25
13
|
---
|
|
26
14
|
|
|
27
15
|
## Install
|
|
28
16
|
|
|
29
|
-
**Zero-install (recommended for one-off use):**
|
|
30
|
-
|
|
31
17
|
```sh
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
**Global install via npm:**
|
|
36
|
-
|
|
37
|
-
```sh
|
|
38
|
-
npm install -g mdcat
|
|
39
|
-
```
|
|
18
|
+
# Zero-install
|
|
19
|
+
npx @dunkinfrunkin/mdcat README.md
|
|
40
20
|
|
|
41
|
-
|
|
21
|
+
# Global
|
|
22
|
+
npm install -g @dunkinfrunkin/mdcat
|
|
42
23
|
|
|
43
|
-
|
|
24
|
+
# Homebrew
|
|
44
25
|
brew install frankchan/tap/mdcat
|
|
45
26
|
```
|
|
46
27
|
|
|
47
|
-
---
|
|
48
|
-
|
|
49
28
|
## Usage
|
|
50
29
|
|
|
51
30
|
```sh
|
|
52
|
-
#
|
|
53
|
-
mdcat README.md
|
|
54
|
-
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
# Read from curl
|
|
59
|
-
curl -s https://raw.githubusercontent.com/user/repo/main/README.md | mdcat
|
|
60
|
-
|
|
61
|
-
# Check version
|
|
62
|
-
mdcat --version
|
|
63
|
-
|
|
64
|
-
# Help
|
|
65
|
-
mdcat --help
|
|
31
|
+
mdcat README.md # open a file
|
|
32
|
+
mdcat --web README.md # render and open in browser
|
|
33
|
+
cat CHANGELOG.md | mdcat # pipe from stdin
|
|
34
|
+
curl -s https://… | mdcat # pipe from curl
|
|
35
|
+
mdcat --help # show help
|
|
36
|
+
mdcat --version # show version
|
|
66
37
|
```
|
|
67
38
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
|
73
|
-
|
|
74
|
-
| `
|
|
75
|
-
| `j` / `k`
|
|
76
|
-
|
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
| `
|
|
82
|
-
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
| Element | Rendering |
|
|
104
|
-
|------------------|------------------------------------------------|
|
|
105
|
-
| H1–H6 headings | Coloured by level; H1 gets a Unicode box |
|
|
106
|
-
| Paragraphs | Word-wrapped to terminal width |
|
|
107
|
-
| **Bold** | Bold white |
|
|
108
|
-
| _Italic_ | Italic |
|
|
109
|
-
| ~~Strikethrough~~| Strikethrough + dim |
|
|
110
|
-
| `inline code` | Amber text on dark background |
|
|
111
|
-
| Fenced code | Bordered box + syntax highlighting |
|
|
112
|
-
| Blockquotes | Amber `▌` bar with dim italic text |
|
|
113
|
-
| Unordered lists | `●` / `○` / `‣` bullets at increasing depth |
|
|
114
|
-
| Ordered lists | Cyan numbers |
|
|
115
|
-
| Task lists | `☑` / `☐` with green / dim colouring |
|
|
116
|
-
| Tables | Box-drawing borders; respects column alignment |
|
|
117
|
-
| Links | Blue underline + OSC 8 clickable |
|
|
118
|
-
| Images | `🖼 alt-text` in dim |
|
|
119
|
-
| Horizontal rules | Full-width dim `─` line |
|
|
120
|
-
| HTML entities | Decoded (`&`, `<`, `>`, …) |
|
|
121
|
-
|
|
122
|
-
---
|
|
39
|
+
## Keys
|
|
40
|
+
|
|
41
|
+
| Key | Action |
|
|
42
|
+
|-----|--------|
|
|
43
|
+
| `q` | Quit |
|
|
44
|
+
| `y` | Copy visible page to clipboard |
|
|
45
|
+
| `M` | Toggle mouse (off = free text selection) |
|
|
46
|
+
| `j` / `k` | Scroll down / up |
|
|
47
|
+
| `Space` / `b` | Page down / up |
|
|
48
|
+
| `d` / `u` | Half-page down / up |
|
|
49
|
+
| `g` / `G` | Top / bottom |
|
|
50
|
+
| `/` | Search |
|
|
51
|
+
| `n` / `N` | Next / previous match |
|
|
52
|
+
| `Esc` | Clear search |
|
|
53
|
+
| Mouse wheel | Scroll three lines |
|
|
54
|
+
|
|
55
|
+
## What it renders
|
|
56
|
+
|
|
57
|
+
| Element | Rendering |
|
|
58
|
+
|---------|-----------|
|
|
59
|
+
| H1 | Purple Unicode box |
|
|
60
|
+
| H2 | Bold blue with underline |
|
|
61
|
+
| H3–H6 | Green → yellow → cyan → dim |
|
|
62
|
+
| **Bold** / _italic_ / ~~strike~~ | Standard ANSI |
|
|
63
|
+
| `inline code` | Amber on dark background |
|
|
64
|
+
| Fenced code | Bordered box with syntax highlighting |
|
|
65
|
+
| Blockquotes | Amber `▌` bar, dim italic |
|
|
66
|
+
| Unordered lists | `●` / `○` / `‣` bullets |
|
|
67
|
+
| Ordered lists | Cyan numbers |
|
|
68
|
+
| Task lists | `☑` / `☐` with green / dim |
|
|
69
|
+
| Tables | Box-drawing borders, column alignment |
|
|
70
|
+
| Links | Blue underline, OSC 8 clickable |
|
|
71
|
+
| Images | `[alt text]` badge in dim |
|
|
72
|
+
| Horizontal rules | Full-width dim `─` line |
|
|
73
|
+
| HTML entities | Decoded (`&`, `<`, …) |
|
|
123
74
|
|
|
124
75
|
## Contributing
|
|
125
76
|
|
|
126
|
-
|
|
77
|
+
Bug reports and pull requests are welcome. Please open an issue first to discuss significant changes.
|
|
127
78
|
|
|
128
79
|
```sh
|
|
129
|
-
git clone https://github.com/
|
|
80
|
+
git clone https://github.com/dunkinfrunkin/mdcat
|
|
130
81
|
cd mdcat
|
|
131
82
|
npm install
|
|
132
83
|
npm test
|
|
133
84
|
```
|
|
134
85
|
|
|
135
|
-
|
|
86
|
+
All PRs must pass `npm test` (68 tests).
|
|
136
87
|
|
|
137
|
-
|
|
88
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for more details.
|
|
138
89
|
|
|
139
90
|
## License
|
|
140
91
|
|
|
141
|
-
MIT
|
|
92
|
+
[MIT](LICENSE) © Frank Chan
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -1,53 +1,129 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync } from "fs";
|
|
2
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
3
3
|
import { resolve, basename } from "path";
|
|
4
|
+
import { tmpdir } from "os";
|
|
5
|
+
import { execFileSync } from "child_process";
|
|
4
6
|
import { marked } from "marked";
|
|
5
7
|
import { renderTokens } from "./render.js";
|
|
6
8
|
import { launch } from "./tui.js";
|
|
7
9
|
|
|
8
10
|
marked.use({ gfm: true });
|
|
9
11
|
|
|
12
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
13
|
+
|
|
14
|
+
const E = "\x1B";
|
|
15
|
+
const dim = s => `${E}[2m${s}${E}[0m`;
|
|
16
|
+
const blue = s => `${E}[38;2;97;175;239m${s}${E}[0m`;
|
|
17
|
+
const gray = s => `${E}[38;2;92;99;112m${s}${E}[0m`;
|
|
18
|
+
const bold = s => `${E}[1m${s}${E}[0m`;
|
|
19
|
+
|
|
20
|
+
const CAT = ` ${gray("/\\")}${blue("(o.o)")}${gray("/\\")} `;
|
|
21
|
+
|
|
10
22
|
const args = process.argv.slice(2);
|
|
11
23
|
|
|
12
24
|
if (args[0] === "--help" || args[0] === "-h") {
|
|
13
|
-
console.log("
|
|
14
|
-
console.log("
|
|
15
|
-
console.log("
|
|
25
|
+
console.log(`\n${CAT} ${bold("mdcat")} ${dim(`v${pkg.version}`)}`);
|
|
26
|
+
console.log(`${dim(" markdown pager for your terminal")}\n`);
|
|
27
|
+
console.log(`${bold("Usage:")}`);
|
|
28
|
+
console.log(` mdcat ${dim("<file.md>")}`);
|
|
29
|
+
console.log(` mdcat ${dim("--web <file.md>")} ${dim("# open in browser")}`);
|
|
30
|
+
console.log(` cat file.md ${dim("|")} mdcat\n`);
|
|
31
|
+
console.log(`${bold("Keys:")}`);
|
|
32
|
+
console.log(` ${blue("/")} search ${blue("n/N")} next/prev match`);
|
|
33
|
+
console.log(` ${blue("j/k")} ${dim("↑↓")} scroll line ${blue("space/b")} page down/up`);
|
|
34
|
+
console.log(` ${blue("d/u")} half page ${blue("g/G")} top/bottom`);
|
|
35
|
+
console.log(` ${blue("q")} quit\n`);
|
|
16
36
|
process.exit(0);
|
|
17
37
|
}
|
|
18
38
|
|
|
19
39
|
if (args[0] === "--version" || args[0] === "-v") {
|
|
20
|
-
|
|
21
|
-
console.log(pkg.version);
|
|
40
|
+
console.log(`${CAT} ${bold("mdcat")} ${dim(`v${pkg.version}`)}`);
|
|
22
41
|
process.exit(0);
|
|
23
42
|
}
|
|
24
43
|
|
|
25
|
-
const MAX_COLS = 100;
|
|
44
|
+
const MAX_COLS = 100;
|
|
45
|
+
|
|
46
|
+
function openInBrowser(title, content) {
|
|
47
|
+
const html = marked.parse(content);
|
|
48
|
+
const page = `<!DOCTYPE html>
|
|
49
|
+
<html lang="en">
|
|
50
|
+
<head>
|
|
51
|
+
<meta charset="UTF-8">
|
|
52
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
53
|
+
<title>${title}</title>
|
|
54
|
+
<style>
|
|
55
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
56
|
+
body {
|
|
57
|
+
background: #282c34;
|
|
58
|
+
color: #abb2bf;
|
|
59
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
60
|
+
font-size: 16px;
|
|
61
|
+
line-height: 1.7;
|
|
62
|
+
padding: 2rem 1rem;
|
|
63
|
+
}
|
|
64
|
+
.wrap { max-width: 760px; margin: 0 auto; }
|
|
65
|
+
h1, h2, h3, h4, h5, h6 { color: #e5c07b; margin: 1.5rem 0 0.5rem; font-weight: 700; }
|
|
66
|
+
h1 { font-size: 2rem; color: #c678dd; border-bottom: 2px solid #3e4451; padding-bottom: 0.4rem; }
|
|
67
|
+
h2 { font-size: 1.4rem; color: #61afef; border-bottom: 1px solid #3e4451; padding-bottom: 0.3rem; }
|
|
68
|
+
h3 { font-size: 1.15rem; color: #98c379; }
|
|
69
|
+
p { margin: 0.75rem 0; }
|
|
70
|
+
a { color: #61afef; text-decoration: underline; }
|
|
71
|
+
code { background: #2c313a; color: #e5c07b; padding: 0.15em 0.4em; border-radius: 4px; font-size: 0.9em; font-family: 'JetBrains Mono', 'Fira Code', monospace; }
|
|
72
|
+
pre { background: #21252b; border: 1px solid #3e4451; border-radius: 8px; padding: 1rem 1.25rem; overflow-x: auto; margin: 1rem 0; }
|
|
73
|
+
pre code { background: none; color: #abb2bf; padding: 0; font-size: 0.875rem; }
|
|
74
|
+
blockquote { border-left: 3px solid #e5c07b; padding: 0.5rem 1rem; margin: 1rem 0; color: #5c6370; font-style: italic; }
|
|
75
|
+
ul, ol { padding-left: 1.5rem; margin: 0.75rem 0; }
|
|
76
|
+
li { margin: 0.25rem 0; }
|
|
77
|
+
table { border-collapse: collapse; width: 100%; margin: 1rem 0; }
|
|
78
|
+
th { background: #21252b; color: #61afef; padding: 0.5rem 0.75rem; border: 1px solid #3e4451; text-align: left; }
|
|
79
|
+
td { padding: 0.5rem 0.75rem; border: 1px solid #3e4451; }
|
|
80
|
+
tr:nth-child(even) { background: #2c313a; }
|
|
81
|
+
hr { border: none; border-top: 1px solid #3e4451; margin: 1.5rem 0; }
|
|
82
|
+
img { max-width: 100%; border-radius: 6px; }
|
|
83
|
+
</style>
|
|
84
|
+
</head>
|
|
85
|
+
<body><div class="wrap">${html}</div></body>
|
|
86
|
+
</html>`;
|
|
26
87
|
|
|
27
|
-
|
|
88
|
+
const tmp = `${tmpdir()}/mdcat-${Date.now()}.html`;
|
|
89
|
+
writeFileSync(tmp, page);
|
|
90
|
+
|
|
91
|
+
const opener = process.platform === "darwin" ? "open"
|
|
92
|
+
: process.platform === "win32" ? "start"
|
|
93
|
+
: "xdg-open";
|
|
94
|
+
try { execFileSync(opener, [tmp]); }
|
|
95
|
+
catch { console.error(`mdcat: could not open browser`); process.exit(1); }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function runTUI(title, content) {
|
|
28
99
|
const cols = Math.min(process.stdout.columns || 80, MAX_COLS);
|
|
29
100
|
const tokens = marked.lexer(content);
|
|
30
101
|
const lines = renderTokens(tokens, cols);
|
|
31
102
|
launch(title, lines);
|
|
32
103
|
}
|
|
33
104
|
|
|
34
|
-
//
|
|
35
|
-
|
|
105
|
+
// --web flag
|
|
106
|
+
const webMode = args[0] === "--web" || args[0] === "-w";
|
|
107
|
+
const fileArgs = webMode ? args.slice(1) : args;
|
|
108
|
+
|
|
109
|
+
// Piped input
|
|
110
|
+
if (!process.stdin.isTTY && fileArgs.length === 0) {
|
|
36
111
|
let input = "";
|
|
37
112
|
process.stdin.setEncoding("utf8");
|
|
38
113
|
process.stdin.on("data", (chunk) => (input += chunk));
|
|
39
|
-
process.stdin.on("end", () =>
|
|
40
|
-
} else if (
|
|
114
|
+
process.stdin.on("end", () => webMode ? openInBrowser("stdin", input) : runTUI("stdin", input));
|
|
115
|
+
} else if (fileArgs.length === 0) {
|
|
41
116
|
console.error("Usage: mdcat <file.md>");
|
|
42
117
|
process.exit(1);
|
|
43
118
|
} else {
|
|
44
|
-
const filePath = resolve(
|
|
119
|
+
const filePath = resolve(fileArgs[0]);
|
|
45
120
|
let content;
|
|
46
121
|
try {
|
|
47
122
|
content = readFileSync(filePath, "utf8");
|
|
48
123
|
} catch (err) {
|
|
49
|
-
console.error(`mdcat: ${
|
|
124
|
+
console.error(`mdcat: ${fileArgs[0]}: ${err.code === "ENOENT" ? "No such file" : err.message}`);
|
|
50
125
|
process.exit(1);
|
|
51
126
|
}
|
|
52
|
-
|
|
127
|
+
const title = basename(filePath);
|
|
128
|
+
webMode ? openInBrowser(title, content) : runTUI(title, content);
|
|
53
129
|
}
|
package/src/tui.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
|
|
1
3
|
const ESC = "\x1B";
|
|
2
4
|
const ALT_ON = `${ESC}[?1049h`;
|
|
3
5
|
const ALT_OFF = `${ESC}[?1049l`;
|
|
@@ -99,6 +101,15 @@ function highlightInLine(line, query, isCurrent) {
|
|
|
99
101
|
return out;
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
function copyText(text) {
|
|
105
|
+
// OSC 52 — works in iTerm2, Kitty, WezTerm, tmux (with allow-passthrough)
|
|
106
|
+
const b64 = Buffer.from(text).toString("base64");
|
|
107
|
+
process.stdout.write(`${ESC}]52;c;${b64}${ESC}\\`);
|
|
108
|
+
// pbcopy fallback for macOS
|
|
109
|
+
try { execFileSync("pbcopy", [], { input: text, stdio: ["pipe", "ignore", "ignore"] }); }
|
|
110
|
+
catch { /* not on macOS or pbcopy unavailable */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
102
113
|
export function launch(title, lines) {
|
|
103
114
|
// Viewport state
|
|
104
115
|
let offset = 0;
|
|
@@ -110,6 +121,11 @@ export function launch(title, lines) {
|
|
|
110
121
|
let matchSet = new Set(); // for O(1) gutter lookup
|
|
111
122
|
let matchIdx = 0;
|
|
112
123
|
|
|
124
|
+
// Mouse / clipboard state
|
|
125
|
+
let mouseEnabled = true;
|
|
126
|
+
let toast = ""; // brief status message
|
|
127
|
+
let toastTimer = null;
|
|
128
|
+
|
|
113
129
|
const rows = () => process.stdout.rows || 24;
|
|
114
130
|
const cols = () => process.stdout.columns || 80;
|
|
115
131
|
const visible = () => Math.max(1, rows() - 2); // top bar + bottom bar
|
|
@@ -174,14 +190,12 @@ export function launch(title, lines) {
|
|
|
174
190
|
// ─── Chrome helpers ────────────────────────────────────────────────────────
|
|
175
191
|
|
|
176
192
|
function topBar(title, w) {
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
const
|
|
180
|
-
const
|
|
181
|
-
const leftW = 1 + ext.length + 2 + 1 + title.length + 1;
|
|
182
|
-
const rightW = "mdcat ".length;
|
|
193
|
+
const left = ` ${C.bold}${C.titleFg}${title}${RESET}${C.chromeBg}`;
|
|
194
|
+
const cat = `${C.dimFg}/\\${RESET}${C.chromeBg}${C.accentFg}(o.o)${RESET}${C.chromeBg}${C.dimFg}/\\ mdcat ${RESET}`;
|
|
195
|
+
const leftW = 1 + title.length;
|
|
196
|
+
const rightW = "/\\(o.o)/\\ mdcat ".length;
|
|
183
197
|
const gap = Math.max(0, w - leftW - rightW);
|
|
184
|
-
return `${C.chromeBg}${left}${C.dimFg}${" ".repeat(gap)}${
|
|
198
|
+
return `${C.chromeBg}${left}${C.dimFg}${" ".repeat(gap)}${cat}${RESET}`;
|
|
185
199
|
}
|
|
186
200
|
|
|
187
201
|
function gutterFor(absLine) {
|
|
@@ -223,12 +237,32 @@ export function launch(title, lines) {
|
|
|
223
237
|
// Normal mode
|
|
224
238
|
const end = Math.min(offset + visible(), lines.length);
|
|
225
239
|
const pct = lines.length === 0 ? "100%" : Math.round((end / lines.length) * 100) + "%";
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
240
|
+
|
|
241
|
+
if (toast) {
|
|
242
|
+
const toastStr = `${C.greenFg} ✔ ${toast}${RESET}`;
|
|
243
|
+
const right = `${C.dimFg} ${pct} ${RESET}`;
|
|
244
|
+
const rightW = ` ${pct} `.length;
|
|
245
|
+
const gap = Math.max(0, w - (3 + toast.length) - rightW);
|
|
246
|
+
return `${C.chromeBg}${toastStr}${" ".repeat(gap)}${right}${RESET}`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const mouseHint = mouseEnabled ? "" : `${C.matchFg} [select mode]${RESET}`;
|
|
250
|
+
const mouseW = mouseEnabled ? 0 : " [select mode]".length;
|
|
251
|
+
const hints = `${C.dim} q y / j k ↑↓ space g G${RESET}`;
|
|
252
|
+
const right = `${C.dimFg} ${pct} ${RESET}`;
|
|
253
|
+
const hintsW = " q y / j k ↑↓ space g G".length;
|
|
229
254
|
const rightW = ` ${pct} `.length;
|
|
230
|
-
const gap = Math.max(0, w - hintsW - rightW);
|
|
231
|
-
return `${C.chromeBg}${hints}${" ".repeat(gap)}${right}${RESET}`;
|
|
255
|
+
const gap = Math.max(0, w - hintsW - mouseW - rightW);
|
|
256
|
+
return `${C.chromeBg}${hints}${mouseHint}${" ".repeat(gap)}${right}${RESET}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ─── Toast ─────────────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
function showToast(msg, ms = 1500) {
|
|
262
|
+
toast = msg;
|
|
263
|
+
draw();
|
|
264
|
+
if (toastTimer) clearTimeout(toastTimer);
|
|
265
|
+
toastTimer = setTimeout(() => { toast = ""; draw(); }, ms);
|
|
232
266
|
}
|
|
233
267
|
|
|
234
268
|
// ─── Cleanup ───────────────────────────────────────────────────────────────
|
|
@@ -302,6 +336,17 @@ export function launch(title, lines) {
|
|
|
302
336
|
case "g": offset = 0; break;
|
|
303
337
|
case "G": offset = maxOff(); break;
|
|
304
338
|
|
|
339
|
+
case "y": {
|
|
340
|
+
const text = lines.slice(offset, offset + visible()).map(plain).join("\n");
|
|
341
|
+
copyText(text);
|
|
342
|
+
showToast("Copied to clipboard"); return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
case "M":
|
|
346
|
+
mouseEnabled = !mouseEnabled;
|
|
347
|
+
process.stdout.write(mouseEnabled ? MOUSE_ON : MOUSE_OFF);
|
|
348
|
+
showToast(mouseEnabled ? "Mouse scroll on" : "Mouse off — select text freely"); return;
|
|
349
|
+
|
|
305
350
|
case "/": case "\x06": // Ctrl+F
|
|
306
351
|
mode = "search"; searchQuery = ""; matchLines = []; matchSet = new Set();
|
|
307
352
|
draw(); return;
|