neo-vtop 1.0.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/.github/workflows/ci.yml +20 -0
- package/.husky/pre-commit +1 -0
- package/.npm_ignore +1 -0
- package/.prettierrc +5 -0
- package/CHANGELOG.md +81 -0
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/app.ts +503 -0
- package/bin/vtop.ts +2 -0
- package/docs/example.gif +0 -0
- package/eslint.config.js +18 -0
- package/package.json +57 -0
- package/sensors/cpu.ts +33 -0
- package/sensors/memory.ts +66 -0
- package/sensors/process.ts +96 -0
- package/themes/acid.json +34 -0
- package/themes/acid.png +0 -0
- package/themes/becca.json +35 -0
- package/themes/becca.png +0 -0
- package/themes/brew.json +34 -0
- package/themes/brew.png +0 -0
- package/themes/certs.json +33 -0
- package/themes/certs.png +0 -0
- package/themes/dark.json +34 -0
- package/themes/dark.png +0 -0
- package/themes/gooey.json +35 -0
- package/themes/gruvbox.json +33 -0
- package/themes/monokai.json +33 -0
- package/themes/monokai.png +0 -0
- package/themes/nord.json +35 -0
- package/themes/nord.png +0 -0
- package/themes/parallax.json +33 -0
- package/themes/seti.json +33 -0
- package/themes/wizard.json +34 -0
- package/themes/wizard.png +0 -0
- package/tsconfig.json +10 -0
- package/types/index.ts +23 -0
package/app.ts
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import Canvas from 'drawille'
|
|
2
|
+
import blessed from 'neo-blessed'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import childProcess from 'child_process'
|
|
5
|
+
import { Command } from 'commander'
|
|
6
|
+
import { glob } from 'glob'
|
|
7
|
+
import path from 'path'
|
|
8
|
+
import { fileURLToPath } from 'url'
|
|
9
|
+
import { createRequire } from 'module'
|
|
10
|
+
import cpuPlugin from './sensors/cpu'
|
|
11
|
+
import memoryPlugin from './sensors/memory'
|
|
12
|
+
import processPlugin from './sensors/process'
|
|
13
|
+
|
|
14
|
+
process.env.TERM = 'xterm-256color'
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url)
|
|
17
|
+
const { version: VERSION } = require('./package.json')
|
|
18
|
+
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
20
|
+
const __dirname = path.dirname(__filename)
|
|
21
|
+
|
|
22
|
+
const App = (() => {
|
|
23
|
+
const cli = new Command()
|
|
24
|
+
let themes = ''
|
|
25
|
+
let program = blessed.program()
|
|
26
|
+
|
|
27
|
+
const files = glob.sync(path.join(__dirname, 'themes', '*.json'))
|
|
28
|
+
for (let i = 0; i < files.length; i++) {
|
|
29
|
+
const themeName = files[i]
|
|
30
|
+
.replace(path.join(__dirname, 'themes') + path.sep, '')
|
|
31
|
+
.replace('.json', '')
|
|
32
|
+
themes += `${themeName}|`
|
|
33
|
+
}
|
|
34
|
+
themes = themes.slice(0, -1)
|
|
35
|
+
|
|
36
|
+
cli
|
|
37
|
+
.option('-t, --theme [name]', `set the neo vtop theme [${themes}]`, 'parallax')
|
|
38
|
+
.option('--no-mouse', 'Disables mouse interactivity')
|
|
39
|
+
.option('--quit-after [seconds]', 'Quits neo vtop after interval', '0')
|
|
40
|
+
.option('--update-interval [milliseconds]', 'Interval between updates', '300')
|
|
41
|
+
.version(VERSION)
|
|
42
|
+
.parse(process.argv)
|
|
43
|
+
|
|
44
|
+
interface ChartData {
|
|
45
|
+
chart: any
|
|
46
|
+
height: number
|
|
47
|
+
width: number
|
|
48
|
+
plugin: any
|
|
49
|
+
values: Record<number, number>
|
|
50
|
+
plugins: any
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface LoadedTheme {
|
|
54
|
+
title: { fg: string }
|
|
55
|
+
footer: { fg: string }
|
|
56
|
+
[key: string]: any
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let screen: any
|
|
60
|
+
const charts: Record<string, ChartData> = {}
|
|
61
|
+
let loadedTheme: LoadedTheme
|
|
62
|
+
const intervals: NodeJS.Timeout[] = []
|
|
63
|
+
|
|
64
|
+
let disableTableUpdate = false
|
|
65
|
+
let disableTableUpdateTimeout: NodeJS.Timeout = setTimeout(() => {}, 0)
|
|
66
|
+
let graphScale = 1
|
|
67
|
+
let position = 0
|
|
68
|
+
|
|
69
|
+
const size = {
|
|
70
|
+
pixel: { width: 0, height: 0 },
|
|
71
|
+
character: { width: 0, height: 0 },
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let graph: any
|
|
75
|
+
let graph2: any
|
|
76
|
+
let processList: any
|
|
77
|
+
let processListSelection: any
|
|
78
|
+
const sensorMap: Record<string, any> = {
|
|
79
|
+
cpu: cpuPlugin,
|
|
80
|
+
memory: memoryPlugin,
|
|
81
|
+
process: processPlugin,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const drawHeader = () => {
|
|
85
|
+
const headerText = ` {bold}neo vtop{/bold}{white-fg} for ${os.hostname()} `
|
|
86
|
+
const headerTextNoTags = ` neo vtop for ${os.hostname()} `
|
|
87
|
+
|
|
88
|
+
const header = blessed.text({
|
|
89
|
+
top: 'top',
|
|
90
|
+
left: 'left',
|
|
91
|
+
width: headerTextNoTags.length,
|
|
92
|
+
height: '1',
|
|
93
|
+
fg: loadedTheme.title.fg,
|
|
94
|
+
content: headerText,
|
|
95
|
+
tags: true,
|
|
96
|
+
})
|
|
97
|
+
const date = blessed.text({
|
|
98
|
+
top: 'top',
|
|
99
|
+
right: 0,
|
|
100
|
+
width: 9,
|
|
101
|
+
height: '1',
|
|
102
|
+
align: 'right',
|
|
103
|
+
content: '',
|
|
104
|
+
tags: true,
|
|
105
|
+
})
|
|
106
|
+
const loadAverage = blessed.text({
|
|
107
|
+
top: 'top',
|
|
108
|
+
height: '1',
|
|
109
|
+
align: 'center',
|
|
110
|
+
content: '',
|
|
111
|
+
tags: true,
|
|
112
|
+
left: Math.floor(program.cols / 2 - 28 / 2),
|
|
113
|
+
})
|
|
114
|
+
screen.append(header)
|
|
115
|
+
screen.append(date)
|
|
116
|
+
screen.append(loadAverage)
|
|
117
|
+
|
|
118
|
+
const zeroPad = (input: number) => `0${input}`.slice(-2)
|
|
119
|
+
|
|
120
|
+
const updateTime = () => {
|
|
121
|
+
const time = new Date()
|
|
122
|
+
date.setContent(
|
|
123
|
+
`${zeroPad(time.getHours())}:${zeroPad(time.getMinutes())}:${zeroPad(time.getSeconds())} `,
|
|
124
|
+
)
|
|
125
|
+
screen.render()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const updateLoadAverage = () => {
|
|
129
|
+
const avg = os.loadavg()
|
|
130
|
+
loadAverage.setContent(
|
|
131
|
+
`Load Average: ${avg[0].toFixed(2)} ${avg[1].toFixed(2)} ${avg[2].toFixed(2)}`,
|
|
132
|
+
)
|
|
133
|
+
screen.render()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
updateTime()
|
|
137
|
+
updateLoadAverage()
|
|
138
|
+
setInterval(updateTime, 1000)
|
|
139
|
+
setInterval(updateLoadAverage, 1000)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const drawFooter = () => {
|
|
143
|
+
const commands: Record<string, string> = {
|
|
144
|
+
dd: 'Kill process',
|
|
145
|
+
j: 'Down',
|
|
146
|
+
k: 'Up',
|
|
147
|
+
g: 'Jump to top',
|
|
148
|
+
G: 'Jump to bottom',
|
|
149
|
+
c: 'Sort by CPU',
|
|
150
|
+
m: 'Sort by Mem',
|
|
151
|
+
}
|
|
152
|
+
let text = ''
|
|
153
|
+
for (const c in commands) {
|
|
154
|
+
text += ` {white-bg}{black-fg}${c}{/black-fg}{/white-bg} ${commands[c]}`
|
|
155
|
+
}
|
|
156
|
+
text += '{|}https://github.com/nexusocean8/neo-vtop'
|
|
157
|
+
const footerRight = blessed.box({
|
|
158
|
+
width: '100%',
|
|
159
|
+
top: program.rows - 1,
|
|
160
|
+
tags: true,
|
|
161
|
+
fg: loadedTheme.footer.fg,
|
|
162
|
+
})
|
|
163
|
+
footerRight.setContent(text)
|
|
164
|
+
screen.append(footerRight)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const stringRepeat = (string: string, num: number): string => {
|
|
168
|
+
if (num < 0) return ''
|
|
169
|
+
return new Array(num + 1).join(string)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const drawChart = (chartKey: string) => {
|
|
173
|
+
const chart = charts[chartKey]
|
|
174
|
+
const c = chart.chart
|
|
175
|
+
c.clear()
|
|
176
|
+
|
|
177
|
+
if (!chart.plugin.initialized) return false
|
|
178
|
+
|
|
179
|
+
const dataPointsToKeep = 5000
|
|
180
|
+
charts[chartKey].values[position] = chart.plugin.currentValue
|
|
181
|
+
|
|
182
|
+
const computeValue = (input: number) =>
|
|
183
|
+
chart.height - Math.floor(((chart.height + 1) / 100) * input) - 1
|
|
184
|
+
|
|
185
|
+
if (position > dataPointsToKeep) {
|
|
186
|
+
delete charts[chartKey].values[position - dataPointsToKeep]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
for (const pos in charts[chartKey].values) {
|
|
190
|
+
const posInt = parseInt(pos, 10)
|
|
191
|
+
|
|
192
|
+
if (graphScale >= 1 || (graphScale < 1 && posInt % (1 / graphScale) === 0)) {
|
|
193
|
+
const p = posInt + (chart.width - Object.keys(charts[chartKey].values).length)
|
|
194
|
+
const x = p * graphScale + (1 - graphScale) * chart.width
|
|
195
|
+
|
|
196
|
+
if (p > 1 && computeValue(charts[chartKey].values[posInt - 1]) > 0) {
|
|
197
|
+
c.set(x, computeValue(charts[chartKey].values[posInt - 1]))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
for (let y = computeValue(charts[chartKey].values[posInt - 1]); y < chart.height; y++) {
|
|
201
|
+
if (graphScale > 1 && p > 0 && y > 0) {
|
|
202
|
+
const current = computeValue(charts[chartKey].values[posInt - 1])
|
|
203
|
+
const next = computeValue(charts[chartKey].values[posInt])
|
|
204
|
+
const diff = (next - current) / graphScale
|
|
205
|
+
|
|
206
|
+
for (let i = 0; i < graphScale; i++) {
|
|
207
|
+
c.set(x + i, y + diff * i)
|
|
208
|
+
for (let j = y + diff * i; j < chart.height; j++) {
|
|
209
|
+
c.set(x + i, j)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} else if (graphScale <= 1) {
|
|
213
|
+
c.set(x, y)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const textOutput = c.frame().split('\n')
|
|
220
|
+
const percent = ` ${chart.plugin.currentValue}`
|
|
221
|
+
textOutput[0] = `${textOutput[0].slice(0, textOutput[0].length - 4)}{white-fg}${percent.slice(-3)}%{/white-fg}`
|
|
222
|
+
|
|
223
|
+
return textOutput.join('\n')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const drawTable = (chartKey: string) => {
|
|
227
|
+
const chart = charts[chartKey]
|
|
228
|
+
const columnLengths: Record<string, number> = {}
|
|
229
|
+
const columns: string[] = chart.plugin.columns.slice(0)
|
|
230
|
+
columns.reverse()
|
|
231
|
+
let removeColumn = false
|
|
232
|
+
const lastItem = columns[columns.length - 1]
|
|
233
|
+
|
|
234
|
+
const minimumWidth = 12
|
|
235
|
+
let padding = 1
|
|
236
|
+
if (chart.width > 50) padding = 2
|
|
237
|
+
if (chart.width > 80) padding = 3
|
|
238
|
+
|
|
239
|
+
do {
|
|
240
|
+
let totalUsed = 0
|
|
241
|
+
let firstLength = 0
|
|
242
|
+
for (const column in columns) {
|
|
243
|
+
const item = columns[column]
|
|
244
|
+
if (item === lastItem) {
|
|
245
|
+
columnLengths[item] = chart.width - totalUsed
|
|
246
|
+
firstLength = columnLengths[item]
|
|
247
|
+
} else {
|
|
248
|
+
columnLengths[item] = item.length + padding
|
|
249
|
+
}
|
|
250
|
+
totalUsed += columnLengths[item]
|
|
251
|
+
}
|
|
252
|
+
if (firstLength < minimumWidth && columns.length > 1) {
|
|
253
|
+
columns.shift()
|
|
254
|
+
removeColumn = true
|
|
255
|
+
} else {
|
|
256
|
+
removeColumn = false
|
|
257
|
+
}
|
|
258
|
+
} while (removeColumn)
|
|
259
|
+
|
|
260
|
+
columns.reverse()
|
|
261
|
+
let titleOutput = '{bold}'
|
|
262
|
+
for (const headerColumn in columns) {
|
|
263
|
+
const colText = ` ${columns[headerColumn]}`
|
|
264
|
+
titleOutput +=
|
|
265
|
+
colText + stringRepeat(' ', columnLengths[columns[headerColumn]] - colText.length)
|
|
266
|
+
}
|
|
267
|
+
titleOutput += '{/bold}\n'
|
|
268
|
+
|
|
269
|
+
const bodyOutput: string[] = []
|
|
270
|
+
for (const row in chart.plugin.currentValue) {
|
|
271
|
+
const currentRow = chart.plugin.currentValue[row]
|
|
272
|
+
let rowText = ''
|
|
273
|
+
for (const bodyColumn in columns) {
|
|
274
|
+
const colText = ` ${currentRow[columns[bodyColumn]]}`
|
|
275
|
+
rowText += (
|
|
276
|
+
colText + stringRepeat(' ', columnLengths[columns[bodyColumn]] - colText.length)
|
|
277
|
+
).slice(0, columnLengths[columns[bodyColumn]])
|
|
278
|
+
}
|
|
279
|
+
bodyOutput.push(rowText)
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
title: titleOutput,
|
|
283
|
+
body: bodyOutput,
|
|
284
|
+
processWidth: columnLengths[columns[0]],
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let currentItems: string[] = []
|
|
289
|
+
let processWidth = 0
|
|
290
|
+
|
|
291
|
+
const draw = () => {
|
|
292
|
+
position++
|
|
293
|
+
|
|
294
|
+
graph.setContent(drawChart('0'))
|
|
295
|
+
graph2.setContent(drawChart('1'))
|
|
296
|
+
|
|
297
|
+
if (!disableTableUpdate) {
|
|
298
|
+
const table = drawTable('2')
|
|
299
|
+
processList.setContent(table.title)
|
|
300
|
+
|
|
301
|
+
const existingStats: Record<string, string> = {}
|
|
302
|
+
for (const stat in currentItems) {
|
|
303
|
+
const thisStat = currentItems[stat]
|
|
304
|
+
existingStats[thisStat.slice(0, table.processWidth)] = thisStat
|
|
305
|
+
}
|
|
306
|
+
processWidth = table.processWidth
|
|
307
|
+
|
|
308
|
+
processListSelection.setItems(table.body)
|
|
309
|
+
processListSelection.focus()
|
|
310
|
+
currentItems = table.body
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
screen.render()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
init() {
|
|
318
|
+
const theme = (process as any).theme ?? cli.opts().theme
|
|
319
|
+
|
|
320
|
+
if (cli.opts()['quitAfter'] !== '0') {
|
|
321
|
+
setTimeout(() => process.exit(0), parseInt(cli.opts()['quitAfter'], 10) * 1000)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
loadedTheme = require(`./themes/${theme}.json`)
|
|
326
|
+
} catch {
|
|
327
|
+
console.log(`The theme '${theme}' does not exist.`)
|
|
328
|
+
process.exit(1)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
screen = blessed.screen()
|
|
332
|
+
|
|
333
|
+
let lastKey = ''
|
|
334
|
+
|
|
335
|
+
screen.on('keypress', (ch: any, key: any) => {
|
|
336
|
+
if (['up', 'down', 'k', 'j'].includes(key.name)) {
|
|
337
|
+
disableTableUpdate = true
|
|
338
|
+
clearTimeout(disableTableUpdateTimeout)
|
|
339
|
+
disableTableUpdateTimeout = setTimeout(() => {
|
|
340
|
+
disableTableUpdate = false
|
|
341
|
+
}, 1000)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (key.name === 'q' || key.name === 'escape' || (key.name === 'c' && key.ctrl)) {
|
|
345
|
+
return process.exit(0)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (lastKey === 'd' && key.name === 'd') {
|
|
349
|
+
let selectedProcess = processListSelection.getItem(processListSelection.selected).content
|
|
350
|
+
selectedProcess = selectedProcess.slice(0, processWidth).trim()
|
|
351
|
+
childProcess.exec(`killall "${selectedProcess}"`, () => {})
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (key.name === 'c' && charts['2'].plugin.sort !== 'cpu') {
|
|
355
|
+
charts['2'].plugin.sort = 'cpu'
|
|
356
|
+
charts['2'].plugin.poll()
|
|
357
|
+
setTimeout(() => processListSelection.select(0), 200)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (key.name === 'm' && charts['2'].plugin.sort !== 'mem') {
|
|
361
|
+
charts['2'].plugin.sort = 'mem'
|
|
362
|
+
charts['2'].plugin.poll()
|
|
363
|
+
setTimeout(() => processListSelection.select(0), 200)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if ((key.name === 'left' || key.name === 'h') && graphScale < 8) {
|
|
367
|
+
graphScale *= 2
|
|
368
|
+
} else if ((key.name === 'right' || key.name === 'l') && graphScale > 0.125) {
|
|
369
|
+
graphScale /= 2
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
lastKey = key.name
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
drawHeader()
|
|
376
|
+
drawFooter()
|
|
377
|
+
|
|
378
|
+
graph = blessed.box({
|
|
379
|
+
top: 1,
|
|
380
|
+
left: 'left',
|
|
381
|
+
width: '100%',
|
|
382
|
+
height: '50%',
|
|
383
|
+
content: '',
|
|
384
|
+
fg: loadedTheme.chart.fg,
|
|
385
|
+
tags: true,
|
|
386
|
+
border: loadedTheme.chart.border,
|
|
387
|
+
})
|
|
388
|
+
screen.append(graph)
|
|
389
|
+
|
|
390
|
+
let graph2appended = false
|
|
391
|
+
|
|
392
|
+
const createBottom = () => {
|
|
393
|
+
if (graph2appended) {
|
|
394
|
+
screen.remove(graph2)
|
|
395
|
+
screen.remove(processList)
|
|
396
|
+
}
|
|
397
|
+
graph2appended = true
|
|
398
|
+
|
|
399
|
+
graph2 = blessed.box({
|
|
400
|
+
top: graph.height + 1,
|
|
401
|
+
left: 'left',
|
|
402
|
+
width: '50%',
|
|
403
|
+
height: graph.height - 2,
|
|
404
|
+
content: '',
|
|
405
|
+
fg: loadedTheme.chart.fg,
|
|
406
|
+
tags: true,
|
|
407
|
+
border: loadedTheme.chart.border,
|
|
408
|
+
})
|
|
409
|
+
screen.append(graph2)
|
|
410
|
+
|
|
411
|
+
processList = blessed.box({
|
|
412
|
+
top: graph.height + 1,
|
|
413
|
+
left: '50%',
|
|
414
|
+
width: screen.width - graph2.width,
|
|
415
|
+
height: graph.height - 2,
|
|
416
|
+
keys: true,
|
|
417
|
+
mouse: cli.opts().mouse,
|
|
418
|
+
fg: loadedTheme.table.fg,
|
|
419
|
+
tags: true,
|
|
420
|
+
border: loadedTheme.table.border,
|
|
421
|
+
})
|
|
422
|
+
screen.append(processList)
|
|
423
|
+
|
|
424
|
+
processListSelection = blessed.list({
|
|
425
|
+
height: processList.height - 3,
|
|
426
|
+
top: 1,
|
|
427
|
+
width: processList.width - 2,
|
|
428
|
+
left: 0,
|
|
429
|
+
keys: true,
|
|
430
|
+
vi: true,
|
|
431
|
+
style: loadedTheme.table.items,
|
|
432
|
+
mouse: cli.opts().mouse,
|
|
433
|
+
})
|
|
434
|
+
processList.append(processListSelection)
|
|
435
|
+
processListSelection.focus()
|
|
436
|
+
screen.render()
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
screen.on('resize', createBottom)
|
|
440
|
+
createBottom()
|
|
441
|
+
screen.append(graph)
|
|
442
|
+
screen.append(processList)
|
|
443
|
+
screen.render()
|
|
444
|
+
|
|
445
|
+
const plugins = ['cpu', 'memory', 'process']
|
|
446
|
+
|
|
447
|
+
const setupCharts = () => {
|
|
448
|
+
size.pixel.width = (graph.width - 2) * 2
|
|
449
|
+
size.pixel.height = (graph.height - 2) * 4
|
|
450
|
+
|
|
451
|
+
plugins.forEach((plugin, index) => {
|
|
452
|
+
let width: number
|
|
453
|
+
let height: number
|
|
454
|
+
let currentCanvas: any
|
|
455
|
+
|
|
456
|
+
switch (plugin) {
|
|
457
|
+
case 'cpu':
|
|
458
|
+
width = (graph.width - 3) * 2
|
|
459
|
+
height = (graph.height - 2) * 4
|
|
460
|
+
currentCanvas = new Canvas(width, height)
|
|
461
|
+
break
|
|
462
|
+
case 'memory':
|
|
463
|
+
width = (graph2.width - 3) * 2
|
|
464
|
+
height = (graph2.height - 2) * 4
|
|
465
|
+
currentCanvas = new Canvas(width, height)
|
|
466
|
+
break
|
|
467
|
+
case 'process':
|
|
468
|
+
default:
|
|
469
|
+
width = processList.width - 3
|
|
470
|
+
height = processList.height - 2
|
|
471
|
+
break
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const key = String(index)
|
|
475
|
+
const values = charts[key]?.values ?? {}
|
|
476
|
+
|
|
477
|
+
charts[key] = {
|
|
478
|
+
chart: currentCanvas,
|
|
479
|
+
values,
|
|
480
|
+
plugin: sensorMap[plugin],
|
|
481
|
+
plugins,
|
|
482
|
+
width,
|
|
483
|
+
height,
|
|
484
|
+
}
|
|
485
|
+
charts[key].plugin.poll()
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
graph.setLabel(` ${charts['0'].plugin.title} `)
|
|
489
|
+
graph2.setLabel(` ${charts['1'].plugin.title} `)
|
|
490
|
+
processList.setLabel(` ${charts['2'].plugin.title} `)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
setupCharts()
|
|
494
|
+
screen.on('resize', setupCharts)
|
|
495
|
+
intervals.push(setInterval(draw, parseInt(cli.opts()['updateInterval'], 10)))
|
|
496
|
+
intervals.push(setInterval(charts['0'].plugin.poll, charts['0'].plugin.interval))
|
|
497
|
+
intervals.push(setInterval(charts['1'].plugin.poll, charts['1'].plugin.interval))
|
|
498
|
+
intervals.push(setInterval(charts['2'].plugin.poll, charts['2'].plugin.interval))
|
|
499
|
+
},
|
|
500
|
+
}
|
|
501
|
+
})()
|
|
502
|
+
|
|
503
|
+
App.init()
|
package/bin/vtop.ts
ADDED
package/docs/example.gif
ADDED
|
Binary file
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import tseslint from '@typescript-eslint/eslint-plugin'
|
|
2
|
+
import tsparser from '@typescript-eslint/parser'
|
|
3
|
+
|
|
4
|
+
export default [
|
|
5
|
+
{
|
|
6
|
+
files: ['**/*.ts'],
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser: tsparser,
|
|
9
|
+
},
|
|
10
|
+
plugins: {
|
|
11
|
+
'@typescript-eslint': tseslint,
|
|
12
|
+
},
|
|
13
|
+
rules: {
|
|
14
|
+
...tseslint.configs.recommended.rules,
|
|
15
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
]
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "neo-vtop",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Wow such top. So stats. More neo than vtop, very based.",
|
|
5
|
+
"homepage": "https://github.com/nexusocean8/neo-vtop",
|
|
6
|
+
"main": "app.ts",
|
|
7
|
+
"preferGlobal": true,
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">= 24"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "tsx app.ts",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"prepare": "husky"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"neo-vtop": "./bin/vtop.js",
|
|
18
|
+
"vtop": "./bin/vtop.js"
|
|
19
|
+
},
|
|
20
|
+
"author": {
|
|
21
|
+
"name": "Vincent",
|
|
22
|
+
"email": "support@nexusocean.io"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git@github.com:nexusocean8/neo-vtop.git"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^15.0.0",
|
|
32
|
+
"drawille": "1.1.0",
|
|
33
|
+
"glob": "^13.0.6",
|
|
34
|
+
"neo-blessed": "^0.2.0",
|
|
35
|
+
"os-utils": "0.0.14"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^26.0.1",
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
40
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
41
|
+
"eslint": "^9.0.0",
|
|
42
|
+
"husky": "^9.1.7",
|
|
43
|
+
"lint-staged": "^17.0.8",
|
|
44
|
+
"prettier": "^3.9.0",
|
|
45
|
+
"tsx": "latest",
|
|
46
|
+
"typescript": "latest"
|
|
47
|
+
},
|
|
48
|
+
"lint-staged": {
|
|
49
|
+
"*.{ts,js}": [
|
|
50
|
+
"eslint --fix",
|
|
51
|
+
"prettier --write"
|
|
52
|
+
],
|
|
53
|
+
"*.{json,md}": [
|
|
54
|
+
"prettier --write"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
}
|
package/sensors/cpu.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CPU Usage sensor
|
|
3
|
+
*
|
|
4
|
+
* (c) 2014 James Hall
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import os from 'os-utils'
|
|
8
|
+
|
|
9
|
+
interface CpuPlugin {
|
|
10
|
+
title: string
|
|
11
|
+
type: string
|
|
12
|
+
interval: number
|
|
13
|
+
initialized: boolean
|
|
14
|
+
currentValue: number
|
|
15
|
+
poll: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const plugin: CpuPlugin = {
|
|
19
|
+
title: 'CPU Usage',
|
|
20
|
+
type: 'chart',
|
|
21
|
+
interval: 200,
|
|
22
|
+
initialized: false,
|
|
23
|
+
currentValue: 0,
|
|
24
|
+
|
|
25
|
+
poll() {
|
|
26
|
+
;(os as any).cpuUsage((v: number) => {
|
|
27
|
+
plugin.currentValue = Math.floor(v * 100)
|
|
28
|
+
plugin.initialized = true
|
|
29
|
+
})
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default plugin
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Usage sensor
|
|
3
|
+
*
|
|
4
|
+
* (c) 2014 James Hall
|
|
5
|
+
*/
|
|
6
|
+
import os from 'os-utils'
|
|
7
|
+
import _os from 'os'
|
|
8
|
+
import child from 'child_process'
|
|
9
|
+
|
|
10
|
+
interface MemoryPlugin {
|
|
11
|
+
title: string
|
|
12
|
+
type: string
|
|
13
|
+
interval: number
|
|
14
|
+
initialized: boolean
|
|
15
|
+
currentValue: number
|
|
16
|
+
isLinux: boolean
|
|
17
|
+
isMac: boolean
|
|
18
|
+
poll: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const computeUsage = (used: number, total: number): number => Math.round(100 * (used / total))
|
|
22
|
+
|
|
23
|
+
const plugin: MemoryPlugin = {
|
|
24
|
+
title: 'Memory Usage',
|
|
25
|
+
type: 'chart',
|
|
26
|
+
interval: 200,
|
|
27
|
+
initialized: false,
|
|
28
|
+
currentValue: 0,
|
|
29
|
+
isLinux: _os.platform().includes('linux'),
|
|
30
|
+
isMac: _os.platform().includes('darwin'),
|
|
31
|
+
|
|
32
|
+
poll() {
|
|
33
|
+
if (plugin.isLinux) {
|
|
34
|
+
child.exec('free -m', (err, stdout) => {
|
|
35
|
+
if (err) console.error(err)
|
|
36
|
+
const data = stdout
|
|
37
|
+
.split('\n')[1]
|
|
38
|
+
.replace(/[\s\n\r]+/g, ' ')
|
|
39
|
+
.split(' ')
|
|
40
|
+
const used = parseInt(data[2], 10)
|
|
41
|
+
const total = parseInt(data[1], 10)
|
|
42
|
+
plugin.currentValue = computeUsage(used, total)
|
|
43
|
+
})
|
|
44
|
+
} else if (plugin.isMac) {
|
|
45
|
+
child.exec('ps -caxm -orss,comm', (err, stdout) => {
|
|
46
|
+
if (err) throw err
|
|
47
|
+
const sp = stdout.split('\n')
|
|
48
|
+
let total = 0
|
|
49
|
+
for (let i = 0; i < sp.length; i++) {
|
|
50
|
+
const val = parseInt(sp[i].replace(/([a-zA-Z]).*/, ''))
|
|
51
|
+
if (!isNaN(val)) total += val
|
|
52
|
+
}
|
|
53
|
+
const usedmem = total / 1024 ** 2
|
|
54
|
+
const freemem = (os as any).totalmem() - usedmem
|
|
55
|
+
const per = freemem / (os as any).totalmem()
|
|
56
|
+
plugin.currentValue = Math.round((1 - per) * 100)
|
|
57
|
+
})
|
|
58
|
+
} else {
|
|
59
|
+
plugin.currentValue = Math.round((1 - (os as any).freememPercentage()) * 100)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
plugin.initialized = true
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default plugin
|