diff --git a/examples/process-monitor/src/index.tsx b/examples/process-monitor/src/index.tsx index 7d018737..367f5b19 100644 --- a/examples/process-monitor/src/index.tsx +++ b/examples/process-monitor/src/index.tsx @@ -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(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)}%`; +} + +// ── 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([]); + 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,7 +168,12 @@ 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)}%`, @@ -119,6 +181,14 @@ function ProcessMonitor() { 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 ( {/* Header */} @@ -126,17 +196,25 @@ function ProcessMonitor() { 📊 TERMUI PROCESS MONITOR - {sys.hostname} • {sys.platform} ({sys.arch}) • Uptime: {sys.uptime} + {sys.hostname} • {sys.platform} ({sys.arch}) • Uptime: {formatUptime(sys.uptime)} + {/* System Summary Bar */} + + CPU: {cpuGauge} + RAM: {mem.used}/{mem.total} ({mem.percent.toFixed(0)}%) + Procs: {totalProcs} + Up: {formatUptime(sys.uptime)} + + {/* Main Area */} - {/* Left Panel: CPU Line Chart & Memory status */} + {/* Left Panel: CPU Line Chart & Memory status & Process Stats */} - Load: {cpu.percent.toFixed(1)}% + Load: {cpu.percent.toFixed(1)}% ({cpu.count} cores @ {cpu.speed}MHz) + + {/* Process Statistics Panel */} + + + Total Processes: {totalProcs} + CPU Model: {cpu.model.slice(0, 30)} + Load Avg: {cpu.loadAvg.map(l => l.toFixed(2)).join(', ')} + + - {/* Right Panel: Top Processes */} - - - + {/* Right Panel: Search + Top Processes */} + + {/* Search/Filter Bar */} + + 🔍 Filter: + + {filterText.length > 0 && ( + ({filteredProcs.length} matches) + )} + + + + + + {/* Footer */} - Controls: Press [q] to exit + Controls: [q] Quit [/] Search [Esc] Clear search Refreshed automatically diff --git a/examples/quiz-app/src/index.tsx b/examples/quiz-app/src/index.tsx index 71157e9c..c2268216 100644 --- a/examples/quiz-app/src/index.tsx +++ b/examples/quiz-app/src/index.tsx @@ -14,6 +14,7 @@ interface Question { question: string; choices: string[]; correctIndex: number; + explanation?: string; } // ── Quiz Data ───────────────────────────────────────── @@ -28,21 +29,25 @@ const QUESTIONS: Question[] = [ 'Home Tool Markup Language', ], correctIndex: 0, + explanation: 'HTML (Hyper Text Markup Language) is the standard markup language for documents designed to be displayed in a web browser.', }, { question: 'Which keyword declares a constant in JavaScript?', choices: ['var', 'let', 'const', 'def'], correctIndex: 2, + explanation: 'The const keyword declares a block-scoped local variable that cannot be reassigned or redeclared.', }, { question: 'What is the time complexity of binary search?', choices: ['O(n)', 'O(n^2)', 'O(log n)', 'O(1)'], correctIndex: 2, + explanation: 'Binary search works by repeatedly dividing in half the portion of the list that could contain the item, resulting in O(log n) time complexity.', }, { question: 'Which data structure uses LIFO order?', choices: ['Queue', 'Stack', 'Heap', 'Tree'], correctIndex: 1, + explanation: 'A stack is a linear data structure that follows the Last In, First Out (LIFO) principle, where the last element added is the first one removed.', }, { question: 'What does CSS stand for?', @@ -53,6 +58,7 @@ const QUESTIONS: Question[] = [ 'Colorful Style Syntax', ], correctIndex: 2, + explanation: 'CSS (Cascading Style Sheets) is a stylesheet language used to describe the presentation of a document written in HTML or XML.', }, ]; @@ -135,20 +141,33 @@ class SelectableList extends Widget { // header(1) + divider(1) + gap(1) + question(1) + gap(1) // + choices(4) + gap(1) + feedback(1) + gap(1) + footer(1) = 13 content rows // Total = 2 + 2 + 13 = 17 +// ── Progress Bar Helper ──────────────────────────────── +function getProgressBar(percent: number, width = 15): string { + const filledChar = caps.unicode ? '█' : '#'; + const emptyChar = caps.unicode ? '░' : '-'; + const filledCount = Math.min(width, Math.max(0, Math.round((percent / 100) * width))); + const emptyCount = width - filledCount; + return `[${filledChar.repeat(filledCount)}${emptyChar.repeat(emptyCount)}] ${percent}%`; +} + const WIDGET_WIDTH = 72; -const WIDGET_HEIGHT = 19; +const WIDGET_HEIGHT = 22; -class QuizApp extends Widget { +export class QuizApp extends Widget { private currentIndex = 0; private score = 0; + private streak = 0; private answered = false; private lastCorrect = false; private done = false; + private userChoiceIndex = -1; private _header: Text; + private _progressBar: Text; private _questionText: Text; private _choiceList: SelectableList; - private _feedback: Text; + private _feedbackStatus: Text; + private _feedbackDetails: Text; private _footer: Text; constructor() { @@ -167,6 +186,12 @@ class QuizApp extends Widget { { align: 'center' } ); + this._progressBar = new Text( + this.progressBarText(), + { height: 1, fg: { type: 'named', name: 'cyan' } }, + { align: 'center' } + ); + const divider = new Text( '─'.repeat(66), { height: 1, fg: { type: 'named', name: 'brightBlack' } }, @@ -190,12 +215,18 @@ class QuizApp extends Widget { const gap3 = new Box({ height: 1 }); - this._feedback = new Text( + this._feedbackStatus = new Text( '', { bold: true, height: 1, fg: { type: 'named', name: 'green' } }, { align: 'left' } ); + this._feedbackDetails = new Text( + '', + { height: 6, fg: { type: 'named', name: 'white' } }, + { align: 'left' } + ); + const gap4 = new Box({ height: 1 }); this._footer = new Text( @@ -205,13 +236,15 @@ class QuizApp extends Widget { ); this.addChild(this._header); + this.addChild(this._progressBar); this.addChild(divider); this.addChild(gap1); this.addChild(this._questionText); this.addChild(gap2); this.addChild(this._choiceList); this.addChild(gap3); - this.addChild(this._feedback); + this.addChild(this._feedbackStatus); + this.addChild(this._feedbackDetails); this.addChild(gap4); this.addChild(this._footer); } @@ -222,7 +255,17 @@ class QuizApp extends Widget { private headerText(): string { if (this.done) return ' Quiz Complete! '; - return ` Question ${this.currentIndex + 1} / ${QUESTIONS.length} `; + const fireEmoji = caps.unicode ? '🔥 ' : ''; + const streakText = `${fireEmoji}Streak: ${this.streak}`; + return ` Question ${this.currentIndex + 1}/${QUESTIONS.length} ${streakText} `; + } + + private progressBarText(): string { + if (this.done) return ''; + const total = QUESTIONS.length; + const current = this.currentIndex + 1; + const pct = Math.round((current / total) * 100); + return getProgressBar(pct); } private footerHint(): string { @@ -235,20 +278,37 @@ class QuizApp extends Widget { if (this.answered || this.done) return; this.answered = true; + this.userChoiceIndex = choiceIndex; this.lastCorrect = choiceIndex === this.currentQuestion().correctIndex; - if (this.lastCorrect) this.score++; + if (this.lastCorrect) { + this.score++; + this.streak++; + } else { + this.streak = 0; + } - const correctLabel = String.fromCharCode(65 + this.currentQuestion().correctIndex); - const correctText = this.currentQuestion().choices[this.currentQuestion().correctIndex]; + const q = this.currentQuestion(); + const correctLabel = String.fromCharCode(65 + q.correctIndex); + const correctText = q.choices[q.correctIndex]; + const userLabel = String.fromCharCode(65 + choiceIndex); + const userText = q.choices[choiceIndex]; + const statusEmoji = this.lastCorrect ? (caps.unicode ? '✔️ Correct' : 'Correct') : (caps.unicode ? '❌ Incorrect' : 'Incorrect'); if (this.lastCorrect) { - this._feedback.setStyle({ fg: { type: 'named', name: 'green' }, bold: true, height: 1 }); - this._feedback.setContent(' Correct!'); + this._feedbackStatus.setStyle({ fg: { type: 'named', name: 'green' }, bold: true, height: 1 }); + this._feedbackStatus.setContent(` ${statusEmoji}`); } else { - this._feedback.setStyle({ fg: { type: 'named', name: 'red' }, bold: true, height: 1 }); - this._feedback.setContent(` Wrong — correct: ${correctLabel}) ${correctText}`); + this._feedbackStatus.setStyle({ fg: { type: 'named', name: 'red' }, bold: true, height: 1 }); + this._feedbackStatus.setContent(` ${statusEmoji}`); + } + + let details = ` Your Answer: ${userLabel}) ${userText}\n Correct Answer: ${correctLabel}) ${correctText}`; + if (q.explanation) { + details += `\n\n Explanation:\n ${q.explanation}`; } + this._feedbackDetails.setContent(details); + this._header.setContent(this.headerText()); this._footer.setContent(this.footerHint()); this.markDirty(); } @@ -258,7 +318,9 @@ class QuizApp extends Widget { this.currentIndex++; this.answered = false; - this._feedback.setContent(''); + this.userChoiceIndex = -1; + this._feedbackStatus.setContent(''); + this._feedbackDetails.setContent(''); if (this.currentIndex >= QUESTIONS.length) { this.showSummary(); @@ -267,6 +329,7 @@ class QuizApp extends Widget { const q = this.currentQuestion(); this._header.setContent(this.headerText()); + this._progressBar.setContent(this.progressBarText()); this._questionText.setContent(q.question); this._choiceList.setItems(q.choices); this._footer.setContent(this.footerHint()); @@ -279,9 +342,11 @@ class QuizApp extends Widget { const grade = pct >= 80 ? 'Excellent!' : pct >= 60 ? 'Good job!' : 'Keep practicing!'; this._header.setContent(this.headerText()); + this._progressBar.setContent(this.progressBarText()); this._questionText.setContent(`Score: ${this.score} / ${QUESTIONS.length} (${pct}%) ${grade}`); this._choiceList.setItems([]); - this._feedback.setContent(''); + this._feedbackStatus.setContent(''); + this._feedbackDetails.setContent(''); this._footer.setContent(this.footerHint()); this.markDirty(); } @@ -289,14 +354,18 @@ class QuizApp extends Widget { private restart(): void { this.currentIndex = 0; this.score = 0; + this.streak = 0; this.answered = false; this.done = false; + this.userChoiceIndex = -1; const q = this.currentQuestion(); this._header.setContent(this.headerText()); + this._progressBar.setContent(this.progressBarText()); this._questionText.setContent(q.question); this._choiceList.setItems(q.choices); - this._feedback.setContent(''); + this._feedbackStatus.setContent(''); + this._feedbackDetails.setContent(''); this._footer.setContent(this.footerHint()); this.markDirty(); } diff --git a/scratch_status.ts b/scratch_status.ts new file mode 100644 index 00000000..f6ad8687 --- /dev/null +++ b/scratch_status.ts @@ -0,0 +1,8 @@ +import { execFileSync } from 'node:child_process'; +try { + const output = execFileSync('tasklist', [], { encoding: 'utf-8' }); + const lines = output.trim().split('\n'); + console.log('Total processes:', lines.length - 3); +} catch (e) { + console.error(e); +} diff --git a/test_layout.ts b/test_layout.ts new file mode 100644 index 00000000..c34349f1 --- /dev/null +++ b/test_layout.ts @@ -0,0 +1,10 @@ +import { QuizApp } from './examples/quiz-app/src/index.tsx'; + +const quiz = new QuizApp(); +const layoutNode = quiz.getLayoutNode(); + +// Let's inspect the layoutNode children +console.log("LayoutNode Children ids in order:"); +layoutNode.children.forEach((child, index) => { + console.log(`Index ${index}: id=${child.id}, visible=${child.style.visible}, height=${child.style.height}`); +});