-
Notifications
You must be signed in to change notification settings - Fork 198
Feat/process monitor enhancements #1718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,8 @@ | ||||||||||||||||||||||||||
| /** @jsxImportSource @termuijs/jsx */ | ||||||||||||||||||||||||||
| import { renderApp, useState, useEffect, useRef, useKeymap } from '@termuijs/jsx'; | ||||||||||||||||||||||||||
| import { Table, LineChart, BarChart, type TableColumn, type TableRow, type BarGroup } from '@termuijs/widgets'; | ||||||||||||||||||||||||||
| import { renderApp, useState, useEffect, useRef, useKeymap, useInput } from '@termuijs/jsx'; | ||||||||||||||||||||||||||
| import { Table, LineChart, BarChart, TextInput, type TableColumn, type TableRow, type BarGroup } from '@termuijs/widgets'; | ||||||||||||||||||||||||||
| import { useCpu, useMemory, useTopProcesses, useSystemInfo } from '@termuijs/data'; | ||||||||||||||||||||||||||
| import type { KeyEvent } from '@termuijs/core'; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| declare module '@termuijs/jsx' { | ||||||||||||||||||||||||||
| export namespace JSX { | ||||||||||||||||||||||||||
|
|
@@ -81,13 +82,67 @@ function BarChartComponent({ data, style, options }: { data: BarGroup[]; style?: | |||||||||||||||||||||||||
| return chartRef.current; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // ── TextInput JSX wrapper ──────────────────────────── | ||||||||||||||||||||||||||
| function TextInputJSX({ value, onChange, placeholder, isFocused }: { | ||||||||||||||||||||||||||
| value: string; | ||||||||||||||||||||||||||
| onChange: (val: string) => void; | ||||||||||||||||||||||||||
| placeholder?: string; | ||||||||||||||||||||||||||
| isFocused: boolean; | ||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||
| const ref = useRef<TextInput | null>(null); | ||||||||||||||||||||||||||
| if (!ref.current) { | ||||||||||||||||||||||||||
| ref.current = new TextInput({ width: 30, height: 1 }, { placeholder, onChange }); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if (ref.current.value !== value) { | ||||||||||||||||||||||||||
| ref.current.value = value; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ref.current.isFocused = isFocused; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| useInput((key: string, event: KeyEvent) => { | ||||||||||||||||||||||||||
| if (!isFocused) return; | ||||||||||||||||||||||||||
| if (!ref.current) return; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| switch (key) { | ||||||||||||||||||||||||||
| case 'backspace': ref.current.deleteBack(); break; | ||||||||||||||||||||||||||
| case 'delete': ref.current.deleteForward(); break; | ||||||||||||||||||||||||||
| case 'left': ref.current.moveCursorLeft(); break; | ||||||||||||||||||||||||||
| case 'right': ref.current.moveCursorRight(); break; | ||||||||||||||||||||||||||
| case 'home': ref.current.moveCursorHome(); break; | ||||||||||||||||||||||||||
| case 'end': ref.current.moveCursorEnd(); break; | ||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||
| if (key && key.length === 1 && !event.ctrl && !event.alt) { | ||||||||||||||||||||||||||
| ref.current.insertChar(key); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return ref.current as any; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // ── Helper: build a text-based gauge bar ───────────── | ||||||||||||||||||||||||||
| function gaugeBar(percent: number, width: number): string { | ||||||||||||||||||||||||||
| const filled = Math.round((percent / 100) * width); | ||||||||||||||||||||||||||
| const empty = width - filled; | ||||||||||||||||||||||||||
| const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(empty); | ||||||||||||||||||||||||||
| return `[${bar}] ${percent.toFixed(0)}%`; | ||||||||||||||||||||||||||
|
Comment on lines
+125
to
+129
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clamp gauge percent before building the bar
Suggested fix function gaugeBar(percent: number, width: number): string {
- const filled = Math.round((percent / 100) * width);
+ const safePercent = Math.max(0, Math.min(100, percent));
+ const filled = Math.round((safePercent / 100) * width);
const empty = width - filled;
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(empty);
- return `[${bar}] ${percent.toFixed(0)}%`;
+ return `[${bar}] ${safePercent.toFixed(0)}%`;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // ── Helper: format uptime seconds ──────────────────── | ||||||||||||||||||||||||||
| function formatUptime(uptimeStr: string): string { | ||||||||||||||||||||||||||
| return uptimeStr; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function ProcessMonitor() { | ||||||||||||||||||||||||||
| const sys = useSystemInfo(); | ||||||||||||||||||||||||||
| const cpu = useCpu(1000); | ||||||||||||||||||||||||||
| const mem = useMemory(1000); | ||||||||||||||||||||||||||
| const procs = useTopProcesses(10, 2000); | ||||||||||||||||||||||||||
| const procs = useTopProcesses(30, 2000); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const [cpuHistory, setCpuHistory] = useState<number[]>([]); | ||||||||||||||||||||||||||
| const [filterText, setFilterText] = useState(''); | ||||||||||||||||||||||||||
| const [filterFocused, setFilterFocused] = useState(false); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||
| setCpuHistory(prev => { | ||||||||||||||||||||||||||
|
|
@@ -98,7 +153,9 @@ function ProcessMonitor() { | |||||||||||||||||||||||||
| }, [cpu.percent]); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| useKeymap([ | ||||||||||||||||||||||||||
| { key: 'q', action: () => process.exit(0), description: 'Quit' }, | ||||||||||||||||||||||||||
| { key: 'q', action: () => { if (!filterFocused) process.exit(0); }, description: 'Quit' }, | ||||||||||||||||||||||||||
| { key: '/', action: () => setFilterFocused(true), description: 'Search' }, | ||||||||||||||||||||||||||
| { key: 'escape', action: () => { setFilterFocused(false); setFilterText(''); }, description: 'Clear search' }, | ||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const memData: BarGroup[] = [ | ||||||||||||||||||||||||||
|
|
@@ -111,32 +168,53 @@ function ProcessMonitor() { | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| ]; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const tableRows = procs.map(p => ({ | ||||||||||||||||||||||||||
| // Filter processes by name if search is active | ||||||||||||||||||||||||||
| const filteredProcs = filterText.length > 0 | ||||||||||||||||||||||||||
| ? procs.filter(p => p.name.toLowerCase().includes(filterText.toLowerCase())) | ||||||||||||||||||||||||||
| : procs; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| const tableRows = filteredProcs.slice(0, 15).map(p => ({ | ||||||||||||||||||||||||||
| pid: String(p.pid), | ||||||||||||||||||||||||||
| name: p.name.slice(0, 18), | ||||||||||||||||||||||||||
| cpu: `${p.cpu.toFixed(1)}%`, | ||||||||||||||||||||||||||
| mem: `${p.mem.toFixed(1)}%`, | ||||||||||||||||||||||||||
| user: p.user, | ||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Compute process stats from the full list | ||||||||||||||||||||||||||
| const totalProcs = procs.length; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // CPU gauge bar for the header | ||||||||||||||||||||||||||
| const cpuGauge = gaugeBar(cpu.percent, 15); | ||||||||||||||||||||||||||
| // Memory gauge bar for the header | ||||||||||||||||||||||||||
| const memGauge = gaugeBar(mem.percent, 15); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||
| <box flexDirection="column" padding={1} gap={1} flexGrow={1}> | ||||||||||||||||||||||||||
| {/* Header */} | ||||||||||||||||||||||||||
| <box flexDirection="row" height={3} border="round" borderColor="cyan" padding={1}> | ||||||||||||||||||||||||||
| <text bold color="cyan"> 📊 TERMUI PROCESS MONITOR </text> | ||||||||||||||||||||||||||
| <spacer /> | ||||||||||||||||||||||||||
| <text dim={true}> | ||||||||||||||||||||||||||
| {sys.hostname} • {sys.platform} ({sys.arch}) • Uptime: {sys.uptime} | ||||||||||||||||||||||||||
| {sys.hostname} • {sys.platform} ({sys.arch}) • Uptime: {formatUptime(sys.uptime)} | ||||||||||||||||||||||||||
| </text> | ||||||||||||||||||||||||||
| </box> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| {/* System Summary Bar */} | ||||||||||||||||||||||||||
| <box flexDirection="row" height={3} border="single" borderColor="magenta" padding={1} gap={2}> | ||||||||||||||||||||||||||
| <text bold color="green"> CPU: {cpuGauge} </text> | ||||||||||||||||||||||||||
| <text bold color="yellow"> RAM: {mem.used}/{mem.total} ({mem.percent.toFixed(0)}%) </text> | ||||||||||||||||||||||||||
| <text bold color="blue"> Procs: {totalProcs} </text> | ||||||||||||||||||||||||||
| <text bold color="cyan"> Up: {formatUptime(sys.uptime)} </text> | ||||||||||||||||||||||||||
| </box> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| {/* Main Area */} | ||||||||||||||||||||||||||
| <box flexDirection="row" flexGrow={1} gap={1}> | ||||||||||||||||||||||||||
| {/* Left Panel: CPU Line Chart & Memory status */} | ||||||||||||||||||||||||||
| {/* Left Panel: CPU Line Chart & Memory status & Process Stats */} | ||||||||||||||||||||||||||
| <box flexDirection="column" width={40} gap={1}> | ||||||||||||||||||||||||||
| <card title="CPU Load Trend" borderColor="green" flexGrow={1}> | ||||||||||||||||||||||||||
| <box flexDirection="column" flexGrow={1}> | ||||||||||||||||||||||||||
| <text bold color="green">Load: {cpu.percent.toFixed(1)}%</text> | ||||||||||||||||||||||||||
| <text bold color="green">Load: {cpu.percent.toFixed(1)}% ({cpu.count} cores @ {cpu.speed}MHz)</text> | ||||||||||||||||||||||||||
| <LineChartComponent | ||||||||||||||||||||||||||
| data={cpuHistory} | ||||||||||||||||||||||||||
| style={{ flexGrow: 1 }} | ||||||||||||||||||||||||||
|
|
@@ -162,22 +240,47 @@ function ProcessMonitor() { | |||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
| </box> | ||||||||||||||||||||||||||
| </card> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| {/* Process Statistics Panel */} | ||||||||||||||||||||||||||
| <card title="Process Stats" borderColor="magenta" height={6}> | ||||||||||||||||||||||||||
| <box flexDirection="column"> | ||||||||||||||||||||||||||
| <text bold color="magenta"> Total Processes: {totalProcs}</text> | ||||||||||||||||||||||||||
| <text color="green"> CPU Model: {cpu.model.slice(0, 30)}</text> | ||||||||||||||||||||||||||
| <text color="cyan"> Load Avg: {cpu.loadAvg.map(l => l.toFixed(2)).join(', ')}</text> | ||||||||||||||||||||||||||
| </box> | ||||||||||||||||||||||||||
| </card> | ||||||||||||||||||||||||||
| </box> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| {/* Right Panel: Top Processes */} | ||||||||||||||||||||||||||
| <card title="Top Processes" borderColor="blue" flexGrow={1}> | ||||||||||||||||||||||||||
| <TableComponent | ||||||||||||||||||||||||||
| columns={columns} | ||||||||||||||||||||||||||
| rows={tableRows} | ||||||||||||||||||||||||||
| style={{ flexGrow: 1 }} | ||||||||||||||||||||||||||
| options={{ stripe: true }} | ||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
| </card> | ||||||||||||||||||||||||||
| {/* Right Panel: Search + Top Processes */} | ||||||||||||||||||||||||||
| <box flexDirection="column" flexGrow={1} gap={1}> | ||||||||||||||||||||||||||
| {/* Search/Filter Bar */} | ||||||||||||||||||||||||||
| <box flexDirection="row" height={3} border="single" borderColor={filterFocused ? 'green' : 'white'} padding={1}> | ||||||||||||||||||||||||||
| <text bold color={filterFocused ? 'green' : 'white'}> 🔍 Filter: </text> | ||||||||||||||||||||||||||
| <TextInputJSX | ||||||||||||||||||||||||||
| value={filterText} | ||||||||||||||||||||||||||
| onChange={setFilterText} | ||||||||||||||||||||||||||
| placeholder="Type to filter processes..." | ||||||||||||||||||||||||||
| isFocused={filterFocused} | ||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
| {filterText.length > 0 && ( | ||||||||||||||||||||||||||
| <text dim={true}> ({filteredProcs.length} matches)</text> | ||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||
| </box> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| <card title={filterText ? `Processes matching "${filterText}"` : "Top Processes"} borderColor="blue" flexGrow={1}> | ||||||||||||||||||||||||||
| <TableComponent | ||||||||||||||||||||||||||
| columns={columns} | ||||||||||||||||||||||||||
| rows={tableRows} | ||||||||||||||||||||||||||
| style={{ flexGrow: 1 }} | ||||||||||||||||||||||||||
| options={{ stripe: true }} | ||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||
| </card> | ||||||||||||||||||||||||||
| </box> | ||||||||||||||||||||||||||
| </box> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| {/* Footer */} | ||||||||||||||||||||||||||
| <box flexDirection="row" height={1}> | ||||||||||||||||||||||||||
| <text dim={true}>Controls: Press [q] to exit</text> | ||||||||||||||||||||||||||
| <text dim={true}>Controls: [q] Quit [/] Search [Esc] Clear search</text> | ||||||||||||||||||||||||||
| <spacer /> | ||||||||||||||||||||||||||
| <text dim={true}>Refreshed automatically</text> | ||||||||||||||||||||||||||
| </box> | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove
as anytype assertion inTextInputJSXreturnLine 121 violates repo TS rules (
any+ assertion without justification). Return a concrete type instead (e.g., make the function returnTextInputand returnref.currentdirectly).Suggested fix
As per coding guidelines: "
**/*.{ts,tsx}: Use TypeScript strict mode. Noanywithout an inline comment explaining why... No type assertions without an inline comment explaining why."🤖 Prompt for AI Agents
Source: Coding guidelines