MIRROR: javascript for ๐Ÿœ's, a tiny runtime with big ambitions
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

normalize enter key event to emit '\r'

+349 -351
-218
examples/midi2bells.js
··· 1 - import { readFile } from 'node:fs'; 2 - 3 - const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; 4 - 5 - function midiToNote(midi) { 6 - const octave = Math.floor(midi / 12) - 1; 7 - const name = NOTE_NAMES[midi % 12]; 8 - return { name, octave, str: `${name}${octave}` }; 9 - } 10 - 11 - function readVarLen(buf, pos) { 12 - let value = 0; 13 - let byte; 14 - do { 15 - byte = buf[pos++]; 16 - value = (value << 7) | (byte & 0x7f); 17 - } while (byte & 0x80); 18 - return { value, pos }; 19 - } 20 - 21 - function parseMidi(buf) { 22 - let pos = 0; 23 - 24 - const headerChunk = String.fromCharCode(buf[0], buf[1], buf[2], buf[3]); 25 - if (headerChunk !== 'MThd') throw new Error('Not a MIDI file'); 26 - const format = (buf[8] << 8) | buf[9]; 27 - const numTracks = (buf[10] << 8) | buf[11]; 28 - const division = (buf[12] << 8) | buf[13]; 29 - pos = 14; 30 - 31 - console.log(`Format: ${format}, Tracks: ${numTracks}, Division: ${division}`); 32 - 33 - const tracks = []; 34 - let tempo = 500000; 35 - 36 - for (let t = 0; t < numTracks; t++) { 37 - const chunkType = String.fromCharCode(buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]); 38 - pos += 4; 39 - const chunkLen = (buf[pos] << 24) | (buf[pos + 1] << 16) | (buf[pos + 2] << 8) | buf[pos + 3]; 40 - pos += 4; 41 - 42 - if (chunkType !== 'MTrk') { 43 - pos += chunkLen; 44 - continue; 45 - } 46 - 47 - const endPos = pos + chunkLen; 48 - const events = []; 49 - let absTick = 0; 50 - let runningStatus = 0; 51 - let trackName = `Track ${t}`; 52 - 53 - while (pos < endPos) { 54 - const delta = readVarLen(buf, pos); 55 - absTick += delta.value; 56 - pos = delta.pos; 57 - 58 - let status = buf[pos]; 59 - if (status < 0x80) { 60 - status = runningStatus; 61 - } else { 62 - runningStatus = status; 63 - pos++; 64 - } 65 - 66 - const type = status & 0xf0; 67 - const channel = status & 0x0f; 68 - 69 - if (type === 0x90) { 70 - const note = buf[pos++]; 71 - const velocity = buf[pos++]; 72 - if (velocity > 0) { 73 - events.push({ type: 'on', tick: absTick, note, velocity, channel }); 74 - } else { 75 - events.push({ type: 'off', tick: absTick, note, channel }); 76 - } 77 - } else if (type === 0x80) { 78 - const note = buf[pos++]; 79 - pos++; 80 - events.push({ type: 'off', tick: absTick, note, channel }); 81 - } else if (type === 0xa0 || type === 0xb0 || type === 0xe0) { 82 - pos += 2; 83 - } else if (type === 0xc0 || type === 0xd0) { 84 - pos += 1; 85 - } else if (status === 0xff) { 86 - const metaType = buf[pos++]; 87 - const len = readVarLen(buf, pos); 88 - pos = len.pos; 89 - if (metaType === 0x03) { 90 - trackName = ''; 91 - for (let i = 0; i < len.value; i++) trackName += String.fromCharCode(buf[pos + i]); 92 - } else if (metaType === 0x51 && len.value === 3) { 93 - tempo = (buf[pos] << 16) | (buf[pos + 1] << 8) | buf[pos + 2]; 94 - } 95 - pos += len.value; 96 - } else if (status === 0xf0 || status === 0xf7) { 97 - const len = readVarLen(buf, pos); 98 - pos = len.pos + len.value; 99 - } 100 - } 101 - 102 - tracks.push({ name: trackName, events }); 103 - } 104 - 105 - return { format, numTracks, division, tracks, tempo }; 106 - } 107 - 108 - function buildNoteList(track) { 109 - const notes = []; 110 - const pending = new Map(); 111 - 112 - for (const ev of track.events) { 113 - if (ev.type === 'on') { 114 - pending.set(ev.note, ev.tick); 115 - } else if (ev.type === 'off' && pending.has(ev.note)) { 116 - const startTick = pending.get(ev.note); 117 - const duration = ev.tick - startTick; 118 - const info = midiToNote(ev.note); 119 - notes.push({ ...info, startTick, duration, midi: ev.note }); 120 - pending.delete(ev.note); 121 - } 122 - } 123 - 124 - notes.sort((a, b) => a.startTick - b.startTick || a.midi - b.midi); 125 - return notes; 126 - } 127 - 128 - function notesToBells(notes, division) { 129 - const eighth = division / 2; 130 - let bells = ''; 131 - let currentTick = 0; 132 - 133 - for (let i = 0; i < notes.length; i++) { 134 - const note = notes[i]; 135 - 136 - const chord = [note]; 137 - while (i + 1 < notes.length && notes[i + 1].startTick === note.startTick) { 138 - chord.push(notes[++i]); 139 - } 140 - 141 - const gap = note.startTick - currentTick; 142 - const restCount = Math.round(gap / eighth); 143 - for (let r = 0; r < restCount; r++) bells += '/'; 144 - 145 - const maxDur = Math.max(...chord.map(n => n.duration)); 146 - const eighths = Math.max(1, Math.round(maxDur / eighth)); 147 - const tildes = Math.max(0, eighths - 1); 148 - 149 - if (chord.length > 1) { 150 - bells += '['; 151 - for (const n of chord) { 152 - bells += n.str; 153 - } 154 - for (let t = 0; t < tildes; t++) bells += '~'; 155 - bells += ']'; 156 - } else { 157 - bells += note.str; 158 - for (let t = 0; t < tildes; t++) bells += '~'; 159 - } 160 - 161 - currentTick = note.startTick + maxDur; 162 - } 163 - 164 - return bells; 165 - } 166 - 167 - async function main() { 168 - const path = process.argv[2]; 169 - if (!path) { 170 - console.error('Usage: ant midi2bells.js <path-to-midi>'); 171 - process.exit(1); 172 - } 173 - 174 - const buf = await readFile(path); 175 - const data = new Uint8Array(buf); 176 - const midi = parseMidi(data); 177 - 178 - console.log('\n=== Tracks ==='); 179 - for (const track of midi.tracks) { 180 - const noteEvents = track.events.filter(e => e.type === 'on'); 181 - console.log(`${track.name}: ${noteEvents.length} note-on events`); 182 - } 183 - 184 - const melodicTracks = midi.tracks.filter(t => { 185 - const notes = t.events.filter(e => e.type === 'on' && e.channel !== 9); 186 - return notes.length > 0; 187 - }); 188 - 189 - console.log(`\nMelodic tracks: ${melodicTracks.map(t => t.name).join(', ')}`); 190 - 191 - const bellParts = []; 192 - for (const track of melodicTracks) { 193 - const filteredEvents = track.events.filter(e => e.channel !== 9); 194 - const notes = buildNoteList({ events: filteredEvents }, midi.division); 195 - if (notes.length === 0) continue; 196 - const bells = notesToBells(notes, midi.division); 197 - console.log(`\n--- ${track.name} (${notes.length} notes) ---`); 198 - console.log(bells.substring(0, 200) + (bells.length > 200 ? '...' : '')); 199 - bellParts.push({ name: track.name, bells }); 200 - } 201 - 202 - const order = ['square', 'overdrive', 'bass', 'organ', 'brass']; 203 - const sorted = []; 204 - for (const name of order) { 205 - const found = bellParts.find(p => p.name.toLowerCase().includes(name)); 206 - if (found) sorted.push(found); 207 - } 208 - for (const p of bellParts) { 209 - if (!sorted.includes(p)) sorted.push(p); 210 - } 211 - 212 - const bpm = Math.round(60000000 / midi.tempo); 213 - const combined = `${bpm}${sorted.map(p => p.bells).join('|')}`; 214 - console.log('\n\n=== FINAL BELL NOTATION ===\n'); 215 - console.log(combined); 216 - } 217 - 218 - main();
+295 -110
examples/tui/index.js
··· 1 - import { Screen, List, ProgressBar, Input, Table, colors, box, keys, codes, modal, confirm, pad, padCenter, truncate } from './tuey.js'; 1 + import { Screen, List, ProgressBar, Input, Table, colors, box, keys, codes, modal, confirm, pad, padCenter, truncate, visibleLength } from './tuey.js'; 2 2 3 3 const screen = new Screen({ fullscreen: true, hideCursor: true }); 4 4 ··· 37 37 memUsage: 62, 38 38 diskUsage: 78, 39 39 networkIn: 0, 40 - networkOut: 0 40 + networkOut: 0, 41 + settingsIndex: 0 41 42 }; 42 43 44 + const settings = [ 45 + { key: 'refreshRate', label: 'Refresh Rate', type: 'cycle', options: [500, 1000, 2000, 5000], value: 1000, format: v => `${v}ms` }, 46 + { key: 'logInterval', label: 'Log Interval', type: 'cycle', options: [1000, 2000, 5000, 10000], value: 2000, format: v => `${v / 1000}s` }, 47 + { key: 'logLevel', label: 'Min Log Level', type: 'cycle', options: ['DEBUG', 'INFO', 'WARN', 'ERROR'], value: 'DEBUG', format: v => v }, 48 + { key: 'boxStyle', label: 'Box Style', type: 'cycle', options: ['rounded', 'light', 'heavy', 'double'], value: 'rounded', format: v => v }, 49 + { key: 'confirmQuit', label: 'Confirm on Quit', type: 'toggle', value: true, format: v => v ? 'On' : 'Off' }, 50 + { key: 'simulateStats', label: 'Simulate Stats', type: 'toggle', value: true, format: v => v ? 'On' : 'Off' }, 51 + { key: 'maxLogs', label: 'Max Log Entries', type: 'cycle', options: [50, 100, 250, 500], value: 100, format: v => String(v) } 52 + ]; 53 + 54 + function getSetting(key) { 55 + return settings.find(s => s.key === key).value; 56 + } 57 + 58 + function cycleSetting(index, direction) { 59 + const s = settings[index]; 60 + if (s.type === 'toggle') { 61 + s.value = !s.value; 62 + } else if (s.type === 'cycle') { 63 + const idx = s.options.indexOf(s.value); 64 + const next = (idx + direction + s.options.length) % s.options.length; 65 + s.value = s.options[next]; 66 + } 67 + applySetting(s.key); 68 + } 69 + 70 + function applySetting(key) { 71 + if (key === 'refreshRate') { 72 + clearInterval(statsTimer); 73 + statsTimer = setInterval(updateStats, getSetting('refreshRate')); 74 + } else if (key === 'logInterval') { 75 + clearInterval(logTimer); 76 + logTimer = setInterval(addRandomLog, getSetting('logInterval')); 77 + } else if (key === 'logLevel') { 78 + const levels = ['DEBUG', 'INFO', 'WARN', 'ERROR']; 79 + const minIdx = levels.indexOf(getSetting('logLevel')); 80 + const filtered = logs.filter(l => levels.indexOf(l.level) >= minIdx); 81 + logList.setItems(filtered); 82 + } else if (key === 'maxLogs') { 83 + const max = getSetting('maxLogs'); 84 + while (logs.length > max) logs.shift(); 85 + logList.setItems(logs); 86 + } 87 + } 88 + 43 89 const taskList = new List({ 44 90 items: tasks, 45 91 x: 2, 46 92 y: 5, 47 93 width: 50, 48 94 height: 12, 49 - selectedStyle: colors.bgBlue + colors.bold + colors.white, 95 + selectedStyle: colors.bgGray + colors.white, 50 96 renderItem: task => { 51 97 const statusIcon = 52 98 task.status === 'done' ··· 91 137 } 92 138 93 139 function drawHeader() { 94 - const title = ' TUI Demo - Press ? for help '; 95 - const w = screen.width; 96 - 97 - screen.write(0, 0, colors.bgBlue + colors.bold + colors.white + padCenter(title, w) + codes.reset); 98 - 99 140 const tabs = [ 100 141 { key: '1', name: 'Dashboard', view: 'dashboard' }, 101 142 { key: '2', name: 'Tasks', view: 'tasks' }, ··· 109 150 const style = isActive ? colors.bgWhite + colors.black + colors.bold : colors.dim; 110 151 tabLine += `${style} ${tab.key}:${tab.name} ${codes.reset} `; 111 152 } 112 - screen.write(0, 1, tabLine); 153 + screen.write(0, 0, tabLine); 113 154 } 114 155 115 156 function drawFooter() { ··· 124 165 125 166 function drawDashboard() { 126 167 const w = screen.width; 168 + const sbw = 42; 169 + const tbw = 44; 170 + const rbw = 55; 171 + const rcw = rbw - 4; 172 + const bs = box[getSetting('boxStyle')]; 127 173 128 - screen.write(2, 3, colors.bold + colors.cyan + 'โ”Œโ”€ System Status โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”' + codes.reset); 174 + screen.box(2, 3, sbw, 5, bs, 'System Status', colors.bold + colors.cyan, colors.cyan); 129 175 130 176 cpuBar.setValue(state.cpuUsage); 131 177 memBar.setValue(state.memUsage); 132 178 diskBar.setValue(state.diskUsage); 133 179 134 - screen.write(2, 4, colors.cyan + 'โ”‚' + codes.reset); 135 180 screen.write(4, 4, `CPU: ${cpuBar.render()}`); 136 - screen.write(45, 4, colors.cyan + 'โ”‚' + codes.reset); 137 - 138 - screen.write(2, 5, colors.cyan + 'โ”‚' + codes.reset); 139 181 screen.write(4, 5, `Memory: ${memBar.render()}`); 140 - screen.write(45, 5, colors.cyan + 'โ”‚' + codes.reset); 141 - 142 - screen.write(2, 6, colors.cyan + 'โ”‚' + codes.reset); 143 182 screen.write(4, 6, `Disk: ${diskBar.render()}`); 144 - screen.write(45, 6, colors.cyan + 'โ”‚' + codes.reset); 145 183 146 - screen.write(2, 7, colors.cyan + 'โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜' + codes.reset); 147 - 148 - screen.write(2, 9, colors.bold + colors.yellow + 'โ”Œโ”€ Task Summary โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”' + codes.reset); 184 + screen.box(2, 9, tbw, 6, bs, 'Task Summary', colors.bold + colors.yellow, colors.yellow); 149 185 150 186 const done = tasks.filter(t => t.status === 'done').length; 151 187 const inProgress = tasks.filter(t => t.status === 'in_progress').length; 152 188 const todo = tasks.filter(t => t.status === 'todo').length; 153 189 154 - screen.write(2, 10, colors.yellow + 'โ”‚' + codes.reset); 155 - screen.write( 156 - 4, 157 - 10, 158 - `${colors.green}โœ“ Completed:${codes.reset} ${done} ${colors.yellow}โ— In Progress:${codes.reset} ${inProgress} ${colors.gray}โ—‹ Todo:${codes.reset} ${todo}` 159 - ); 160 - screen.write(45, 10, colors.yellow + 'โ”‚' + codes.reset); 190 + screen.write(4, 10, `${colors.green}โœ“ Completed:${codes.reset} ${done}`); 191 + screen.write(4, 11, `${colors.yellow}โ— In Progress:${codes.reset} ${inProgress}`); 192 + screen.write(4, 12, `${colors.gray}โ—‹ Todo:${codes.reset} ${todo}`); 161 193 162 194 const progress = new ProgressBar({ 163 195 value: done, 164 196 max: tasks.length, 165 - width: 35, 197 + width: 25, 166 198 filledStyle: colors.green, 167 199 showPercent: true 168 200 }); 169 201 170 - screen.write(2, 11, colors.yellow + 'โ”‚' + codes.reset); 171 - screen.write(4, 11, `Progress: ${progress.render()}`); 172 - screen.write(45, 11, colors.yellow + 'โ”‚' + codes.reset); 202 + screen.write(4, 13, `Progress: ${progress.render()}`); 173 203 174 - screen.write(2, 12, colors.yellow + 'โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜' + codes.reset); 204 + screen.box(2, 16, rbw, 7, bs, 'Recent Activity', colors.bold + colors.magenta, colors.magenta); 175 205 176 - screen.write(2, 14, colors.bold + colors.magenta + 'โ”Œโ”€ Recent Activity โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”' + codes.reset); 177 - 178 - for (let i = 0; i < Math.min(5, logs.length); i++) { 179 - const log = logs[logs.length - 1 - i]; 180 - const levelColor = log.level === 'ERROR' ? colors.red : log.level === 'WARN' ? colors.yellow : colors.green; 181 - screen.write(2, 15 + i, colors.magenta + 'โ”‚' + codes.reset); 182 - screen.write(4, 15 + i, `${colors.dim}${log.time}${codes.reset} ${levelColor}${log.level}${codes.reset} ${truncate(log.message, 30)}`); 183 - screen.write(45, 15 + i, colors.magenta + 'โ”‚' + codes.reset); 206 + const recentLogs = logs.slice(-5).reverse(); 207 + for (let i = 0; i < recentLogs.length; i++) { 208 + const log = recentLogs[i]; 209 + const levelColor = log.level === 'ERROR' ? colors.red : log.level === 'WARN' ? colors.yellow : log.level === 'DEBUG' ? colors.cyan : colors.green; 210 + screen.write(4, 17 + i, `${colors.dim}${log.time}${codes.reset} ${levelColor}${pad(log.level, 5)}${codes.reset} ${truncate(log.message, rcw - 16)}`); 184 211 } 185 212 186 - screen.write(2, 20, colors.magenta + 'โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜' + codes.reset); 187 - 188 - if (w > 55) { 189 - screen.box(50, 3, 30, 10, box.rounded, 'Quick Stats', colors.bold + colors.green); 190 - screen.write(52, 5, `${colors.bold}Uptime:${codes.reset} 2h 34m`); 191 - screen.write(52, 6, `${colors.bold}Network In:${codes.reset} ${state.networkIn} KB/s`); 192 - screen.write(52, 7, `${colors.bold}Network Out:${codes.reset} ${state.networkOut} KB/s`); 193 - screen.write(52, 8, `${colors.bold}Active Users:${codes.reset} 42`); 194 - screen.write(52, 9, `${colors.bold}Requests/s:${codes.reset} 1,234`); 195 - screen.write(52, 10, `${colors.bold}Errors:${codes.reset} ${colors.red}3${codes.reset}`); 213 + const qw = 30; 214 + const qx = w - qw - 2; 215 + if (qx > rbw + 4) { 216 + screen.box(qx, 3, qw, 10, bs, 'Quick Stats', colors.bold + colors.green, colors.green); 217 + screen.write(qx + 2, 5, `${colors.bold}Uptime:${codes.reset} 2h 34m`); 218 + screen.write(qx + 2, 6, `${colors.bold}Network In:${codes.reset} ${state.networkIn} KB/s`); 219 + screen.write(qx + 2, 7, `${colors.bold}Network Out:${codes.reset} ${state.networkOut} KB/s`); 220 + screen.write(qx + 2, 8, `${colors.bold}Active Users:${codes.reset} 42`); 221 + screen.write(qx + 2, 9, `${colors.bold}Requests/s:${codes.reset} 1,234`); 222 + screen.write(qx + 2, 10, `${colors.bold}Errors:${codes.reset} ${colors.red}3${codes.reset}`); 196 223 } 197 224 } 198 225 ··· 222 249 screen.write(detailX + 2, 10, `${colors.bold}Priority:${codes.reset} ${selected.priority}`); 223 250 224 251 screen.write(detailX + 2, 12, colors.dim + 'Enter to toggle status' + codes.reset); 225 - screen.write(detailX + 2, 13, colors.dim + 'D to delete task' + codes.reset); 226 - screen.write(detailX + 2, 14, colors.dim + 'N to add new task' + codes.reset); 252 + screen.write(detailX + 2, 13, colors.dim + 'P to cycle priority' + codes.reset); 253 + screen.write(detailX + 2, 14, colors.dim + 'D to delete task' + codes.reset); 254 + screen.write(detailX + 2, 15, colors.dim + 'N to add new task' + codes.reset); 227 255 } 228 256 } 229 257 ··· 238 266 function drawSettings() { 239 267 screen.write(2, 3, colors.bold + colors.cyan + 'Settings' + codes.reset); 240 268 241 - const table = new Table({ 242 - x: 2, 243 - y: 5, 244 - width: 60, 245 - columns: [ 246 - { key: 'setting', header: 'Setting' }, 247 - { key: 'value', header: 'Value' }, 248 - { key: 'description', header: 'Description' } 249 - ], 250 - rows: [ 251 - { setting: 'Theme', value: 'Dark', description: 'UI color scheme' }, 252 - { setting: 'Refresh Rate', value: '1000ms', description: 'Dashboard update interval' }, 253 - { setting: 'Log Level', value: 'INFO', description: 'Minimum log level to display' }, 254 - { setting: 'Notifications', value: 'Enabled', description: 'Show system notifications' }, 255 - { setting: 'Auto-save', value: 'On', description: 'Automatically save changes' } 256 - ], 257 - borderStyle: box.rounded, 258 - headerStyle: colors.bold + colors.cyan 259 - }); 269 + for (let i = 0; i < settings.length; i++) { 270 + const s = settings[i]; 271 + const y = 5 + i; 272 + const isSelected = state.settingsIndex === i; 273 + const style = isSelected ? colors.bgGray + colors.white : ''; 274 + const label = pad(s.label, 20); 275 + const value = s.format(s.value); 276 + const valueStyle = isSelected ? colors.cyan + colors.bold : colors.yellow; 277 + const line = ` ${label} ${valueStyle}${value}${codes.reset} `; 278 + const visible = visibleLength(line); 279 + const padding = Math.max(0, 50 - visible); 260 280 261 - table.render(screen); 281 + if (isSelected) { 282 + screen.write(2, y, style + line + ' '.repeat(padding) + codes.reset); 283 + } else { 284 + screen.write(2, y, line); 285 + } 286 + } 262 287 263 - screen.write(2, 15, colors.dim + 'Press Enter on a setting to modify it' + codes.reset); 288 + screen.write(2, 5 + settings.length + 1, colors.dim + 'โ†‘โ†“: Navigate Enter/โ†’: Change โ†: Change back' + codes.reset); 264 289 } 290 + 291 + let _renderPending = false; 265 292 266 293 function render() { 267 - screen.clear(); 268 - drawHeader(); 294 + if (_renderPending) return; 295 + _renderPending = true; 296 + queueMicrotask(() => { 297 + _renderPending = false; 298 + screen.clear(); 299 + drawHeader(); 269 300 270 - switch (state.view) { 271 - case 'dashboard': 272 - drawDashboard(); 273 - break; 274 - case 'tasks': 275 - drawTasks(); 276 - break; 277 - case 'logs': 278 - drawLogs(); 279 - break; 280 - case 'settings': 281 - drawSettings(); 282 - break; 283 - } 301 + switch (state.view) { 302 + case 'dashboard': 303 + drawDashboard(); 304 + break; 305 + case 'tasks': 306 + drawTasks(); 307 + break; 308 + case 'logs': 309 + drawLogs(); 310 + break; 311 + case 'settings': 312 + drawSettings(); 313 + break; 314 + } 284 315 285 - drawFooter(); 286 - screen.render(); 316 + drawFooter(); 317 + screen.render(); 318 + }); 287 319 } 288 320 289 321 function showHelp() { ··· 338 370 modal(screen, { 339 371 id: 'memory', 340 372 width: 40, 341 - height: 14, 373 + height: 24, 342 374 title: 'Memory Usage', 343 375 titleStyle: colors.bold + colors.yellow, 344 376 borderStyle: box.rounded, ··· 355 387 }, 356 388 render: (buf, _w, _h, ox, oy) => { 357 389 const mem = Ant.stats(); 358 - buf.writeStyled(ox, oy + 1, `${colors.cyan}Arena${codes.reset}`); 359 - buf.writeStyled(ox, oy + 2, ` Used: ${colors.bold}${fmt(mem.arenaUsed)}${codes.reset}`); 360 - buf.writeStyled(ox, oy + 3, ` Size: ${colors.bold}${fmt(mem.arenaSize)}${codes.reset}`); 361 - buf.writeStyled(ox, oy + 5, `${colors.cyan}Process${codes.reset}`); 362 - buf.writeStyled(ox, oy + 6, ` RSS: ${colors.bold}${fmt(mem.rss)}${codes.reset}`); 363 - buf.writeStyled(ox, oy + 8, `${colors.cyan}C Stack${codes.reset}`); 364 - buf.writeStyled(ox, oy + 9, ` Max: ${colors.bold}${fmt(mem.cstack)}${codes.reset}`); 365 - buf.writeStyled(ox, oy + 11, colors.dim + ' g: Run GC m/Esc: Close' + codes.reset); 390 + let y = 1; 391 + 392 + buf.writeStyled(ox, oy + y++, `${colors.cyan}Arena${codes.reset}`); 393 + buf.writeStyled(ox, oy + y++, ` Used: ${colors.bold}${fmt(mem.arenaUsed)}${codes.reset}`); 394 + buf.writeStyled(ox, oy + y++, ` Size: ${colors.bold}${fmt(mem.arenaSize)}${codes.reset}`); 395 + y++; 396 + 397 + if (mem.external) { 398 + buf.writeStyled(ox, oy + y++, `${colors.cyan}External${codes.reset}`); 399 + buf.writeStyled(ox, oy + y++, ` Buffers: ${colors.bold}${fmt(mem.external.buffers)}${codes.reset}`); 400 + buf.writeStyled(ox, oy + y++, ` Code: ${colors.bold}${fmt(mem.external.code)}${codes.reset}`); 401 + buf.writeStyled(ox, oy + y++, ` Collections: ${colors.bold}${fmt(mem.external.collections)}${codes.reset}`); 402 + buf.writeStyled(ox, oy + y++, ` Total: ${colors.bold}${fmt(mem.external.total)}${codes.reset}`); 403 + y++; 404 + } 405 + 406 + if (mem.intern) { 407 + buf.writeStyled(ox, oy + y++, `${colors.cyan}Intern Table${codes.reset}`); 408 + buf.writeStyled(ox, oy + y++, ` Strings: ${colors.bold}${mem.intern.count}${codes.reset}`); 409 + buf.writeStyled(ox, oy + y++, ` Bytes: ${colors.bold}${fmt(mem.intern.bytes)}${codes.reset}`); 410 + y++; 411 + } 412 + 413 + buf.writeStyled(ox, oy + y++, `${colors.cyan}Process${codes.reset}`); 414 + buf.writeStyled(ox, oy + y++, ` RSS: ${colors.bold}${fmt(mem.rss)}${codes.reset}`); 415 + if (mem.virtualSize) { 416 + buf.writeStyled(ox, oy + y++, ` Virtual: ${colors.bold}${fmt(mem.virtualSize)}${codes.reset}`); 417 + } 418 + y++; 419 + 420 + buf.writeStyled(ox, oy + y++, `${colors.cyan}C Stack${codes.reset}`); 421 + buf.writeStyled(ox, oy + y++, ` Max: ${colors.bold}${fmt(mem.cstack)}${codes.reset}`); 422 + y++; 423 + 424 + buf.writeStyled(ox, oy + y, colors.dim + ' g: Run GC m/Esc: Close' + codes.reset); 366 425 } 367 426 }); 368 427 screen.render(); ··· 393 452 switch (key) { 394 453 case 'q': 395 454 case keys.CTRL_C: 396 - confirm(screen, { 397 - title: 'Quit', 398 - message: 'Are you sure you want to quit?' 399 - }).then(confirmed => { 400 - if (confirmed) screen.exit(0); 401 - render(); 402 - }); 455 + if (getSetting('confirmQuit')) { 456 + confirm(screen, { 457 + title: 'Quit', 458 + message: 'Are you sure you want to quit?' 459 + }).then(confirmed => { 460 + if (confirmed) screen.exit(0); 461 + render(); 462 + }); 463 + } else { 464 + screen.exit(0); 465 + } 403 466 return; 404 467 405 468 case '1': ··· 430 493 case 'k': 431 494 if (state.view === 'tasks') taskList.selectPrev(); 432 495 else if (state.view === 'logs') logList.selectPrev(); 496 + else if (state.view === 'settings') state.settingsIndex = Math.max(0, state.settingsIndex - 1); 433 497 render(); 434 498 break; 435 499 ··· 437 501 case 'j': 438 502 if (state.view === 'tasks') taskList.selectNext(); 439 503 else if (state.view === 'logs') logList.selectNext(); 504 + else if (state.view === 'settings') state.settingsIndex = Math.min(settings.length - 1, state.settingsIndex + 1); 440 505 render(); 441 506 break; 442 507 ··· 453 518 break; 454 519 455 520 case keys.ENTER: 521 + case keys.RIGHT: 456 522 if (state.view === 'tasks') { 457 523 const task = taskList.getSelected(); 458 524 if (task) { 459 525 task.status = task.status === 'done' ? 'todo' : task.status === 'todo' ? 'in_progress' : 'done'; 460 526 filterTasks(); 461 527 } 528 + } else if (state.view === 'settings') { 529 + cycleSetting(state.settingsIndex, 1); 530 + } 531 + render(); 532 + break; 533 + 534 + case keys.LEFT: 535 + if (state.view === 'settings') { 536 + cycleSetting(state.settingsIndex, -1); 462 537 } 463 538 render(); 464 539 break; ··· 495 570 } 496 571 break; 497 572 573 + case 'P': 574 + if (state.view === 'tasks') { 575 + const task = taskList.getSelected(); 576 + if (task) { 577 + task.priority = task.priority === 'low' ? 'medium' : task.priority === 'medium' ? 'high' : 'low'; 578 + } 579 + } 580 + render(); 581 + break; 582 + 583 + case 'D': 584 + if (state.view === 'tasks') { 585 + const task = taskList.getSelected(); 586 + if (task) { 587 + const idx = tasks.indexOf(task); 588 + if (idx >= 0) tasks.splice(idx, 1); 589 + filterTasks(); 590 + } 591 + } 592 + render(); 593 + break; 594 + 595 + case 'N': 596 + if (state.view === 'tasks') { 597 + const nameInput = new Input({ width: 30, placeholder: 'Task name...' }); 598 + modal(screen, { 599 + id: 'new-task', 600 + width: 40, 601 + height: 7, 602 + title: 'New Task', 603 + titleStyle: colors.bold + colors.cyan, 604 + borderStyle: box.rounded, 605 + onKey: key => { 606 + if (key === keys.ENTER && nameInput.value) { 607 + const newTask = { 608 + id: tasks.length ? Math.max(...tasks.map(t => t.id)) + 1 : 1, 609 + name: nameInput.value, 610 + status: 'todo', 611 + priority: 'medium' 612 + }; 613 + tasks.push(newTask); 614 + filterTasks(); 615 + screen.popModal('new-task'); 616 + render(); 617 + } else if (key === keys.ESCAPE) { 618 + screen.popModal('new-task'); 619 + render(); 620 + } else { 621 + nameInput.handleKey(key); 622 + screen.render(); 623 + } 624 + return false; 625 + }, 626 + render: (buf, w, h, ox, oy) => { 627 + buf.writeStyled(ox, oy + 1, ' Name: ' + nameInput.render()); 628 + buf.writeStyled(ox, oy + 3, colors.dim + ' Enter: Save Esc: Cancel' + codes.reset); 629 + } 630 + }); 631 + screen.render(); 632 + } 633 + break; 634 + 498 635 case 'd': 499 636 if (state.view === 'tasks') { 500 637 state.taskFilter = 'done'; ··· 511 648 render(); 512 649 }); 513 650 514 - setInterval(() => { 651 + const randomMessages = [ 652 + 'Request processed successfully', 653 + 'User session expired', 654 + 'Cache miss for key: user_prefs', 655 + 'Rate limit exceeded for IP 192.168.1.42', 656 + 'Garbage collection completed', 657 + 'New connection from 10.0.0.15', 658 + 'Query took 234ms to execute', 659 + 'SSL certificate renewal scheduled', 660 + 'Worker thread pool resized to 8', 661 + 'Health check passed', 662 + 'Disk usage above 80% threshold', 663 + 'Backup completed successfully', 664 + 'Failed to resolve hostname: api.example.com', 665 + 'Retrying failed request (attempt 3/5)', 666 + 'Memory pressure detected, evicting cache', 667 + 'Configuration hot-reloaded', 668 + 'Websocket connection dropped', 669 + 'Slow query detected: SELECT * FROM events', 670 + 'Plugin loaded: analytics-v2', 671 + 'Cron job triggered: cleanup_temp_files' 672 + ]; 673 + 674 + const randomLevels = ['INFO', 'INFO', 'INFO', 'DEBUG', 'DEBUG', 'WARN', 'ERROR']; 675 + 676 + function updateStats() { 677 + if (!getSetting('simulateStats')) return; 515 678 state.cpuUsage = Math.max(5, Math.min(95, state.cpuUsage + (Math.random() - 0.5) * 10)); 516 679 state.memUsage = Math.max(20, Math.min(90, state.memUsage + (Math.random() - 0.5) * 5)); 517 680 state.networkIn = Math.floor(Math.random() * 500); ··· 520 683 if (state.view === 'dashboard' && !screen.hasModal()) { 521 684 render(); 522 685 } 523 - }, 1000); 686 + } 687 + 688 + function addRandomLog() { 689 + const now = new Date(); 690 + const time = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`; 691 + const level = randomLevels[Math.floor(Math.random() * randomLevels.length)]; 692 + const message = randomMessages[Math.floor(Math.random() * randomMessages.length)]; 693 + logs.push({ time, level, message }); 694 + 695 + const max = getSetting('maxLogs'); 696 + while (logs.length > max) logs.shift(); 697 + 698 + const levels = ['DEBUG', 'INFO', 'WARN', 'ERROR']; 699 + const minIdx = levels.indexOf(getSetting('logLevel')); 700 + logList.setItems(logs.filter(l => levels.indexOf(l.level) >= minIdx)); 701 + 702 + if (!screen.hasModal()) { 703 + render(); 704 + } 705 + } 706 + 707 + let statsTimer = setInterval(updateStats, getSetting('refreshRate')); 708 + let logTimer = setInterval(addRandomLog, getSetting('logInterval')); 524 709 525 710 screen.start(); 526 711 render();
+52 -21
examples/tui/tuey.js
··· 143 143 return char.repeat(left) + str + char.repeat(right); 144 144 } 145 145 146 + export function sliceAnsi(str, start, end) { 147 + let vis = 0; 148 + let i = 0; 149 + let result = ''; 150 + let lastStyle = ''; 151 + 152 + while (i < str.length) { 153 + if (str[i] === '\x1b') { 154 + const match = str.slice(i).match(/^\x1b\[[0-9;]*m/); 155 + if (match) { 156 + if (vis >= start && (end === undefined || vis < end)) { 157 + result += match[0]; 158 + } else if (vis < start) { 159 + lastStyle = match[0]; 160 + } 161 + i += match[0].length; 162 + continue; 163 + } 164 + } 165 + 166 + if (vis >= start && (end === undefined || vis < end)) { 167 + if (result === '' && lastStyle) result = lastStyle; 168 + result += str[i]; 169 + } 170 + vis++; 171 + if (end !== undefined && vis >= end) break; 172 + i++; 173 + } 174 + 175 + return result; 176 + } 177 + 146 178 export function truncate(str, len, suffix = 'โ€ฆ') { 147 179 const stripped = stripAnsi(str); 148 180 if (stripped.length <= len) return str; ··· 242 274 if (y < 0 || y >= this.height) return; 243 275 const stripped = stripAnsi(text); 244 276 const line = this.lines[y]; 245 - const lineStripped = stripAnsi(line); 246 277 247 - const before = lineStripped.slice(0, Math.max(0, x)); 248 - const after = lineStripped.slice(x + stripped.length); 278 + const before = sliceAnsi(line, 0, x); 279 + const after = sliceAnsi(line, x + stripped.length); 249 280 this.lines[y] = pad(before, x) + text + codes.reset + after; 250 281 } 251 282 ··· 257 288 } 258 289 } 259 290 260 - box(x, y, width, height, style = box.light, title = '', titleStyle = '') { 291 + box(x, y, width, height, style = box.light, title = '', titleStyle = '', borderColor = '') { 261 292 if (height < 2 || width < 2) return; 262 293 if (!style || !style.h) { 263 294 console.error("box() style undefined:", style); ··· 265 296 process.exit(1); 266 297 } 267 298 268 - const top = style.tl + style.h.repeat(width - 2) + style.tr; 269 - const bottom = style.bl + style.h.repeat(width - 2) + style.br; 299 + const bc = borderColor || ''; 300 + const top = bc + style.tl + style.h.repeat(width - 2) + style.tr + codes.reset; 301 + const bottom = bc + style.bl + style.h.repeat(width - 2) + style.br + codes.reset; 270 302 271 303 this.writeStyled(x, y, top); 272 304 this.writeStyled(x, y + height - 1, bottom); 273 305 274 306 for (let row = y + 1; row < y + height - 1 && row < this.height; row++) { 275 307 if (row < 0) continue; 276 - this.writeStyled(x, row, style.v); 277 - this.writeStyled(x + width - 1, row, style.v); 308 + this.writeStyled(x, row, bc + style.v + codes.reset); 309 + this.writeStyled(x + width - 1, row, bc + style.v + codes.reset); 278 310 } 279 311 280 312 if (title) { 281 313 const titleStr = ` ${title} `; 282 - const titleX = x + Math.floor((width - visibleLength(titleStr)) / 2); 314 + const titleX = x + 2; 283 315 this.writeStyled(titleX, y, titleStyle + titleStr + codes.reset); 284 316 } 285 317 } ··· 307 339 this._width = this.stdout.columns || 80; 308 340 this._height = this.stdout.rows || 24; 309 341 this._buffer = new Buffer(this._width, this._height); 310 - this._prevBuffer = null; 311 342 this._running = false; 312 343 this._keyHandlers = []; 313 344 this._resizeHandlers = []; ··· 371 402 this._width = this.stdout.columns || 80; 372 403 this._height = this.stdout.rows || 24; 373 404 this._buffer.resize(this._width, this._height); 374 - this._prevBuffer = null; 375 405 for (const handler of this._resizeHandlers) { 376 406 handler(this._width, this._height); 377 407 } ··· 428 458 } 429 459 } 430 460 431 - box(x, y, width, height, style = box.light, title = '', titleStyle = '') { 432 - this._buffer.box(x, y, width, height, style, title, titleStyle); 461 + box(x, y, width, height, style = box.light, title = '', titleStyle = '', borderColor = '') { 462 + this._buffer.box(x, y, width, height, style, title, titleStyle, borderColor); 433 463 } 434 464 435 465 pushModal(options) { ··· 489 519 if (y + row < 0) continue; 490 520 const modalLine = modalBuffer.lines[row]; 491 521 const bgLine = this._buffer.lines[y + row]; 492 - const bgStripped = stripAnsi(bgLine); 493 522 494 - const before = bgStripped.slice(0, Math.max(0, x)); 495 - const after = bgStripped.slice(x + width); 523 + const before = sliceAnsi(bgLine, 0, x); 524 + const after = sliceAnsi(bgLine, x + width); 496 525 497 - this._buffer.lines[y + row] = pad(before, x) + modalLine + after; 526 + this._buffer.lines[y + row] = pad(before, x) + codes.reset + modalLine + codes.reset + after; 498 527 } 499 528 } 500 529 ··· 512 541 output + 513 542 codes.syncEnd 514 543 ); 515 - this._prevBuffer = this._buffer.clone(); 516 544 } 517 545 518 546 exit(code = 0) { ··· 620 648 const isSelected = i === this.index; 621 649 const style = isSelected ? this.selectedStyle : this.normalStyle; 622 650 623 - screen.write(this.x, row, pad(style + text + codes.reset, this.width)); 651 + const styledText = style ? text.replaceAll(codes.reset, codes.reset + style) : text; 652 + const visible = visibleLength(text); 653 + const padding = Math.max(0, this.width - visible); 654 + screen.write(this.x, row, style + styledText + ' '.repeat(padding) + codes.reset); 624 655 } 625 656 626 657 for (let i = end - this.scrollOffset; i < this.height; i++) { ··· 887 918 }, 888 919 render: (buf, w, h, ox, oy) => { 889 920 buf.writeStyled(ox, oy + 1, padCenter(message, w)); 890 - buf.writeStyled(ox, oy + 3, padCenter(`${colors.green}[Y]es${codes.reset} ${colors.red}[N]o${codes.reset}`, w + 20)); 921 + buf.writeStyled(ox, oy + 3, padCenter(`${colors.green}[Y]es${codes.reset} ${colors.red}[N]o${codes.reset}`, w)); 891 922 } 892 923 }); 893 924 screen.render(); ··· 919 950 for (let i = 0; i < lines.length; i++) { 920 951 buf.writeStyled(ox, oy + i + 1, padCenter(lines[i], w)); 921 952 } 922 - buf.writeStyled(ox, oy + lines.length + 2, padCenter(`${colors.dim}[Enter] OK${codes.reset}`, w + 10)); 953 + buf.writeStyled(ox, oy + lines.length + 2, padCenter(`${colors.dim}[Enter] OK${codes.reset}`, w)); 923 954 } 924 955 }); 925 956 screen.render();
+1 -1
meson/ant.version
··· 1 - 0.6.3 1 + 0.6.4
+1 -1
src/modules/process.c
··· 478 478 } 479 479 480 480 if (c == '\r' || c == '\n') { 481 - emit_keypress_event(js, "\n", 1, "return", false, false, false, "\n", 1); 481 + emit_keypress_event(js, "\r", 1, "return", false, false, false, "\r", 1); 482 482 continue; 483 483 } 484 484