repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources15d ago| #1 | /** |
| #2 | * HERMES x402 TUI — Full-Screen Renderer |
| #3 | * |
| #4 | * Merges panels side-by-side and writes the complete frame to stdout |
| #5 | * using ANSI escape codes. No external TUI framework needed. |
| #6 | */ |
| #7 | |
| #8 | import chalk from 'chalk'; |
| #9 | import type { DashboardState } from './state.js'; |
| #10 | import { renderHeader } from './panels/header.js'; |
| #11 | import { renderOODA } from './panels/ooda.js'; |
| #12 | import { renderMarket } from './panels/market.js'; |
| #13 | import { renderPayments } from './panels/payments.js'; |
| #14 | import { renderLog } from './panels/log.js'; |
| #15 | |
| #16 | // ANSI escape helpers |
| #17 | const CLEAR_SCREEN = '\x1b[2J\x1b[H'; |
| #18 | const HIDE_CURSOR = '\x1b[?25l'; |
| #19 | const SHOW_CURSOR = '\x1b[?25h'; |
| #20 | |
| #21 | export function enableRawMode(): void { |
| #22 | process.stdout.write(HIDE_CURSOR); |
| #23 | if (process.stdin.setRawMode) process.stdin.setRawMode(true); |
| #24 | } |
| #25 | |
| #26 | export function disableRawMode(): void { |
| #27 | process.stdout.write(SHOW_CURSOR); |
| #28 | if (process.stdin.setRawMode) process.stdin.setRawMode(false); |
| #29 | } |
| #30 | |
| #31 | // Plain text column width (strips ANSI color codes) |
| #32 | function visLen(s: string): number { |
| #33 | // eslint-disable-next-line no-control-regex |
| #34 | return s.replace(/\x1b\[[0-9;]*m/g, '').length; |
| #35 | } |
| #36 | |
| #37 | function padRight(s: string, targetLen: number): string { |
| #38 | const current = visLen(s); |
| #39 | return s + ' '.repeat(Math.max(0, targetLen - current)); |
| #40 | } |
| #41 | |
| #42 | /** Merge multiple columns side-by-side into combined line array */ |
| #43 | function mergeColumns(cols: string[][], colWidths: number[]): string[] { |
| #44 | const maxHeight = Math.max(...cols.map(c => c.length)); |
| #45 | const merged: string[] = []; |
| #46 | for (let i = 0; i < maxHeight; i++) { |
| #47 | let row = ''; |
| #48 | for (let j = 0; j < cols.length; j++) { |
| #49 | const cell = cols[j]![i] ?? ''; |
| #50 | row += padRight(cell, colWidths[j]!); |
| #51 | } |
| #52 | merged.push(row); |
| #53 | } |
| #54 | return merged; |
| #55 | } |
| #56 | |
| #57 | export function renderFrame(state: DashboardState): void { |
| #58 | const { columns: termWidth = 120, rows: termHeight = 40 } = process.stdout; |
| #59 | const width = Math.max(termWidth - 2, 80); |
| #60 | |
| #61 | // Column widths (inner content) |
| #62 | const oodaW = 34; |
| #63 | const marketW = 36; |
| #64 | const payW = 36; |
| #65 | |
| #66 | // Number of body rows available |
| #67 | const headerLines = 9; // header panel height |
| #68 | const logLines = 8; |
| #69 | const footerLines = 2; |
| #70 | const bodyHeight = Math.max(10, termHeight - headerLines - logLines - footerLines); |
| #71 | |
| #72 | // Render each panel |
| #73 | const header = renderHeader(state, width); |
| #74 | const ooda = renderOODA(state, bodyHeight); |
| #75 | const market = renderMarket(state, bodyHeight); |
| #76 | const pay = renderPayments(state, bodyHeight); |
| #77 | const log = renderLog(state, width, logLines); |
| #78 | |
| #79 | // ─── Body: 3 columns separated by ║ borders ──────────────────────────────── |
| #80 | const maxRows = Math.max(ooda.length, market.length, pay.length); |
| #81 | const bodyLines: string[] = []; |
| #82 | for (let i = 0; i < maxRows; i++) { |
| #83 | const o = padRight(ooda[i] ?? '', oodaW); |
| #84 | const m = padRight(market[i] ?? '', marketW); |
| #85 | const p = padRight(pay[i] ?? '', payW); |
| #86 | const remaining = Math.max(0, width - oodaW - marketW - payW - 6); |
| #87 | bodyLines.push( |
| #88 | chalk.cyan('║') + ' ' + o + chalk.cyan('║') + ' ' + m + chalk.cyan('║') + ' ' + p + |
| #89 | ' '.repeat(remaining) + chalk.cyan('║'), |
| #90 | ); |
| #91 | } |
| #92 | |
| #93 | // ─── Footer ──────────────────────────────────────────────────────────────── |
| #94 | const uptime = Math.floor((Date.now() - state.startedAt) / 1000); |
| #95 | const uptimeStr = `${Math.floor(uptime / 60)}m ${uptime % 60}s`; |
| #96 | const refreshStr = state.lastRefresh > 0 |
| #97 | ? new Date(state.lastRefresh).toTimeString().slice(0, 8) |
| #98 | : 'fetching…'; |
| #99 | const footerLeft = ` OpenClawd Stack • HERMES x402 • $CLAWD • Solana • Uptime: ${uptimeStr}`; |
| #100 | const footerRight = ` Last: ${refreshStr} • [Q] Quit [R] Refresh `; |
| #101 | const footerPad = Math.max(0, width - footerLeft.length - footerRight.length + 1); |
| #102 | |
| #103 | const border = chalk.cyan('═'.repeat(width)); |
| #104 | const footer = [ |
| #105 | chalk.cyan('╠') + border + chalk.cyan('╣'), |
| #106 | chalk.cyan('║') + |
| #107 | chalk.gray(footerLeft) + |
| #108 | ' '.repeat(footerPad) + |
| #109 | chalk.gray(footerRight) + |
| #110 | chalk.cyan('║'), |
| #111 | chalk.cyan('╚') + border + chalk.cyan('╝'), |
| #112 | ]; |
| #113 | |
| #114 | // ─── Compose & emit ──────────────────────────────────────────────────────── |
| #115 | const allLines = [ |
| #116 | ...header, |
| #117 | ...bodyLines, |
| #118 | ...log, |
| #119 | ...footer, |
| #120 | ]; |
| #121 | |
| #122 | const frame = CLEAR_SCREEN + allLines.join('\n') + '\n'; |
| #123 | process.stdout.write(frame); |
| #124 | } |
| #125 |