tuimon 0.1.0 → 0.3.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/AI.md +191 -0
- package/README.md +240 -94
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +3 -0
- package/dist/browser.js.map +1 -1
- package/dist/cli.js +224 -3
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +106 -0
- package/dist/config.js.map +1 -0
- package/dist/fkeybar.d.ts.map +1 -1
- package/dist/fkeybar.js +13 -0
- package/dist/fkeybar.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +148 -18
- package/dist/index.js.map +1 -1
- package/dist/layout/generator-css.d.ts +3 -0
- package/dist/layout/generator-css.d.ts.map +1 -0
- package/dist/layout/generator-css.js +202 -0
- package/dist/layout/generator-css.js.map +1 -0
- package/dist/layout/generator-js.d.ts +3 -0
- package/dist/layout/generator-js.d.ts.map +1 -0
- package/dist/layout/generator-js.js +353 -0
- package/dist/layout/generator-js.js.map +1 -0
- package/dist/layout/generator.d.ts +4 -0
- package/dist/layout/generator.d.ts.map +1 -0
- package/dist/layout/generator.js +124 -0
- package/dist/layout/generator.js.map +1 -0
- package/dist/layout/theme.d.ts +4 -0
- package/dist/layout/theme.d.ts.map +1 -0
- package/dist/layout/theme.js +28 -0
- package/dist/layout/theme.js.map +1 -0
- package/dist/layout/types.d.ts +68 -0
- package/dist/layout/types.d.ts.map +1 -0
- package/dist/layout/types.js +3 -0
- package/dist/layout/types.js.map +1 -0
- package/dist/quick/auto-layout.d.ts +5 -0
- package/dist/quick/auto-layout.d.ts.map +1 -0
- package/dist/quick/auto-layout.js +262 -0
- package/dist/quick/auto-layout.js.map +1 -0
- package/dist/quick/db/detect.d.ts +16 -0
- package/dist/quick/db/detect.d.ts.map +1 -0
- package/dist/quick/db/detect.js +131 -0
- package/dist/quick/db/detect.js.map +1 -0
- package/dist/quick/db/mongo.d.ts +10 -0
- package/dist/quick/db/mongo.d.ts.map +1 -0
- package/dist/quick/db/mongo.js +81 -0
- package/dist/quick/db/mongo.js.map +1 -0
- package/dist/quick/db/mysql.d.ts +8 -0
- package/dist/quick/db/mysql.d.ts.map +1 -0
- package/dist/quick/db/mysql.js +43 -0
- package/dist/quick/db/mysql.js.map +1 -0
- package/dist/quick/db/postgres.d.ts +8 -0
- package/dist/quick/db/postgres.d.ts.map +1 -0
- package/dist/quick/db/postgres.js +47 -0
- package/dist/quick/db/postgres.js.map +1 -0
- package/dist/quick/db/sqlite.d.ts +8 -0
- package/dist/quick/db/sqlite.d.ts.map +1 -0
- package/dist/quick/db/sqlite.js +43 -0
- package/dist/quick/db/sqlite.js.map +1 -0
- package/dist/quick/db-mode.d.ts +13 -0
- package/dist/quick/db-mode.d.ts.map +1 -0
- package/dist/quick/db-mode.js +159 -0
- package/dist/quick/db-mode.js.map +1 -0
- package/dist/quick/detect.d.ts +4 -0
- package/dist/quick/detect.d.ts.map +1 -0
- package/dist/quick/detect.js +76 -0
- package/dist/quick/detect.js.map +1 -0
- package/dist/quick/file-mode.d.ts +5 -0
- package/dist/quick/file-mode.d.ts.map +1 -0
- package/dist/quick/file-mode.js +158 -0
- package/dist/quick/file-mode.js.map +1 -0
- package/dist/quick/parsers/csv.d.ts +3 -0
- package/dist/quick/parsers/csv.d.ts.map +1 -0
- package/dist/quick/parsers/csv.js +145 -0
- package/dist/quick/parsers/csv.js.map +1 -0
- package/dist/quick/parsers/detect-meta.d.ts +7 -0
- package/dist/quick/parsers/detect-meta.d.ts.map +1 -0
- package/dist/quick/parsers/detect-meta.js +35 -0
- package/dist/quick/parsers/detect-meta.js.map +1 -0
- package/dist/quick/parsers/json.d.ts +3 -0
- package/dist/quick/parsers/json.d.ts.map +1 -0
- package/dist/quick/parsers/json.js +74 -0
- package/dist/quick/parsers/json.js.map +1 -0
- package/dist/quick/parsers/log.d.ts +3 -0
- package/dist/quick/parsers/log.d.ts.map +1 -0
- package/dist/quick/parsers/log.js +185 -0
- package/dist/quick/parsers/log.js.map +1 -0
- package/dist/quick/parsers/modsec.d.ts +3 -0
- package/dist/quick/parsers/modsec.d.ts.map +1 -0
- package/dist/quick/parsers/modsec.js +338 -0
- package/dist/quick/parsers/modsec.js.map +1 -0
- package/dist/quick/presets/coverage.d.ts +3 -0
- package/dist/quick/presets/coverage.d.ts.map +1 -0
- package/dist/quick/presets/coverage.js +362 -0
- package/dist/quick/presets/coverage.js.map +1 -0
- package/dist/quick/presets/deps.d.ts +3 -0
- package/dist/quick/presets/deps.d.ts.map +1 -0
- package/dist/quick/presets/deps.js +194 -0
- package/dist/quick/presets/deps.js.map +1 -0
- package/dist/quick/presets/docker.d.ts +3 -0
- package/dist/quick/presets/docker.d.ts.map +1 -0
- package/dist/quick/presets/docker.js +100 -0
- package/dist/quick/presets/docker.js.map +1 -0
- package/dist/quick/presets/git.d.ts +3 -0
- package/dist/quick/presets/git.d.ts.map +1 -0
- package/dist/quick/presets/git.js +116 -0
- package/dist/quick/presets/git.js.map +1 -0
- package/dist/quick/presets/ps.d.ts +3 -0
- package/dist/quick/presets/ps.d.ts.map +1 -0
- package/dist/quick/presets/ps.js +168 -0
- package/dist/quick/presets/ps.js.map +1 -0
- package/dist/quick/presets/runner.d.ts +3 -0
- package/dist/quick/presets/runner.d.ts.map +1 -0
- package/dist/quick/presets/runner.js +20 -0
- package/dist/quick/presets/runner.js.map +1 -0
- package/dist/quick/presets/types.d.ts +7 -0
- package/dist/quick/presets/types.d.ts.map +1 -0
- package/dist/quick/presets/types.js +2 -0
- package/dist/quick/presets/types.js.map +1 -0
- package/dist/quick/types.d.ts +90 -0
- package/dist/quick/types.d.ts.map +1 -0
- package/dist/quick/types.js +3 -0
- package/dist/quick/types.js.map +1 -0
- package/dist/quick/watch-mode.d.ts +3 -0
- package/dist/quick/watch-mode.d.ts.map +1 -0
- package/dist/quick/watch-mode.js +156 -0
- package/dist/quick/watch-mode.js.map +1 -0
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +9 -2
- package/dist/types.d.ts.map +1 -1
- package/examples/demo.ts +134 -0
- package/examples/generate-access-log.ts +42 -0
- package/examples/screenshot-test.ts +105 -0
- package/examples/screenshot.png +0 -0
- package/frame-log.txt +830 -0
- package/package.json +10 -5
package/AI.md
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# TuiMon — AI Integration Guide
|
|
2
|
+
|
|
3
|
+
> Use this file when a user asks to display, monitor, or visualize data in a terminal.
|
|
4
|
+
> TuiMon renders beautiful dashboards with zero setup. Just install, send data, done.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g tuimon
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quickest Paths (pick one)
|
|
13
|
+
|
|
14
|
+
### Path 1: I have a data file
|
|
15
|
+
```bash
|
|
16
|
+
tuimon data.json # JSON array of objects → instant table + charts
|
|
17
|
+
tuimon users.csv # CSV → instant table + charts
|
|
18
|
+
tuimon access.log # Nginx log → request stats + browsable table
|
|
19
|
+
tuimon modsec.log # ModSecurity → security dashboard
|
|
20
|
+
```
|
|
21
|
+
That's it. No code needed. TuiMon auto-detects format and creates the dashboard.
|
|
22
|
+
|
|
23
|
+
### Path 2: I want live data from a function
|
|
24
|
+
Create one file:
|
|
25
|
+
```js
|
|
26
|
+
// monitor.js
|
|
27
|
+
module.exports = () => ({
|
|
28
|
+
cpu: require('os').cpus().reduce((a,c) => a + (100 - c.times.idle / Object.values(c.times).reduce((x,y)=>x+y,0) * 100), 0) / require('os').cpus().length | 0,
|
|
29
|
+
memory: ((1 - require('os').freemem() / require('os').totalmem()) * 100) | 0,
|
|
30
|
+
uptime: process.uptime() | 0,
|
|
31
|
+
})
|
|
32
|
+
```
|
|
33
|
+
```bash
|
|
34
|
+
tuimon watch monitor.js
|
|
35
|
+
```
|
|
36
|
+
TuiMon auto-detects data types and creates appropriate widgets. Refreshes every second.
|
|
37
|
+
|
|
38
|
+
### Path 3: I want to monitor an HTTP endpoint
|
|
39
|
+
```bash
|
|
40
|
+
tuimon watch --url http://localhost:3000/api/stats
|
|
41
|
+
```
|
|
42
|
+
The endpoint must return JSON. TuiMon polls it every second and auto-creates the dashboard.
|
|
43
|
+
|
|
44
|
+
### Path 4: I want a custom layout (still no HTML)
|
|
45
|
+
```js
|
|
46
|
+
// dashboard.js
|
|
47
|
+
const tuimon = require('tuimon')
|
|
48
|
+
|
|
49
|
+
tuimon.default.start({
|
|
50
|
+
pages: {
|
|
51
|
+
main: {
|
|
52
|
+
default: true,
|
|
53
|
+
layout: {
|
|
54
|
+
title: 'My Dashboard',
|
|
55
|
+
stats: [
|
|
56
|
+
{ id: 'users', label: 'Users', type: 'stat' },
|
|
57
|
+
{ id: 'cpu', label: 'CPU', type: 'gauge' },
|
|
58
|
+
],
|
|
59
|
+
panels: [
|
|
60
|
+
{ id: 'requests', label: 'Requests', type: 'line', span: 2 },
|
|
61
|
+
{ id: 'errors', label: 'Errors', type: 'event-log' },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
refresh: 1000,
|
|
67
|
+
data: () => ({
|
|
68
|
+
users: getUserCount(),
|
|
69
|
+
cpu: getCpuPercent(),
|
|
70
|
+
requests: { HTTP: getReqRate(), WS: getWsRate() },
|
|
71
|
+
errors: getRecentErrors(),
|
|
72
|
+
}),
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Data Format Rules
|
|
77
|
+
|
|
78
|
+
TuiMon accepts the **simplest possible data**. Just send what you have:
|
|
79
|
+
|
|
80
|
+
| You send | TuiMon shows |
|
|
81
|
+
|----------|-------------|
|
|
82
|
+
| `42` | Stat card with number |
|
|
83
|
+
| `73` (for id containing cpu/mem/disk) | Gauge (0-100%) |
|
|
84
|
+
| `"running"` | Stat card with text |
|
|
85
|
+
| `{ value: 42, trend: '+5', unit: 'req/s' }` | Stat card with trend |
|
|
86
|
+
| `{ Requests: 340, Errors: 12 }` | Line chart (auto-accumulates history over time) |
|
|
87
|
+
| `{ GET: 200, POST: 50 }` | Bar chart |
|
|
88
|
+
| `['Deploy completed', 'Scaled up']` | Event log (auto-timestamped) |
|
|
89
|
+
| `[{ text: 'Error', type: 'error' }]` | Event log (colored by type) |
|
|
90
|
+
| `['Node-1', 'Node-2']` | Status grid (all green) |
|
|
91
|
+
| `[{ label: 'DB', status: 'error' }]` | Status grid (colored dots) |
|
|
92
|
+
|
|
93
|
+
**Key insight for line charts:** You don't manage history. Each call to `render()` or each refresh sends CURRENT values. TuiMon accumulates the history automatically and builds the chart over time.
|
|
94
|
+
|
|
95
|
+
## Widget Types
|
|
96
|
+
|
|
97
|
+
| Type | Use for |
|
|
98
|
+
|------|---------|
|
|
99
|
+
| `stat` | Single number or text value |
|
|
100
|
+
| `gauge` | Percentage (0-100 with color bar) |
|
|
101
|
+
| `line` | Time series (auto-accumulates history) |
|
|
102
|
+
| `bar` | Categorical comparison |
|
|
103
|
+
| `doughnut` | Distribution/proportion |
|
|
104
|
+
| `event-log` | Scrolling list of events |
|
|
105
|
+
| `status-grid` | Health indicators (colored dots) |
|
|
106
|
+
| `table` | Tabular data with pagination |
|
|
107
|
+
|
|
108
|
+
## Per-Widget Throttle
|
|
109
|
+
|
|
110
|
+
Widgets can update at different speeds:
|
|
111
|
+
```js
|
|
112
|
+
panels: [
|
|
113
|
+
{ id: 'chart', label: 'Chart', type: 'line' }, // every frame
|
|
114
|
+
{ id: 'events', label: 'Events', type: 'event-log', throttle: 2000 }, // max every 2s
|
|
115
|
+
{ id: 'health', label: 'Health', type: 'status-grid', throttle: 5000 }, // max every 5s
|
|
116
|
+
]
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## VSCode Terminal
|
|
120
|
+
|
|
121
|
+
TuiMon works in VSCode's integrated terminal. Ensure this setting is enabled:
|
|
122
|
+
```json
|
|
123
|
+
{ "terminal.integrated.enableImages": true }
|
|
124
|
+
```
|
|
125
|
+
Running `tuimon init` sets this automatically.
|
|
126
|
+
|
|
127
|
+
## Common AI Use Cases
|
|
128
|
+
|
|
129
|
+
### "Show me my server stats"
|
|
130
|
+
```js
|
|
131
|
+
module.exports = () => ({
|
|
132
|
+
cpu: os.loadavg()[0],
|
|
133
|
+
memory: ((1 - os.freemem()/os.totalmem()) * 100) | 0,
|
|
134
|
+
uptime: process.uptime() | 0,
|
|
135
|
+
processes: execSync('ps aux | wc -l').toString().trim(),
|
|
136
|
+
})
|
|
137
|
+
```
|
|
138
|
+
```bash
|
|
139
|
+
tuimon watch server-stats.js
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### "Monitor my API"
|
|
143
|
+
```bash
|
|
144
|
+
tuimon watch --url http://localhost:3000/health
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### "Visualize this JSON data"
|
|
148
|
+
```bash
|
|
149
|
+
tuimon data.json
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### "Show me my nginx traffic"
|
|
153
|
+
```bash
|
|
154
|
+
tuimon /var/log/nginx/access.log
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### "Monitor my database"
|
|
158
|
+
```js
|
|
159
|
+
module.exports = async () => {
|
|
160
|
+
const pool = require('./db')
|
|
161
|
+
const { rows } = await pool.query('SELECT count(*) as c FROM users WHERE active = true')
|
|
162
|
+
return {
|
|
163
|
+
activeUsers: rows[0].c,
|
|
164
|
+
connections: pool.totalCount,
|
|
165
|
+
idle: pool.idleCount,
|
|
166
|
+
waiting: pool.waitingCount,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
```bash
|
|
171
|
+
tuimon watch db-monitor.js
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### "Show security events"
|
|
175
|
+
```bash
|
|
176
|
+
tuimon /var/log/modsec_audit.log
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## CLI Reference
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
tuimon <file> # Visualize JSON/CSV/log file
|
|
183
|
+
tuimon <file> -c "col1,col2" # Show specific columns only
|
|
184
|
+
tuimon watch <file.js> # Live data from JS module
|
|
185
|
+
tuimon watch --url <url> # Poll JSON endpoint
|
|
186
|
+
tuimon watch --url <url> --interval 5000 # Custom poll interval
|
|
187
|
+
tuimon start # Full config mode
|
|
188
|
+
tuimon init # Scaffold project + enable VSCode
|
|
189
|
+
tuimon check # Verify terminal graphics support
|
|
190
|
+
tuimon ai # Print this guide
|
|
191
|
+
```
|
package/README.md
CHANGED
|
@@ -1,152 +1,298 @@
|
|
|
1
1
|
# TuiMon
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Your HTML, CSS, and JavaScript, rendered directly in the terminal.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What Is TuiMon?
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|-----------|----------|------------|
|
|
9
|
-
| Kitty | Kitty | Supported |
|
|
10
|
-
| Ghostty | Kitty | Supported |
|
|
11
|
-
| WezTerm | Kitty | Supported |
|
|
12
|
-
| iTerm2 | iTerm2 | Supported |
|
|
13
|
-
| VSCode | Sixel | Supported* |
|
|
14
|
-
| mlterm | Sixel | Supported |
|
|
7
|
+
TuiMon takes any HTML page and renders it live in your terminal. Write your dashboard with HTML, CSS flexbox, Chart.js, D3, or whatever you already know, and TuiMon turns it into a real-time terminal application.
|
|
15
8
|
|
|
16
|
-
|
|
9
|
+
No curses. No blessed. No terminal UI framework. Just the web tech you already use.
|
|
17
10
|
|
|
18
|
-
## Quick Start
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
npx tuimon init
|
|
22
|
-
npx tuimon check
|
|
23
|
-
npx tuimon start
|
|
24
11
|
```
|
|
12
|
+
Your HTML/CSS/JS > Headless Chromium > Screenshot > Terminal Graphics > Your Terminal
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
If it works in a browser, it works in TuiMon.
|
|
25
16
|
|
|
26
|
-
|
|
17
|
+
But you don't have to write HTML if you don't want to. TuiMon also comes with a beautiful built-in theme and a set of zero-config CLI tools that let you visualize files, databases, and live data without writing a single line of HTML.
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
27
20
|
|
|
28
21
|
```bash
|
|
29
22
|
npm install -g tuimon
|
|
30
|
-
tuimon init
|
|
23
|
+
tuimon init # scaffolds a starter dashboard
|
|
24
|
+
tuimon start # renders it in your terminal
|
|
31
25
|
```
|
|
32
26
|
|
|
33
|
-
|
|
27
|
+
---
|
|
34
28
|
|
|
35
|
-
|
|
29
|
+
## 1. Build Your Own Dashboard with HTML
|
|
36
30
|
|
|
37
|
-
|
|
31
|
+
This is what TuiMon was built for. You write your dashboard as a normal HTML page. Use CSS flexbox, grid, animations, whatever. Use any charting library. TuiMon renders it in your terminal.
|
|
38
32
|
|
|
39
|
-
|
|
33
|
+
```html
|
|
34
|
+
<!-- pages/dashboard.html -->
|
|
35
|
+
<div style="display: flex; gap: 20px; padding: 20px; background: #0a0e1a; color: white; height: 100vh;">
|
|
36
|
+
<div style="flex: 1; background: #0f1629; border-radius: 8px; padding: 16px;">
|
|
37
|
+
<h3>CPU Usage</h3>
|
|
38
|
+
<canvas id="cpuChart"></canvas>
|
|
39
|
+
</div>
|
|
40
|
+
<div style="flex: 1; background: #0f1629; border-radius: 8px; padding: 16px;">
|
|
41
|
+
<h3>Memory</h3>
|
|
42
|
+
<div id="memValue" style="font-size: 48px; color: #00e5ff;">--</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
|
46
|
+
<script>
|
|
47
|
+
TuiMon.onUpdate(function(data) {
|
|
48
|
+
TuiMon.set('#memValue', data.memory + '%')
|
|
49
|
+
// update your charts, DOM, anything you want
|
|
50
|
+
})
|
|
51
|
+
</script>
|
|
52
|
+
```
|
|
40
53
|
|
|
41
54
|
```typescript
|
|
55
|
+
// tuimon.config.ts
|
|
56
|
+
import tuimon from 'tuimon'
|
|
57
|
+
|
|
42
58
|
const dash = await tuimon.start({
|
|
43
59
|
pages: {
|
|
44
|
-
|
|
45
|
-
html: './pages/
|
|
60
|
+
main: {
|
|
61
|
+
html: './pages/dashboard.html',
|
|
46
62
|
default: true,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
shortcut: 'g',
|
|
52
|
-
label: 'CPU Detail',
|
|
53
|
-
},
|
|
54
|
-
memory: {
|
|
55
|
-
html: './pages/memory-detail.html',
|
|
56
|
-
shortcut: 'm',
|
|
57
|
-
label: 'Memory',
|
|
63
|
+
keys: {
|
|
64
|
+
F5: { label: 'Refresh', action: async () => dash.render(await getData()) },
|
|
65
|
+
F10: { label: 'Quit', action: () => process.exit(0) },
|
|
66
|
+
},
|
|
58
67
|
},
|
|
59
68
|
},
|
|
69
|
+
refresh: 1000,
|
|
70
|
+
data: getData,
|
|
60
71
|
})
|
|
61
72
|
```
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
- Press **ESC** on a detail page to return to overview
|
|
65
|
-
- Press **ESC** on overview to show quit confirmation
|
|
66
|
-
- Press **Ctrl+C** anywhere to exit immediately
|
|
74
|
+
### Multiple Pages
|
|
67
75
|
|
|
68
|
-
|
|
76
|
+
You can define multiple HTML pages and let users switch between them with keyboard shortcuts. Press a letter to jump to a detail page, ESC to go back.
|
|
69
77
|
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
78
|
+
```typescript
|
|
79
|
+
pages: {
|
|
80
|
+
overview: { html: './pages/overview.html', default: true },
|
|
81
|
+
cpu: { html: './pages/cpu-detail.html', shortcut: 'g', label: 'CPU Detail' },
|
|
82
|
+
memory: { html: './pages/memory-detail.html', shortcut: 'm', label: 'Memory' },
|
|
83
|
+
}
|
|
74
84
|
```
|
|
75
85
|
|
|
76
|
-
|
|
86
|
+
### Client Library
|
|
87
|
+
|
|
88
|
+
TuiMon automatically injects a small client script into your HTML pages. You use it to receive data updates:
|
|
89
|
+
|
|
90
|
+
- `TuiMon.onUpdate(callback)` receives data whenever `dash.render(data)` is called
|
|
91
|
+
- `TuiMon.set(selector, value)` is a shortcut to update text content or styles
|
|
92
|
+
- `TuiMon.notify(message)` dispatches a notification event
|
|
77
93
|
|
|
78
|
-
|
|
94
|
+
### Shortcut Badges
|
|
95
|
+
|
|
96
|
+
Add `data-tm-key="g"` to any element and TuiMon automatically renders a `[G]` badge in the corner. Users immediately know they can press G to navigate there.
|
|
97
|
+
|
|
98
|
+
### F-Key Bar
|
|
99
|
+
|
|
100
|
+
Each page can define its own F-key bindings. The bar at the bottom of the terminal always shows the active page's keys.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 2. Use the Built-in Theme (No HTML Needed)
|
|
105
|
+
|
|
106
|
+
If you don't want to design anything, TuiMon comes with a built-in dark neon theme. Just define your widgets and push data. TuiMon generates the HTML for you behind the scenes.
|
|
79
107
|
|
|
80
108
|
```typescript
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
109
|
+
const dash = await tuimon.start({
|
|
110
|
+
pages: {
|
|
111
|
+
main: {
|
|
112
|
+
default: true,
|
|
113
|
+
layout: {
|
|
114
|
+
title: 'My App',
|
|
115
|
+
stats: [
|
|
116
|
+
{ id: 'users', label: 'Users Online', type: 'stat' },
|
|
117
|
+
{ id: 'cpu', label: 'CPU', type: 'gauge' },
|
|
118
|
+
],
|
|
119
|
+
panels: [
|
|
120
|
+
{ id: 'traffic', label: 'Traffic', type: 'line', span: 2 },
|
|
121
|
+
{ id: 'services', label: 'Services', type: 'doughnut' },
|
|
122
|
+
{ id: 'events', label: 'Events', type: 'event-log', throttle: 2000 },
|
|
123
|
+
{ id: 'health', label: 'Health', type: 'status-grid', throttle: 5000 },
|
|
124
|
+
],
|
|
125
|
+
},
|
|
88
126
|
},
|
|
89
127
|
},
|
|
90
|
-
|
|
128
|
+
refresh: 500,
|
|
129
|
+
data: () => ({
|
|
130
|
+
users: 42,
|
|
131
|
+
cpu: 73,
|
|
132
|
+
traffic: { Requests: 340, Errors: 12 },
|
|
133
|
+
services: { Web: 47, API: 27 },
|
|
134
|
+
events: ['Deploy completed'],
|
|
135
|
+
health: ['Node-1', 'Node-2'],
|
|
136
|
+
}),
|
|
137
|
+
})
|
|
91
138
|
```
|
|
92
139
|
|
|
93
|
-
|
|
140
|
+
### Widget Types
|
|
94
141
|
|
|
95
|
-
|
|
142
|
+
There are 8 built-in widget types: `stat`, `gauge`, `line`, `doughnut`, `bar`, `event-log`, `status-grid`, and `table`.
|
|
96
143
|
|
|
97
|
-
###
|
|
144
|
+
### Just Send Numbers
|
|
98
145
|
|
|
99
|
-
|
|
100
|
-
interface TuiMonOptions {
|
|
101
|
-
pages: Record<string, PageConfig>
|
|
102
|
-
data?: () => Record<string, unknown> | Promise<Record<string, unknown>>
|
|
103
|
-
refresh?: number // auto-render interval in ms
|
|
104
|
-
renderDelay?: number // delay after pushData before screenshot (default: 50)
|
|
105
|
-
}
|
|
146
|
+
You don't need to learn a data format. Just send the simplest thing and TuiMon figures it out:
|
|
106
147
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
148
|
+
| What you send | What TuiMon shows |
|
|
149
|
+
|---------------|-------------------|
|
|
150
|
+
| `42` | Stat card with the number |
|
|
151
|
+
| `73` (for something named cpu or mem) | Gauge bar showing 73% |
|
|
152
|
+
| `{ Requests: 340, Errors: 12 }` | Line chart that builds up over time |
|
|
153
|
+
| `['Deploy completed']` | Event log with automatic timestamps |
|
|
154
|
+
| `['Node-1', 'Node-2']` | Status grid with green dots |
|
|
114
155
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
156
|
+
For line charts, you don't manage history. Each render call sends the current values and TuiMon accumulates the history automatically.
|
|
157
|
+
|
|
158
|
+
### Per-Widget Throttle
|
|
159
|
+
|
|
160
|
+
Each widget can update at its own speed. Your charts can update every frame while your event log only updates every 2 seconds and your status grid every 5 seconds. Just add `throttle: 2000` to any widget config.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 3. Instant Visualization (Zero Setup)
|
|
120
165
|
|
|
121
|
-
|
|
166
|
+
Sometimes you just need to quickly look at some data. TuiMon can do that too.
|
|
122
167
|
|
|
123
|
-
|
|
168
|
+
### View a File
|
|
124
169
|
|
|
125
|
-
|
|
170
|
+
```bash
|
|
171
|
+
tuimon data.json # JSON array of objects, shown as table + charts
|
|
172
|
+
tuimon users.csv # CSV file, auto-detects delimiter
|
|
173
|
+
tuimon access.log # Nginx access log, shows request stats
|
|
174
|
+
tuimon modsec_audit.log # ModSecurity log, shows security dashboard
|
|
175
|
+
tuimon data.json -c "name,age" # Only show specific columns
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
TuiMon auto-detects the file format, picks the right widgets, and builds a dashboard. It also watches the file for changes so the dashboard updates if the file is modified.
|
|
126
179
|
|
|
127
|
-
|
|
180
|
+
Press D to switch to a full-screen data table. Use arrow keys to page through the data. ESC goes back.
|
|
128
181
|
|
|
129
|
-
|
|
182
|
+
### Watch Live Data
|
|
130
183
|
|
|
131
|
-
|
|
184
|
+
Create a JS file that exports a function returning your data:
|
|
132
185
|
|
|
133
|
-
```
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
186
|
+
```js
|
|
187
|
+
// metrics.js
|
|
188
|
+
const os = require('os')
|
|
189
|
+
module.exports = () => ({
|
|
190
|
+
cpu: getCpuPercent(),
|
|
191
|
+
memory: getMemPercent(),
|
|
192
|
+
uptime: process.uptime(),
|
|
138
193
|
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
tuimon watch metrics.js
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
TuiMon calls your function every second, auto-detects the data shape, and builds a dashboard.
|
|
201
|
+
|
|
202
|
+
You can also poll an HTTP endpoint that returns JSON:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
tuimon watch --url http://localhost:3000/metrics
|
|
206
|
+
tuimon watch --url http://localhost:3000/metrics --interval 5000
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### View a Database Table
|
|
210
|
+
|
|
211
|
+
If you are working on a project that already has a database driver installed and a connection string in `.env`, you can view your data directly:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
tuimon db users # view a table or collection
|
|
215
|
+
tuimon db users --watch # re-query every 2 seconds
|
|
216
|
+
tuimon db "SELECT * FROM orders" # run a custom query
|
|
217
|
+
tuimon db users --query '{"active":true}' # MongoDB filter
|
|
218
|
+
tuimon db users --env MY_DB_URI # use a specific env variable
|
|
219
|
+
tuimon db users -c "name,email,role" # only show these columns
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
TuiMon finds the database driver in your project's `node_modules/` folder and reads the connection string from your `.env` file. It supports MongoDB, PostgreSQL, MySQL, and SQLite.
|
|
223
|
+
|
|
224
|
+
No new dependencies are installed. TuiMon uses whatever driver your project already has.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Terminal Support
|
|
139
229
|
|
|
140
|
-
|
|
141
|
-
|
|
230
|
+
| Terminal | Protocol | Status |
|
|
231
|
+
|----------|----------|--------|
|
|
232
|
+
| Kitty | Kitty | Supported |
|
|
233
|
+
| Ghostty | Kitty | Supported |
|
|
234
|
+
| WezTerm | Kitty | Supported |
|
|
235
|
+
| iTerm2 | iTerm2 | Supported |
|
|
236
|
+
| VSCode | Sixel | Supported |
|
|
237
|
+
| mlterm | Sixel | Supported |
|
|
238
|
+
|
|
239
|
+
### VSCode
|
|
240
|
+
|
|
241
|
+
Running `tuimon init` automatically enables terminal images in VSCode. It creates a `.vscode/settings.json` file with:
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{ "terminal.integrated.enableImages": true }
|
|
142
245
|
```
|
|
143
246
|
|
|
247
|
+
If you are adding TuiMon to an existing project, add that setting manually or run `tuimon init`.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Global Config
|
|
252
|
+
|
|
253
|
+
You can set preferences once so you don't have to repeat them:
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
tuimon config db.envVar MONGODB_URI # which env var holds your DB connection
|
|
257
|
+
tuimon config db.defaultLimit 500 # how many rows to show by default
|
|
258
|
+
tuimon config refresh 250 # default refresh rate in ms
|
|
259
|
+
tuimon config # show current config
|
|
260
|
+
tuimon config --reset # reset everything to defaults
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Config is stored in `~/.tuimon/config.json`.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## CLI Reference
|
|
268
|
+
|
|
269
|
+
| Command | What it does |
|
|
270
|
+
|---------|--------------|
|
|
271
|
+
| `tuimon <file>` | Visualize a JSON, CSV, or log file |
|
|
272
|
+
| `tuimon watch <file.js>` | Live dashboard from a JS data module |
|
|
273
|
+
| `tuimon watch --url <url>` | Poll a JSON endpoint |
|
|
274
|
+
| `tuimon db <table or query>` | View a database table or run a query |
|
|
275
|
+
| `tuimon start` | Run a custom HTML dashboard from tuimon.config.ts |
|
|
276
|
+
| `tuimon init` | Scaffold a starter project and enable VSCode |
|
|
277
|
+
| `tuimon check` | Check if your terminal supports graphics |
|
|
278
|
+
| `tuimon config` | View or set global preferences |
|
|
279
|
+
| `tuimon ai` | Print the AI integration guide |
|
|
280
|
+
|
|
281
|
+
Set `TUIMON_DEBUG=1` to print per-frame timing to stderr.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## How It Works
|
|
286
|
+
|
|
287
|
+
TuiMon runs a headless Chromium browser via Playwright. Your HTML page loads in the browser. TuiMon pushes data into the page, takes a PNG screenshot, encodes it using the Kitty graphics protocol (or Sixel for terminals that need it), and writes it to stdout. The F-key bar and keyboard input run natively in the terminal.
|
|
288
|
+
|
|
289
|
+
A typical frame takes about 50ms from data push to pixels on screen.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
144
293
|
## Contributing
|
|
145
294
|
|
|
146
|
-
|
|
147
|
-
- Coverage thresholds: 80% lines, 80% functions, 75% branches
|
|
148
|
-
- `npm test` must pass before any PR
|
|
149
|
-
- Strict TypeScript — `strict: true`, no `any`
|
|
295
|
+
Tests first, implementation second. Coverage thresholds are 80% lines, 80% functions, and 75% branches. TypeScript is strict with no exceptions.
|
|
150
296
|
|
|
151
297
|
## License
|
|
152
298
|
|
package/dist/browser.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,wBAAsB,aAAa,CAAC,EAClC,GAAG,EACH,KAAK,EACL,MAAM,GACP,EAAE;IACD,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,wBAAsB,aAAa,CAAC,EAClC,GAAG,EACH,KAAK,EACL,MAAM,GACP,EAAE;IACD,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf,GAAG,OAAO,CAAC,aAAa,CAAC,CAkEzB"}
|
package/dist/browser.js
CHANGED
|
@@ -49,6 +49,9 @@ export async function createBrowser({ url, width, height, }) {
|
|
|
49
49
|
async resize(w, h) {
|
|
50
50
|
await page.setViewportSize({ width: w, height: h });
|
|
51
51
|
},
|
|
52
|
+
async evaluate(expression) {
|
|
53
|
+
await page.evaluate(expression);
|
|
54
|
+
},
|
|
52
55
|
async close() {
|
|
53
56
|
await browser.close();
|
|
54
57
|
},
|
package/dist/browser.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAGrC,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,EAClC,GAAG,EACH,KAAK,EACL,MAAM,GAKP;IACC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACzD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,EAAE,CAAA;IAC1C,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAA;IAEpC,MAAM,IAAI,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAA;IAC7C,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC,CAAA;IAEvD,IAAI,UAAU,GAAG,CAAC,CAAA;IAClB,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,KAAK,IAAI,EAAE;QAC1B,UAAU,EAAE,CAAA;QACZ,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;YACnB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,0DAA0D,CAAC,CAAA;YAChF,OAAM;QACR,CAAC;QACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,kCAAkC,UAAU,kBAAkB,CAAC,CAAA;QACpF,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,EAAE,CAAA;QACrB,CAAC;QAAC,MAAM,CAAC;YACP,yBAAyB;QAC3B,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,EAAE;QAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,uBAAuB,GAAG,CAAC,OAAO,IAAI,CAAC,CAAA;IAC9D,CAAC,CAAC,CAAA;IAEF,OAAO;QACL,KAAK,CAAC,UAAU;YACd,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;YAClD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACzB,CAAC;QAED,KAAK,CAAC,QAAQ,CAAC,IAA6B;YAC1C,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,CAA0B,EAAE,EAAE;gBACjD,MAAM,GAAG,GAAG,UAAqC,CAAA;gBACjD,IAAI,OAAO,GAAG,CAAC,mBAAmB,CAAC,KAAK,UAAU,EAAE,CAAC;oBACnD,CAAC;oBAAC,GAAG,CAAC,mBAAmB,CAA0C,CAAC,CAAC,CAAC,CAAA;gBACxE,CAAC;YACH,CAAC,EAAE,IAAI,CAAC,CAAA;YACR,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,eAAe,CACxB,GAAG,EAAE,CAAE,UAAsC,CAAC,kBAAkB,CAAC,KAAK,IAAI,EAC1E,EAAE,OAAO,EAAE,IAAI,EAAE,CAClB,CAAA;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,kEAAkE;YACpE,CAAC;QACH,CAAC;QAED,KAAK,CAAC,QAAQ,CAAC,MAAc;YAC3B,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC,CAAA;QAC5D,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,CAAS,EAAE,CAAS;YAC/B,MAAM,IAAI,CAAC,eAAe,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAA;QACrD,CAAC;QAED,KAAK,CAAC,KAAK;YACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAA;QACvB,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
|