forked from defunkt/toes
120 lines
2.8 KiB
TypeScript
120 lines
2.8 KiB
TypeScript
import type { App } from '$apps'
|
|
import { TOES_URL } from '$apps'
|
|
|
|
const RENDER_DEBOUNCE = 50
|
|
|
|
let _apps: App[] = []
|
|
let _enabled = (process.stdout.isTTY ?? false) && !process.env.DEBUG
|
|
let _lastRender = 0
|
|
let _renderTimer: Timer | undefined
|
|
let _hostLogListeners = new Set<(text: string) => void>()
|
|
let _showEmoji = false
|
|
|
|
export const onHostLog = (cb: (text: string) => void) => {
|
|
_hostLogListeners.add(cb)
|
|
}
|
|
|
|
export const setShowEmoji = (show: boolean) => {
|
|
_showEmoji = show
|
|
scheduleRender()
|
|
}
|
|
|
|
export function appLog(app: App, ...msg: string[]) {
|
|
if (!_enabled) {
|
|
console.log('🐾', `[${app.name}]`, msg.join(' '))
|
|
}
|
|
}
|
|
|
|
export function hostLog(...msg: string[]) {
|
|
const text = msg.join(' ')
|
|
if (!_enabled) console.log('🐾', text)
|
|
for (const listener of _hostLogListeners) listener(text)
|
|
}
|
|
|
|
export function setApps(apps: App[]) {
|
|
_apps = apps
|
|
scheduleRender()
|
|
}
|
|
|
|
const formatStatus = (app: App): string => {
|
|
switch (app.state) {
|
|
case 'running':
|
|
return '\x1b[32m●\x1b[0m'
|
|
case 'starting':
|
|
return '\x1b[33m◐\x1b[0m'
|
|
case 'stopping':
|
|
return '\x1b[33m◑\x1b[0m'
|
|
case 'stopped':
|
|
return '\x1b[90m○\x1b[0m'
|
|
case 'invalid':
|
|
return '\x1b[31m✕\x1b[0m'
|
|
default:
|
|
return '\x1b[90m?\x1b[0m'
|
|
}
|
|
}
|
|
|
|
const formatAppLine = (app: App, indent: boolean): string => {
|
|
const status = formatStatus(app)
|
|
const port = app.port ? `\x1b[90m:${app.port}\x1b[0m` : ''
|
|
const prefix = indent ? ' ' : ''
|
|
|
|
if (_showEmoji) {
|
|
const icon = app.icon ?? '📦'
|
|
return `${prefix}${icon} ${app.name} ${status}${port}`
|
|
} else {
|
|
return `${prefix}${status} ${app.name}${port}`
|
|
}
|
|
}
|
|
|
|
const scheduleRender = () => {
|
|
if (_renderTimer) return
|
|
const elapsed = Date.now() - _lastRender
|
|
const delay = Math.max(0, RENDER_DEBOUNCE - elapsed)
|
|
_renderTimer = setTimeout(() => {
|
|
_renderTimer = undefined
|
|
render()
|
|
}, delay)
|
|
}
|
|
|
|
function render() {
|
|
if (!_enabled) return
|
|
_lastRender = Date.now()
|
|
|
|
const lines: string[] = []
|
|
|
|
// Clear screen and move cursor to top
|
|
lines.push('\x1b[2J\x1b[H')
|
|
|
|
// Header
|
|
lines.push(`\x1b[1m🐾 Toes\x1b[0m \x1b[90m${TOES_URL}\x1b[0m`)
|
|
lines.push('')
|
|
|
|
// Apps section
|
|
const regularApps = _apps.filter(a => !a.tool)
|
|
const tools = _apps.filter(a => a.tool)
|
|
|
|
if (tools.length === 0) {
|
|
// No tools, just list apps without header/indent
|
|
for (const app of regularApps) {
|
|
lines.push(formatAppLine(app, false))
|
|
}
|
|
lines.push('')
|
|
} else {
|
|
if (regularApps.length > 0) {
|
|
lines.push('\x1b[1mApps\x1b[0m')
|
|
for (const app of regularApps) {
|
|
lines.push(formatAppLine(app, true))
|
|
}
|
|
lines.push('')
|
|
}
|
|
|
|
lines.push('\x1b[1mTools\x1b[0m')
|
|
for (const app of tools) {
|
|
lines.push(formatAppLine(app, true))
|
|
}
|
|
lines.push('')
|
|
}
|
|
|
|
process.stdout.write(lines.join('\n'))
|
|
}
|