top
Node.js 的 top 命令
用法
sh
call top
call top
示例
sh
icall top # on window
icall top # on window
退出
- use
q
- prompt
yes
实现
ts
import os from 'node:os'
import {
ConsoleManager,
ConsoleGuiOptions,
Progress,
ProgressConfig,
ConfirmPopup,
PageBuilder,
KeyListenerArgs,
SimplifiedStyledElement,
InPageWidgetBuilder,
Box,
OptionPopup,
EOL,
} from 'console-gui-tools'
import { RelativeMouseEvent } from 'console-gui-tools/dist/types/components/MouseManager'
import _ from 'lodash'
import psList, { ProcessDescriptor } from 'ps-list'
const opt = {
title: 'Progress Bar Test',
layoutOptions: {
type: 'single',
},
logLocation: 'popup',
enableMouse: true,
} as ConsoleGuiOptions
const GUI = new ConsoleManager(opt)
GUI.on('exit', () => {
closeApp()
})
GUI.on('keypressed', (key: KeyListenerArgs) => {
switch (key.name) {
case 'q':
case 'f10':
new ConfirmPopup({
id: 'popupQuit',
title: 'Are you sure you want to quit?',
})
.show()
.on('confirm', () => closeApp())
break
default:
break
}
})
const closeApp = () => {
console.clear()
process.exit()
}
GUI.refresh()
const numberOfCores = os.cpus().length
const cores: Progress[] = []
for (let i = 0; i < numberOfCores; i++) {
cores.push(
new Progress({
id: `htop-cpu-${i}`,
x: 2,
y: 1 + i,
label: `${i} `,
length: 40,
min: 0,
max: 100,
style: {
boxed: true,
theme: 'htop',
showMinMax: false,
showValue: false,
},
} as ProgressConfig)
)
}
const mem = new Progress({
id: 'htop-mem',
x: 2,
y: 1 + numberOfCores,
label: 'Mem',
length: 40,
min: 0,
max: os.totalmem() / (1024 * 1024 * 1024),
unit: 'G',
style: {
boxed: true,
theme: 'htop',
showMinMax: false,
},
} as ProgressConfig)
const tableData = {
selectedRow: 0,
psTab: [] as ProcessDescriptor[],
header: [] as string[],
table: new InPageWidgetBuilder(100),
maxSizes: [] as number[],
spacing: 2,
sortBy: 'name',
}
const Table = new Box({
id: 'table',
x: 0,
y: mem.absoluteValues.y + 3,
width: GUI.Screen.width,
height: 30,
})
Table.focus()
Table.on('keypress', (key: KeyListenerArgs) => {
if (!Table.focused) return
switch (key.name) {
case 'up':
if (tableData.selectedRow > 0) {
tableData.selectedRow -= 1
drawTable()
}
break
case 'down':
if (tableData.selectedRow < tableData.psTab.length - 1) {
tableData.selectedRow += 1
drawTable()
}
break
case 'f9':
// Kill process
if (tableData.psTab.length > 0) {
const selectedProcess = tableData.psTab[tableData.selectedRow]
new ConfirmPopup({
id: 'popupKill',
title: `Are you sure you want to kill process ${selectedProcess.name} (${selectedProcess.pid})?`,
})
.show()
.on('confirm', () => {
process.kill(selectedProcess.pid)
})
}
break
case 'f6':
// Sort by
{
const sortOptions = Object.keys(tableData.psTab[0])
new OptionPopup({
id: 'popupSort',
title: 'Sort by',
options: sortOptions,
selected: tableData.sortBy,
})
.show()
.on('confirm', (option: string) => {
tableData.sortBy = option
drawTable()
})
}
break
case 'f1':
// Help
new ConfirmPopup({
id: 'popupHelp',
title: 'Help',
message: `Use the mouse wheel to scroll the table.${EOL}Use the up and down arrow keys to select a row.${EOL}Press F9 to kill a process.${EOL}Press F6 to sort the table.${EOL}Press F1 to show this help.${EOL}Press F10 or Q to quit.`,
}).show()
break
default:
break
}
})
Table.on('relativeMouse', (e: RelativeMouseEvent) => {
if (e.name === 'MOUSE_WHEEL_UP') {
if (tableData.selectedRow > 0) {
tableData.selectedRow -= 1
drawTable()
}
} else if (e.name === 'MOUSE_WHEEL_DOWN') {
if (tableData.selectedRow < tableData.psTab.length - 1) {
tableData.selectedRow += 1
drawTable()
}
} else if (e.name === 'MOUSE_LEFT_BUTTON_PRESSED') {
if (tableData.psTab.length <= Table.absoluteValues.height) {
tableData.selectedRow = e.data.y - 1
drawTable()
return
}
const realStartIndex = mapToRange(
Table.content.scrollIndex,
tableData.psTab.length - Table.absoluteValues.height,
0,
0,
tableData.psTab.length - Table.absoluteValues.height
)
tableData.selectedRow = e.data.y + realStartIndex - 1
drawTable()
}
})
function mapToRange(
value: number,
min1: number,
max1: number,
min2: number,
max2: number
) {
return min2 + (max2 - min2) * ((value - min1) / (max1 - min1))
}
//Create function to get CPU information
function cpuAverage(core: number) {
//Initialise sum of idle and time of cores and fetch CPU info
let totalIdle = 0,
totalTick = 0
//Select CPU core
const cpu = os.cpus()[core]
//Total up the time in the cores tick
for (let i = 0, len = Object.keys(cpu.times).length; i < len; i++) {
totalTick += Object.values(cpu.times)[i]
}
//Total up the idle time of the core
totalIdle += cpu.times.idle
//Return the average Idle and Tick times
return { idle: totalIdle, total: totalTick }
}
// load average for the past 1000 milliseconds calculated every 100
function getCPULoadAVG(core: number, avgTime = 1000, delay = 500) {
return new Promise((resolve, reject) => {
const n = ~~(avgTime / delay)
if (n <= 1) {
reject('Error: interval to small')
}
let i = 0
const samples: number[] = []
const avg1 = cpuAverage(core)
const interval = setInterval(() => {
//GUI.log("CPU Interval: " + i)
if (i >= n) {
clearInterval(interval)
resolve(~~((samples.reduce((a, b) => a + b, 0) / samples.length) * 100))
}
const avg2 = cpuAverage(core)
const totalDiff = avg2.total - avg1.total
const idleDiff = avg2.idle - avg1.idle
samples[i] = 1 - idleDiff / totalDiff
i++
}, delay)
})
}
const getSystemInfo = async () => {
const coresPercent = []
for (let i = 0; i < numberOfCores; i++) {
const load = await getCPULoadAVG(i).catch((err) => console.error(err))
coresPercent.push(load)
}
const memUsage = os.totalmem() - os.freemem()
return {
cpuUsage: coresPercent,
memUsage,
}
}
setInterval(() => {
getSystemInfo().then((info) => {
info.cpuUsage.forEach((core, i) => {
cores[i].setValue(core as number)
})
mem.setValue(info.memUsage / (1024 * 1024 * 1024))
})
}, 1000)
export async function getData() {
// Draw header for top table
const uptime = os.uptime()
const hours = Math.floor(uptime / 3600)
const minutes = Math.floor((uptime % 3600) / 60)
const seconds = Math.floor(uptime % 60)
const uptimeText = `${hours}:${minutes}:${seconds}s`
//const limit = 10
const psTable = await psList({ all: true })
// Limit to 20 processes
//psTable.splice(limit)
if (psTable.length === 0) return
const p = new PageBuilder()
p.addSpacer()
p.addRow(
{ text: `${' '.repeat(58)}Uptime: `, color: '#3d96da', bold: true },
{ text: `${uptimeText}`, color: 'cyan', bold: true }
)
p.addSpacer(numberOfCores + 1)
const header = Object.keys(psTable[0]).map((h) => h.toUpperCase())
const maxSizes = header.map((h) => h.length)
const spacing = 2
psTable.forEach((row) => {
const keys = Object.keys(row)
keys.forEach((key, i) => {
if (Object.values(row)[i].toString().length > maxSizes[i]) {
maxSizes[i] = Object.values(row)[i].toString().length
}
})
})
p.addRow(
...header.map((h, i) => {
return {
text: `${h}${' '.repeat(
(maxSizes[i] - h.length > 0 ? maxSizes[i] - h.length : 0) + spacing
)}`,
color: 'black',
bg: 'bgGreen',
bold: false,
} as SimplifiedStyledElement
})
)
drawGui(p)
updateTable(psTable, header, maxSizes, spacing)
drawTable()
setTimeout(getData, 1000)
}
const drawTable = () => {
let orderDirection = ''
switch (tableData.sortBy.toUpperCase()) {
case 'PID':
case 'PPID':
case 'CPU':
case 'MEMORY':
case 'UID':
orderDirection = 'desc'
break
default:
orderDirection = 'asc'
break
}
tableData.psTab = _.orderBy(
tableData.psTab,
(obj: ProcessDescriptor) => {
switch (tableData.sortBy.toUpperCase()) {
case 'PID':
case 'PPID':
case 'CPU':
case 'MEMORY':
case 'UID':
return Number(
Object.values(obj)[Object.keys(obj).indexOf(tableData.sortBy)]
)
default:
return Object.values(obj)[Object.keys(obj).indexOf(tableData.sortBy)]
}
},
orderDirection as 'asc' | 'desc'
)
tableData.table.clear()
tableData.psTab.forEach((row, index) => {
const background = index === tableData.selectedRow ? 'bgCyan' : undefined
tableData.table.addRow(
...tableData.header.map((_, i) => {
return {
text: `${Object.values(row)[i]}${' '.repeat(
(tableData.maxSizes[i] - Object.values(row)[i].toString().length > 0
? tableData.maxSizes[i] - Object.values(row)[i].toString().length
: 0) + tableData.spacing
)}`,
color: 'white',
bg: background,
bold: true,
} as SimplifiedStyledElement
})
)
})
Table.setContent(tableData.table)
//Table.focus()
}
const updateTable = (
psTable: ProcessDescriptor[],
header: string[],
maxSizes: number[],
spacing: number
) => {
tableData.psTab = psTable
tableData.header = header
tableData.maxSizes = maxSizes
tableData.spacing = spacing
}
const drawGui = (p: PageBuilder) => {
GUI.setPage(p)
GUI.refresh()
}
const footer = new Box({
id: 'footer',
x: 0,
y: GUI.Screen.height - 1,
width: GUI.Screen.width,
height: 1,
})
const row = new InPageWidgetBuilder(1)
row.addRow(
{ text: 'F1:', color: 'white', bold: true },
{ text: 'Help ', color: 'black', bg: 'bgCyan', bold: false },
{ text: 'F6:', color: 'white', bold: true },
{ text: 'SortBy', color: 'black', bg: 'bgCyan', bold: false },
{ text: 'F9:', color: 'white', bold: true },
{ text: 'Kill ', color: 'black', bg: 'bgCyan', bold: false },
{ text: 'F10:', color: 'white', bold: true },
{ text: 'Quit ', color: 'black', bg: 'bgCyan', bold: false }
)
footer.setContent(row)
GUI.on('resize', () => {
footer.absoluteValues = {
x: 0,
y: GUI.Screen.height - 2,
width: GUI.Screen.width,
height: 1,
}
GUI.refresh()
})
import os from 'node:os'
import {
ConsoleManager,
ConsoleGuiOptions,
Progress,
ProgressConfig,
ConfirmPopup,
PageBuilder,
KeyListenerArgs,
SimplifiedStyledElement,
InPageWidgetBuilder,
Box,
OptionPopup,
EOL,
} from 'console-gui-tools'
import { RelativeMouseEvent } from 'console-gui-tools/dist/types/components/MouseManager'
import _ from 'lodash'
import psList, { ProcessDescriptor } from 'ps-list'
const opt = {
title: 'Progress Bar Test',
layoutOptions: {
type: 'single',
},
logLocation: 'popup',
enableMouse: true,
} as ConsoleGuiOptions
const GUI = new ConsoleManager(opt)
GUI.on('exit', () => {
closeApp()
})
GUI.on('keypressed', (key: KeyListenerArgs) => {
switch (key.name) {
case 'q':
case 'f10':
new ConfirmPopup({
id: 'popupQuit',
title: 'Are you sure you want to quit?',
})
.show()
.on('confirm', () => closeApp())
break
default:
break
}
})
const closeApp = () => {
console.clear()
process.exit()
}
GUI.refresh()
const numberOfCores = os.cpus().length
const cores: Progress[] = []
for (let i = 0; i < numberOfCores; i++) {
cores.push(
new Progress({
id: `htop-cpu-${i}`,
x: 2,
y: 1 + i,
label: `${i} `,
length: 40,
min: 0,
max: 100,
style: {
boxed: true,
theme: 'htop',
showMinMax: false,
showValue: false,
},
} as ProgressConfig)
)
}
const mem = new Progress({
id: 'htop-mem',
x: 2,
y: 1 + numberOfCores,
label: 'Mem',
length: 40,
min: 0,
max: os.totalmem() / (1024 * 1024 * 1024),
unit: 'G',
style: {
boxed: true,
theme: 'htop',
showMinMax: false,
},
} as ProgressConfig)
const tableData = {
selectedRow: 0,
psTab: [] as ProcessDescriptor[],
header: [] as string[],
table: new InPageWidgetBuilder(100),
maxSizes: [] as number[],
spacing: 2,
sortBy: 'name',
}
const Table = new Box({
id: 'table',
x: 0,
y: mem.absoluteValues.y + 3,
width: GUI.Screen.width,
height: 30,
})
Table.focus()
Table.on('keypress', (key: KeyListenerArgs) => {
if (!Table.focused) return
switch (key.name) {
case 'up':
if (tableData.selectedRow > 0) {
tableData.selectedRow -= 1
drawTable()
}
break
case 'down':
if (tableData.selectedRow < tableData.psTab.length - 1) {
tableData.selectedRow += 1
drawTable()
}
break
case 'f9':
// Kill process
if (tableData.psTab.length > 0) {
const selectedProcess = tableData.psTab[tableData.selectedRow]
new ConfirmPopup({
id: 'popupKill',
title: `Are you sure you want to kill process ${selectedProcess.name} (${selectedProcess.pid})?`,
})
.show()
.on('confirm', () => {
process.kill(selectedProcess.pid)
})
}
break
case 'f6':
// Sort by
{
const sortOptions = Object.keys(tableData.psTab[0])
new OptionPopup({
id: 'popupSort',
title: 'Sort by',
options: sortOptions,
selected: tableData.sortBy,
})
.show()
.on('confirm', (option: string) => {
tableData.sortBy = option
drawTable()
})
}
break
case 'f1':
// Help
new ConfirmPopup({
id: 'popupHelp',
title: 'Help',
message: `Use the mouse wheel to scroll the table.${EOL}Use the up and down arrow keys to select a row.${EOL}Press F9 to kill a process.${EOL}Press F6 to sort the table.${EOL}Press F1 to show this help.${EOL}Press F10 or Q to quit.`,
}).show()
break
default:
break
}
})
Table.on('relativeMouse', (e: RelativeMouseEvent) => {
if (e.name === 'MOUSE_WHEEL_UP') {
if (tableData.selectedRow > 0) {
tableData.selectedRow -= 1
drawTable()
}
} else if (e.name === 'MOUSE_WHEEL_DOWN') {
if (tableData.selectedRow < tableData.psTab.length - 1) {
tableData.selectedRow += 1
drawTable()
}
} else if (e.name === 'MOUSE_LEFT_BUTTON_PRESSED') {
if (tableData.psTab.length <= Table.absoluteValues.height) {
tableData.selectedRow = e.data.y - 1
drawTable()
return
}
const realStartIndex = mapToRange(
Table.content.scrollIndex,
tableData.psTab.length - Table.absoluteValues.height,
0,
0,
tableData.psTab.length - Table.absoluteValues.height
)
tableData.selectedRow = e.data.y + realStartIndex - 1
drawTable()
}
})
function mapToRange(
value: number,
min1: number,
max1: number,
min2: number,
max2: number
) {
return min2 + (max2 - min2) * ((value - min1) / (max1 - min1))
}
//Create function to get CPU information
function cpuAverage(core: number) {
//Initialise sum of idle and time of cores and fetch CPU info
let totalIdle = 0,
totalTick = 0
//Select CPU core
const cpu = os.cpus()[core]
//Total up the time in the cores tick
for (let i = 0, len = Object.keys(cpu.times).length; i < len; i++) {
totalTick += Object.values(cpu.times)[i]
}
//Total up the idle time of the core
totalIdle += cpu.times.idle
//Return the average Idle and Tick times
return { idle: totalIdle, total: totalTick }
}
// load average for the past 1000 milliseconds calculated every 100
function getCPULoadAVG(core: number, avgTime = 1000, delay = 500) {
return new Promise((resolve, reject) => {
const n = ~~(avgTime / delay)
if (n <= 1) {
reject('Error: interval to small')
}
let i = 0
const samples: number[] = []
const avg1 = cpuAverage(core)
const interval = setInterval(() => {
//GUI.log("CPU Interval: " + i)
if (i >= n) {
clearInterval(interval)
resolve(~~((samples.reduce((a, b) => a + b, 0) / samples.length) * 100))
}
const avg2 = cpuAverage(core)
const totalDiff = avg2.total - avg1.total
const idleDiff = avg2.idle - avg1.idle
samples[i] = 1 - idleDiff / totalDiff
i++
}, delay)
})
}
const getSystemInfo = async () => {
const coresPercent = []
for (let i = 0; i < numberOfCores; i++) {
const load = await getCPULoadAVG(i).catch((err) => console.error(err))
coresPercent.push(load)
}
const memUsage = os.totalmem() - os.freemem()
return {
cpuUsage: coresPercent,
memUsage,
}
}
setInterval(() => {
getSystemInfo().then((info) => {
info.cpuUsage.forEach((core, i) => {
cores[i].setValue(core as number)
})
mem.setValue(info.memUsage / (1024 * 1024 * 1024))
})
}, 1000)
export async function getData() {
// Draw header for top table
const uptime = os.uptime()
const hours = Math.floor(uptime / 3600)
const minutes = Math.floor((uptime % 3600) / 60)
const seconds = Math.floor(uptime % 60)
const uptimeText = `${hours}:${minutes}:${seconds}s`
//const limit = 10
const psTable = await psList({ all: true })
// Limit to 20 processes
//psTable.splice(limit)
if (psTable.length === 0) return
const p = new PageBuilder()
p.addSpacer()
p.addRow(
{ text: `${' '.repeat(58)}Uptime: `, color: '#3d96da', bold: true },
{ text: `${uptimeText}`, color: 'cyan', bold: true }
)
p.addSpacer(numberOfCores + 1)
const header = Object.keys(psTable[0]).map((h) => h.toUpperCase())
const maxSizes = header.map((h) => h.length)
const spacing = 2
psTable.forEach((row) => {
const keys = Object.keys(row)
keys.forEach((key, i) => {
if (Object.values(row)[i].toString().length > maxSizes[i]) {
maxSizes[i] = Object.values(row)[i].toString().length
}
})
})
p.addRow(
...header.map((h, i) => {
return {
text: `${h}${' '.repeat(
(maxSizes[i] - h.length > 0 ? maxSizes[i] - h.length : 0) + spacing
)}`,
color: 'black',
bg: 'bgGreen',
bold: false,
} as SimplifiedStyledElement
})
)
drawGui(p)
updateTable(psTable, header, maxSizes, spacing)
drawTable()
setTimeout(getData, 1000)
}
const drawTable = () => {
let orderDirection = ''
switch (tableData.sortBy.toUpperCase()) {
case 'PID':
case 'PPID':
case 'CPU':
case 'MEMORY':
case 'UID':
orderDirection = 'desc'
break
default:
orderDirection = 'asc'
break
}
tableData.psTab = _.orderBy(
tableData.psTab,
(obj: ProcessDescriptor) => {
switch (tableData.sortBy.toUpperCase()) {
case 'PID':
case 'PPID':
case 'CPU':
case 'MEMORY':
case 'UID':
return Number(
Object.values(obj)[Object.keys(obj).indexOf(tableData.sortBy)]
)
default:
return Object.values(obj)[Object.keys(obj).indexOf(tableData.sortBy)]
}
},
orderDirection as 'asc' | 'desc'
)
tableData.table.clear()
tableData.psTab.forEach((row, index) => {
const background = index === tableData.selectedRow ? 'bgCyan' : undefined
tableData.table.addRow(
...tableData.header.map((_, i) => {
return {
text: `${Object.values(row)[i]}${' '.repeat(
(tableData.maxSizes[i] - Object.values(row)[i].toString().length > 0
? tableData.maxSizes[i] - Object.values(row)[i].toString().length
: 0) + tableData.spacing
)}`,
color: 'white',
bg: background,
bold: true,
} as SimplifiedStyledElement
})
)
})
Table.setContent(tableData.table)
//Table.focus()
}
const updateTable = (
psTable: ProcessDescriptor[],
header: string[],
maxSizes: number[],
spacing: number
) => {
tableData.psTab = psTable
tableData.header = header
tableData.maxSizes = maxSizes
tableData.spacing = spacing
}
const drawGui = (p: PageBuilder) => {
GUI.setPage(p)
GUI.refresh()
}
const footer = new Box({
id: 'footer',
x: 0,
y: GUI.Screen.height - 1,
width: GUI.Screen.width,
height: 1,
})
const row = new InPageWidgetBuilder(1)
row.addRow(
{ text: 'F1:', color: 'white', bold: true },
{ text: 'Help ', color: 'black', bg: 'bgCyan', bold: false },
{ text: 'F6:', color: 'white', bold: true },
{ text: 'SortBy', color: 'black', bg: 'bgCyan', bold: false },
{ text: 'F9:', color: 'white', bold: true },
{ text: 'Kill ', color: 'black', bg: 'bgCyan', bold: false },
{ text: 'F10:', color: 'white', bold: true },
{ text: 'Quit ', color: 'black', bg: 'bgCyan', bold: false }
)
footer.setContent(row)
GUI.on('resize', () => {
footer.absoluteValues = {
x: 0,
y: GUI.Screen.height - 2,
width: GUI.Screen.width,
height: 1,
}
GUI.refresh()
})