@origami-minecraft/devbuilds
Version:
Origami is a terminal-first Minecraft launcher that supports authentication, installation, and launching of Minecraft versions — with built-in support for Microsoft accounts, mod loaders, profile management, and more. Designed for power users, modders, an
255 lines (208 loc) • 7.62 kB
text/typescript
import chalk from 'chalk';
import Spinnies, { Color } from 'spinnies';
import { v4 } from 'uuid';
type LogType = 'log' | 'warn' | 'error' | 'progress' | 'success';
const tags = {
log: chalk.hex('#00c4cc')('📝 LOG'),
warn: chalk.hex('#ffc107')('⚠️ WARN'),
error: chalk.hex('#ff4d6d')('❌ ERROR'),
progress: chalk.hex('#b388ff')('⏳ PROGRESS'),
success: chalk.greenBright('✅ SUCCESS'),
};
const logPrefix = chalk.hex('#ff80ab').bold('ORIGAMI LOG');
function getTime(): string {
return chalk.gray(`[${new Date().toLocaleTimeString()}]`);
}
function formatMessage(type: LogType, msg: string): string {
return `${getTime()} ${logPrefix}: ${tags[type]}: ${msg}`;
}
let loggers = {
log: (msg: string) => console.log(formatMessage('log', msg)),
warn: (msg: string) => console.warn(formatMessage('warn', msg)),
error: (msg: string) => console.error(formatMessage('error', msg)),
progress: (msg: string) => console.log(formatMessage('progress', msg)),
success: (msg: string) => console.log(formatMessage('success', msg)),
};
type ProgressState = {
startTime: number;
total: number;
value: number;
name: string;
id: string;
hideTaskLogs: boolean;
};
export interface Bar {
increment: () => void;
update: (amount: number) => void;
total: (newTotal?: number) => void;
stop: (fail?: boolean) => void;
};
function formatTime(ms: number): string {
const seconds = Math.floor(ms / 1000) % 60;
const minutes = Math.floor(ms / (60 * 1000)) % 60;
const hours = Math.floor(ms / (60 * 60 * 1000));
const parts: string[] = [];
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0 || hours > 0) parts.push(`${minutes}m`);
parts.push(`${seconds}s`);
return parts.join(' ');
}
function renderBar(percent: number, width: number = 30): string {
const complete = Math.round(percent * width);
const incomplete = width - complete;
return '█'.repeat(complete) + '░'.repeat(incomplete);
}
const MAX_VISIBLE_BARS = 7;
export class ProgressReport {
private bars: Map<string, ProgressState> = new Map();
private spinners: Spinnies = new Spinnies();
private renderInterval: NodeJS.Timeout | null = null;
private visible: Set<string> = new Set();
private hidden: string[] = [];
constructor() {}
private startRenderLoop(): void {
if (this.renderInterval) return;
this.renderInterval = setInterval(() => {
this.renderAll();
if (this.bars.size === 0) this.stopRenderLoop();
}, 100);
}
private stopRenderLoop(): void {
if (this.renderInterval) {
clearInterval(this.renderInterval);
this.renderInterval = null;
}
}
private task_logs(spinner: ProgressState, prog: number = 0, fail: boolean = false, str: string = "no logs."): { text: string, color: Color } {
switch (prog) {
case 0:
return { text: formatMessage('progress', `Starting Task ${chalk.yellow(`\`${spinner.name}\``)}`), color: 'white' };
case 1:
return { text: formatMessage('progress', `Task ${fail ? chalk.red(`\`${spinner.name}\``) : chalk.green(`\`${spinner.name}\``)} ${fail ? 'failed.' : 'has ended successfully!'}`), color: 'white' };
default:
return { text: formatMessage('progress', `Task ${chalk.grey(`\`${spinner.name}\``)}: ${str}`), color: 'white' };
}
}
create(name: string, total: number, hideTaskLogs: boolean = false): Bar | null {
if (this.bars.has(name)) {
loggers.warn(`Progress bar '${name}' already exists.`);
return null;
}
const spinner_id = v4();
const spinner_data: ProgressState = {
startTime: Date.now(),
total,
value: 0,
name,
id: spinner_id,
hideTaskLogs
};
this.bars.set(name, spinner_data);
if (this.visible.size < MAX_VISIBLE_BARS) {
this.visible.add(name);
this.spinners.add(spinner_id, this.task_logs(spinner_data));
} else {
this.hidden.push(name);
}
return {
increment: () => this.update(name, 1),
update: (amount) => this.updateTo(name, amount),
total: (newTotal) => {
if (newTotal) this.setTotal(name, newTotal);
return this.bars.get(name)?.total || newTotal || 0;
},
stop: (fail = false) => this.stop(name, fail),
};
}
has(name: string) {
return this.bars.has(name);
}
start() {
this.startRenderLoop();
}
update(name: string, amount: number = 1): void {
const bar = this.bars.get(name);
if (bar) {
bar.value += amount;
if (bar.value > bar.total) bar.value = bar.total;
}
}
updateTo(name: string, value: number): void {
const bar = this.bars.get(name);
if (bar) {
bar.value = value;
if (bar.value > bar.total) bar.value = bar.total;
}
}
setTotal(name: string, total: number): void {
const bar = this.bars.get(name);
if (bar) bar.total = total;
}
stop(name: string, fail = false): void {
const spinner = this.bars.get(name);
if (!spinner) return;
this.visible.delete(name);
this.bars.delete(name);
if (!spinner.hideTaskLogs) {
this.spinners.update(spinner.id, this.task_logs(spinner, 1, fail));
this.spinners.succeed(spinner.id);
} else {
this.spinners.remove(spinner.id); // completely remove from terminal
}
const next = this.hidden.shift();
if (next) {
const nextBar = this.bars.get(next);
if (nextBar) {
this.spinners.add(nextBar.id, this.task_logs(nextBar));
this.visible.add(next);
}
}
}
stopAll(fail = false): void {
for (const name of [...this.bars.keys()]) {
this.stop(name, fail);
}
}
private renderAll(): void {
for (const name of this.visible) {
const bar = this.bars.get(name);
if (!bar || bar.value >= bar.total) continue;
if (bar.value >= bar.total) {
this.stop(name);
continue;
}
const percent = bar.total ? bar.value / bar.total : 0;
const elapsed = Date.now() - bar.startTime;
const eta = bar.value > 0 ? elapsed / bar.value * (bar.total - bar.value) : 0;
const barStr = renderBar(percent);
const line = `|${chalk.cyan(barStr)}| ${(percent * 100).toFixed(1)}% || ETA: ${formatTime(eta)}`;
this.spinners.update(bar.id, this.task_logs(bar, -1, false, line));
}
const completed = [...this.visible].filter(name => {
const bar = this.bars.get(name);
return bar && bar.value >= bar.total;
});
for (const name of completed) {
this.stop(name);
}
}
}
export class Logger {
private _progress: ProgressReport = new ProgressReport();
constructor() {}
log(msg: string) {
loggers.log(msg);
}
success(msg: string) {
loggers.success(msg);
}
progress() {
return this._progress;
}
warn(msg: string) {
loggers.warn(msg);
}
error(...msg: string[]) {
loggers.error(msg.join(" "));
}
}