From 36c72e8ac645d466aaf00c602e69c9a5cb1ce19f Mon Sep 17 00:00:00 2001 From: ewsgit Date: Fri, 19 Jun 2026 01:56:48 +0000 Subject: [PATCH] new TerminalUI! --- instance/src/index.ts | 302 +++++++++++---------- instance/src/log.ts | 417 ++++++++++------------------- instance/src/system.ts | 118 ++++---- instance/src/systems/terminal.ts | 224 ++++++++++++++++ instance/src/systems/terminalUI.ts | 91 ------- 5 files changed, 574 insertions(+), 578 deletions(-) create mode 100644 instance/src/systems/terminal.ts delete mode 100644 instance/src/systems/terminalUI.ts diff --git a/instance/src/index.ts b/instance/src/index.ts index 96d7a20..8d2eab5 100644 --- a/instance/src/index.ts +++ b/instance/src/index.ts @@ -1,153 +1,149 @@ -import chalk from "chalk"; -// https://github.com/cah4a/trpc-bun-adapter/blob/main/src/createBunHttpHandler.ts TODO: patch this and merge into the instance package -import Log from "./log.ts"; -import type { Sys } from "./system.ts"; -import ApplicationsSystem from "./systems/applications.ts"; -import AuthorizationSystem from "./systems/authorization.ts"; -import ConfigurationSystem from "./systems/configuration.ts"; -import ConsoleCommandsSystem from "./systems/consoleCommands.ts"; -import DatabaseSystem from "./systems/database.ts"; -import EmailSystem from "./systems/email.ts"; -import EventSystem, { WorkspacesEvent } from "./systems/events.ts"; -import FilesystemSystem from "./systems/filesystem.ts"; -import ImageSystem from "./systems/image.ts"; -import NotificationsSystem from "./systems/notifications.ts"; -import ReverseProxySystem from "./systems/reverseProxy.ts"; -import { StringListApplicationSetting } from "./systems/settings/applicationSetting/stringListSetting.ts"; -import SettingsSystem from "./systems/settings.ts"; -import TerminalUISystem from "./systems/terminalUI.ts"; -import TRPCSystem from "./systems/trpc.ts"; -import { createTRPCContext as createWorkspacesTRPCContext, workspacesRouter } from "./systems/trpcRouter.ts"; -import UsersSystem from "./systems/users.ts"; -import WebFrontendSystem from "./systems/webFrontend.ts"; - -export enum InstanceStatus { - Online, - Offline, - StartingUp, - Stopping, -} - -class Instance { - sys: Sys; - log: Log; - webServer!: Deno.HttpServer; - status: InstanceStatus; - - constructor() { - this.log = new Log(this); - - // @ts-ignore Don't know, don't care - this.sys = {}; - - this.sys.event = new EventSystem(this); - this.sys.filesystem = new FilesystemSystem(this); - this.sys.configuration = new ConfigurationSystem(this); - this.sys.database = new DatabaseSystem(this); - this.sys.notifications = new NotificationsSystem(this); - this.sys.consoleCommands = new ConsoleCommandsSystem(this); - this.sys.users = new UsersSystem(this); - this.sys.authorization = new AuthorizationSystem(this); - this.sys.applications = new ApplicationsSystem(this); - this.sys.tRPC = new TRPCSystem(this); - this.sys.image = new ImageSystem(this); - this.sys.settings = new SettingsSystem(this); - this.sys.webFrontend = new WebFrontendSystem(this); - this.sys.email = new EmailSystem(this); - this.sys.reverseProxy = new ReverseProxySystem(this); - this.sys.terminalUI = new TerminalUISystem(this); - - this.status = InstanceStatus.Offline; - } - - async startup() { - this.log.system.info(`=======================================================================`); - this.log.system.info(` ${chalk.hex("FF002E")("/XXX/")}${chalk.hex("70FF00")("/XXX/")}${chalk.hex("0066FF")("/XXX/")}`); - this.log.system.info( - ` ${chalk.hex("FF002E")("/XXX/")}${chalk.hex("70FF00")("/XXX/")}${chalk.hex("0066FF")("/XXX/")} Online Workspace © 2026 Ewsgit -> https://ewsgit.uk`, - ); - this.log.system.info(` ${chalk.hex("FF002E")("/XXX/")}${chalk.hex("70FF00")("/XXX/")}${chalk.hex("0066FF")("/XXX/")}`); - this.log.system.info(`=======================================================================`); - this.log.system.info(`Starting up...`); - - if (this.status !== InstanceStatus.Offline) { - this.log.system.info("Cannot stop"); - return this; - } - - for (const sys of Object.values(this.sys)) { - const subSystemState = await sys.startup(); - - if (subSystemState) { - this.log.system.success(`System '${sys.id}' Startup Complete!`); - } else { - this.log.system.error(`System '${sys.id}' Startup Failed!`); - } - } - - this.sys.event.on(WorkspacesEvent.BeforeStartupComplete, () => { - this.sys.settings.registerApplicationSetting( - new StringListApplicationSetting("core", "quick_shortcuts", this.sys.configuration.defaultQuickShortcuts).setDisplayName("Quick Shortcuts"), - ); - }); - - this.sys.event.invoke(WorkspacesEvent.BeforeStartupComplete); - - // this.webServer = Deno.serve(route(, () => {}) - // // this.sys.tRPC.serve({ - // // routes: { - - // // }, - // // fetch() { - // // // will be executed if it's not a TRPC request - // // return new Response("Unknown path"); - // // }, - // // development: this.sys.configuration.isDevMode, - // // }), - // ); - - this.sys.tRPC.registeredRouters.push({ - basePath: "/api/trpc", - router: workspacesRouter, - createContext: createWorkspacesTRPCContext(this), - }); - - // this.log.system.success(`Listening for http requests on port ${this.webServer.port}`); - - this.log.system.info("Startup complete"); - this.status = InstanceStatus.Online; - - return this; - } - - async promptForRestart(reason: string): Promise { - this.log.system.warning(`Hey Server Admin, THE INSTANCE HAS BEEN PROMPTED FOR RESTART DUE TO '${reason}' please restart when possible.`); - return this; - } - - async shutdown() { - this.status = InstanceStatus.Stopping; - this.sys.consoleCommands.currentCommandInterface.active = true; - this.sys.consoleCommands.currentCommandInterface.cb = () => 0; - this.log.system.info("Shutting down..."); - - this.sys.event.invoke(WorkspacesEvent.BeforeShutdown); - this.status = InstanceStatus.Offline; - - if (process.stdout.cursorTo) { - process.stdout.cursorTo(0, 0); - process.stdout.clearScreenDown(); - process.stdout.cursorTo(0, 0); - } - - process.stdout.write("Shutdown complete!\n"); - process.exit(0); - } -} - -export type { Instance }; - -const INSTANCE = new Instance(); -export default INSTANCE; - -await INSTANCE.startup(); +import chalk from "chalk"; +// https://github.com/cah4a/trpc-bun-adapter/blob/main/src/createBunHttpHandler.ts TODO: patch this and merge into the instance package +import Log from "./log.ts"; +import type { Sys } from "./system.ts"; +import ApplicationsSystem from "./systems/applications.ts"; +import AuthorizationSystem from "./systems/authorization.ts"; +import ConfigurationSystem from "./systems/configuration.ts"; +import ConsoleCommandsSystem from "./systems/consoleCommands.ts"; +import DatabaseSystem from "./systems/database.ts"; +import EmailSystem from "./systems/email.ts"; +import EventSystem, { WorkspacesEvent } from "./systems/events.ts"; +import FilesystemSystem from "./systems/filesystem.ts"; +import ImageSystem from "./systems/image.ts"; +import NotificationsSystem from "./systems/notifications.ts"; +import ReverseProxySystem from "./systems/reverseProxy.ts"; +import { StringListApplicationSetting } from "./systems/settings/applicationSetting/stringListSetting.ts"; +import SettingsSystem from "./systems/settings.ts"; +import TerminalUISystem from "./systems/terminal.ts"; +import TRPCSystem from "./systems/trpc.ts"; +import { createTRPCContext as createWorkspacesTRPCContext, workspacesRouter } from "./systems/trpcRouter.ts"; +import UsersSystem from "./systems/users.ts"; +import WebFrontendSystem from "./systems/webFrontend.ts"; + +export enum InstanceStatus { + Online, + Offline, + StartingUp, + Stopping, +} + +class Instance { + sys: Sys; + log: Log; + webServer!: Deno.HttpServer; + status: InstanceStatus; + + versionString: string = "Pre-Alpha 0.1.0"; + + constructor() { + this.log = new Log(this); + + // @ts-ignore Don't know, don't care + this.sys = {}; + + this.sys.event = new EventSystem(this); + this.sys.filesystem = new FilesystemSystem(this); + this.sys.configuration = new ConfigurationSystem(this); + this.sys.database = new DatabaseSystem(this); + this.sys.notifications = new NotificationsSystem(this); + this.sys.consoleCommands = new ConsoleCommandsSystem(this); + this.sys.users = new UsersSystem(this); + this.sys.authorization = new AuthorizationSystem(this); + this.sys.applications = new ApplicationsSystem(this); + this.sys.tRPC = new TRPCSystem(this); + this.sys.image = new ImageSystem(this); + this.sys.settings = new SettingsSystem(this); + this.sys.webFrontend = new WebFrontendSystem(this); + this.sys.email = new EmailSystem(this); + this.sys.reverseProxy = new ReverseProxySystem(this); + this.sys.terminal = new TerminalUISystem(this); + + this.status = InstanceStatus.Offline; + } + + async startup() { + this.log.system.info(`=======================================================================`); + this.log.system.info(` ${chalk.hex("FF002E")("/XXX/")}${chalk.hex("70FF00")("/XXX/")}${chalk.hex("0066FF")("/XXX/")}`); + this.log.system.info( + ` ${chalk.hex("FF002E")("/XXX/")}${chalk.hex("70FF00")("/XXX/")}${chalk.hex("0066FF")("/XXX/")} Online Workspace © 2026 Ewsgit -> https://ewsgit.uk`, + ); + this.log.system.info(` ${chalk.hex("FF002E")("/XXX/")}${chalk.hex("70FF00")("/XXX/")}${chalk.hex("0066FF")("/XXX/")}`); + this.log.system.info(`=======================================================================`); + this.log.system.info(`Starting up...`); + + if (this.status !== InstanceStatus.Offline) { + this.log.system.info("Cannot stop"); + return this; + } + + for (const sys of Object.values(this.sys)) { + const subSystemState = await sys.startup(); + + if (subSystemState) { + this.log.system.success(`System '${sys.id}' Startup Complete!`); + } else { + this.log.system.error(`System '${sys.id}' Startup Failed!`); + } + } + + this.sys.event.on(WorkspacesEvent.BeforeStartupComplete, () => { + this.sys.settings.registerApplicationSetting( + new StringListApplicationSetting("core", "quick_shortcuts", this.sys.configuration.defaultQuickShortcuts).setDisplayName("Quick Shortcuts"), + ); + }); + + this.sys.event.invoke(WorkspacesEvent.BeforeStartupComplete); + + // this.webServer = Deno.serve(route(, () => {}) + // // this.sys.tRPC.serve({ + // // routes: { + + // // }, + // // fetch() { + // // // will be executed if it's not a TRPC request + // // return new Response("Unknown path"); + // // }, + // // development: this.sys.configuration.isDevMode, + // // }), + // ); + + this.sys.tRPC.registeredRouters.push({ + basePath: "/api/trpc", + router: workspacesRouter, + createContext: createWorkspacesTRPCContext(this), + }); + + // this.log.system.success(`Listening for http requests on port ${this.webServer.port}`); + + this.log.system.info("Startup complete"); + this.status = InstanceStatus.Online; + + return this; + } + + async promptForRestart(reason: string): Promise { + this.log.system.warning(`Hey Server Admin, THE INSTANCE HAS BEEN PROMPTED FOR RESTART DUE TO '${reason}' please restart when possible.`); + return this; + } + + async shutdown() { + this.status = InstanceStatus.Stopping; + this.sys.consoleCommands.currentCommandInterface.active = true; + this.sys.consoleCommands.currentCommandInterface.cb = () => 0; + this.log.system.info("Shutting down..."); + + this.sys.event.invoke(WorkspacesEvent.BeforeShutdown); + this.status = InstanceStatus.Offline; + + this.log.system.info("Shutdown complete!\n"); + process.exit(0); + } +} + +export type { Instance }; + +const INSTANCE = new Instance(); +export default INSTANCE; + +await INSTANCE.startup(); diff --git a/instance/src/log.ts b/instance/src/log.ts index b6aa29f..5e23325 100644 --- a/instance/src/log.ts +++ b/instance/src/log.ts @@ -1,275 +1,142 @@ -import util from "node:util"; -import chalk from "chalk"; -import type { Instance } from "./index.ts"; -import { WorkspacesFeatureFlags } from "./systems/configuration.ts"; - -export enum LogType { - INFO, - WARNING, - ERROR, - SUCCESS, - DEBUG, - RAW, - PROMPT, -} - -class Logger { - logHistory: { - type: LogType; - level: string; - message: (string | Uint8Array)[]; - }[] = []; - private log: Log; - private level: string; - private isRootLogger: boolean; - - constructor(level: string, log: Log, isRootLogger: boolean = false) { - this.level = level; - this.log = log; - this.isRootLogger = isRootLogger; - - // biome-ignore lint/suspicious/noExplicitAny: data could be of any type - global.console.log = (...data: any[]): void => { - if (process.stdout.cursorTo) { - process.stdout.cursorTo(0, this._internal_getWindowSize()[1], () => { - process.stdout.clearLine(1, () => { - let writtenData = ""; - for (const d of data) { - if (d?.toString !== undefined) { - writtenData += util.format(d); - } else { - writtenData += util.inspect(d, { - compact: false, - colors: true, - depth: 3, - breakLength: this._internal_getWindowSize()[0] - this.log.META_LENGTH + 6, - }); - } - writtenData += " "; - } - if (writtenData.endsWith("\n")) { - process.stdout.write(writtenData, () => { - this._internal_writePrompt(); - }); - } else { - process.stdout.write(`${writtenData}\n`, () => { - this._internal_writePrompt(); - }); - } - }); - }); - } else { - let writtenData = ""; - for (const d of data) { - if (d.toString !== undefined) { - writtenData += util.format(d); - } else { - writtenData += util.inspect(d, { - compact: false, - colors: true, - depth: 3, - breakLength: this._internal_getWindowSize()[0] - this.log.META_LENGTH + 6, - }); - } - writtenData += " "; - } - if (writtenData.endsWith("\n")) { - process.stdout.write(writtenData); - } else { - process.stdout.write(`${writtenData}\n`); - } - } - }; - } - - emphasis(...message: (string | Uint8Array)[]) { - return chalk.bold.magenta(message); - } - - rawLog(...message: (string | Uint8Array)[]) { - if (message.length === 0) { - throw new Error("log message is empty"); - } - - return this.logMessage(LogType.RAW, ...message); - } - - // biome-ignore lint/suspicious/noExplicitAny: the message can be of any type - _internal_promptMessage(...message: any[]) { - return this.logMessage(LogType.PROMPT, ...message); - } - - removeCommandPrompt(cb: () => void) { - process.stdout.cursorTo(0, process.stdout.getWindowSize()[1] - 1, () => { - process.stdout.clearScreenDown(); - cb(); - }); - - return this; - } - - info(...message: (string | Uint8Array)[]) { - if (this.level.length === 0) { - throw new Error("log level is empty"); - } - - if (message.length === 0) { - throw new Error("log message is empty"); - } - - return this.logMessage(LogType.INFO, ...message); - } - - success(...message: (string | Uint8Array)[]) { - if (this.level.length === 0) { - throw new Error("log level is empty"); - } - - if (message.length === 0) { - throw new Error("log message is empty"); - } - - return this.logMessage(LogType.SUCCESS, ...message); - } - - warning(...message: (string | Uint8Array)[]) { - if (this.level.length === 0) { - throw new Error("log level is empty"); - } - - if (message.length === 0) { - throw new Error("log message is empty"); - } - - return this.logMessage(LogType.WARNING, ...message); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - error(...message: any[]) { - if (message.length === 0) { - this.logMessage(LogType.ERROR, "log", new Error("log message is empty").stack); - } - - this.logMessage(LogType.ERROR, ...message); - - return this; - } - - debug(...message: (string | Uint8Array)[]) { - // if (!this.log.instance.configurationManager.config.isDevMode) { - // return this; - // } - - if (this.level.length === 0) { - throw new Error("log level is empty"); - } - - if (message.length === 0) { - throw new Error("log message is empty"); - } - - return this.logMessage(LogType.DEBUG, ...message); - } - - // biome-ignore lint/suspicious/noExplicitAny: the message can be of any type - private logMessage(type: LogType, ...message: any[]): this { - this.logHistory.push({ type: type, level: this.level, message: message }); - - let typeString = ""; - - switch (type) { - case LogType.INFO: - typeString = chalk.bold(`${chalk.blue("INF")} ${chalk.white("|")}`); - break; - case LogType.WARNING: - typeString = chalk.bold(`${chalk.yellow("WAR")} ${chalk.white("|")}`); - break; - case LogType.ERROR: - typeString = chalk.bold(`${chalk.red("ERR")} ${chalk.white("|")}`); - break; - case LogType.SUCCESS: - typeString = chalk.bold(`${chalk.green("SUC")} ${chalk.white("|")}`); - break; - case LogType.DEBUG: - typeString = chalk.bold(`${chalk.magenta("DBG")} ${chalk.white("|")}`); - break; - case LogType.PROMPT: - typeString = chalk.bold(`${chalk.cyan("PRM")} ${chalk.white("|")} `); - break; - case LogType.RAW: - typeString = ` `; - break; - } - - this.writeMessage(type, typeString, ...message); - - return this; - } - - _internal_getWindowSize(): [number, number] { - const size = process?.stdout?.getWindowSize?.(); - - return [size?.[0] || 120, size?.[1] || 60]; - } - - _internal_writePrompt() { - // if (!this.log.instance?.sys.configuration?.hasFeature(WorkspacesFeatureFlags.SlashCommands) || !process.stdout.cursorTo) return this; - // process.stdout.cursorTo(0, this._internal_getWindowSize()[1], () => { - // process.stdout.write(`Workspaces Alpha ${this.log.instance.sys.configuration?.isDevMode ? `[${this.emphasis("Dev Mode")}] ` : ""}`, () => { - // // move the cursor to the metaLen+6th column of the 2nd from the bottom row - // process.stdout.cursorTo(this.log.META_LENGTH + 6, this._internal_getWindowSize()[1], () => { - // // write the prompt indicator to the stdout - // process.stdout.write(`> ${this.log.instance.sys.consoleCommands?.rlInterface?.line || ""}`); - // }); - // }); - // }); - } - - private writeMessage(logType: LogType, typeString: string, ...message: any[]) { - // if (logType === LogType.RAW) { - // process.stdout.write( - // chalk.bold(`${typeString}${chalk.yellow(this.level.toUpperCase().slice(0, this.log.META_LENGTH).padEnd(this.log.META_LENGTH))} `) + message.join(" "), - // ); - // return this; - // } - // if (logType === LogType.PROMPT) { - // process.stdout.cursorTo(0, this._internal_getWindowSize()[1], () => { - // process.stdout.clearLine(1, () => { - // process.stdout.write( - // chalk.bold(`${typeString}${chalk.yellow(this.level.toUpperCase().slice(0, this.log.META_LENGTH).padEnd(this.log.META_LENGTH))} `) + - // message.join(" "), - // ); - // }); - // }); - // return this; - // } - // console.log(typeString, chalk.bold(`${chalk.yellow(this.level.toUpperCase().slice(0, this.log.META_LENGTH).padEnd(this.log.META_LENGTH))} `), ...message); - // return this; - } -} - -export type { Logger }; - -export default class Log { - allLogHistory: { - type: LogType; - level: string; - message: (string | Uint8Array)[]; - }[] = []; - system: Logger; - instance: Instance; - readonly META_LENGTH = 28; - - constructor(instance: Instance) { - this.instance = instance; - this.system = this.createLogger("system"); - } - - createLogger(prefix: string): Logger { - return new Logger(prefix, this); - } -} - -/* - * Prompt a message - */ +import util from "node:util"; +import chalk from "chalk"; +import type { Instance } from "./index.ts"; + +export enum LogType { + INFO, + WARNING, + ERROR, + SUCCESS, + DEBUG, + RAW, + PROMPT, +} + +class Logger { + private log: Log; + private level: string; + + constructor(level: string, log: Log) { + this.level = level; + this.log = log; + } + + emphasis(...message: (string | Uint8Array)[]) { + return chalk.bold.magenta(message); + } + + rawLog(...message: (string | Uint8Array)[]) { + if (message.length === 0) { + throw new Error("log message is empty"); + } + + return this.logMessage(LogType.RAW, ...message); + } + + info(...message: (string | Uint8Array)[]) { + if (this.level.length === 0) { + throw new Error("log level is empty"); + } + + if (message.length === 0) { + throw new Error("log message is empty"); + } + + return this.logMessage(LogType.INFO, ...message); + } + + success(...message: (string | Uint8Array)[]) { + if (this.level.length === 0) { + throw new Error("log level is empty"); + } + + if (message.length === 0) { + throw new Error("log message is empty"); + } + + return this.logMessage(LogType.SUCCESS, ...message); + } + + warning(...message: (string | Uint8Array)[]) { + if (this.level.length === 0) { + throw new Error("log level is empty"); + } + + if (message.length === 0) { + throw new Error("log message is empty"); + } + + return this.logMessage(LogType.WARNING, ...message); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error(...message: any[]) { + if (message.length === 0) { + this.logMessage(LogType.ERROR, "log", new Error("log message is empty").stack); + } + + this.logMessage(LogType.ERROR, ...message); + + return this; + } + + debug(...message: (string | Uint8Array)[]) { + // if (!this.log.instance.configurationManager.config.isDevMode) { + // return this; + // } + + if (this.level.length === 0) { + throw new Error("log level is empty"); + } + + if (message.length === 0) { + throw new Error("log message is empty"); + } + + return this.logMessage(LogType.DEBUG, ...message); + } + + // biome-ignore lint/suspicious/noExplicitAny: the message can be of any type + private logMessage(type: LogType, ...message: any[]): this { + this.writeMessage(type, ...message); + + return this; + } + + private writeMessage(logType: LogType, ...message: any[]) { + this.log.allLogHistory.push({ + type: logType, + level: this.level, + message: `${message}`, + }); + + return this; + } +} + +export type { Logger }; + +export default class Log { + allLogHistory: { + type: LogType; + level: string; + message: string; + }[] = []; + system: Logger; + instance: Instance; + readonly META_LENGTH = 28; + + constructor(instance: Instance) { + this.instance = instance; + this.system = this.createLogger("system"); + + // biome-ignore lint/suspicious/noExplicitAny: data could be of any type + global.console.log = (...data: any[]): void => { + this.system.info(...data); + }; + } + + createLogger(prefix: string): Logger { + return new Logger(prefix, this); + } +} diff --git a/instance/src/system.ts b/instance/src/system.ts index 4a42dbd..4caef63 100644 --- a/instance/src/system.ts +++ b/instance/src/system.ts @@ -1,59 +1,59 @@ -import type { Instance } from "./index.ts"; -import type { Logger } from "./log.ts"; -import type ApplicationsSubsystem from "./systems/applications.ts"; -import type AuthorizationSubsystem from "./systems/authorization.ts"; -import type ConfigurationSubsystem from "./systems/configuration.ts"; -import type ConsoleCommandsSubsystem from "./systems/consoleCommands.ts"; -import type DatabaseSubsystem from "./systems/database.ts"; -import type EmailSystem from "./systems/email.ts"; -import type EventSystem from "./systems/events.ts"; -import type FilesystemSubsystem from "./systems/filesystem.ts"; -import type ImageSubsystem from "./systems/image.ts"; -import type NotificationsSubsystem from "./systems/notifications.ts"; -import type ReverseProxySystem from "./systems/reverseProxy.ts"; -import type SettingsSubsystem from "./systems/settings.ts"; -import type TerminalUISystem from "./systems/terminalUI.ts"; -import type TRPCSubsystem from "./systems/trpc.ts"; -import type UsersSubsystem from "./systems/users.ts"; -import type WebFrontendSubsystem from "./systems/webFrontend.ts"; - -export type Sys = { - users: UsersSubsystem; - notifications: NotificationsSubsystem; - filesystem: FilesystemSubsystem; - configuration: ConfigurationSubsystem; - consoleCommands: ConsoleCommandsSubsystem; - authorization: AuthorizationSubsystem; - database: DatabaseSubsystem; - applications: ApplicationsSubsystem; - tRPC: TRPCSubsystem; - image: ImageSubsystem; - settings: SettingsSubsystem; - webFrontend: WebFrontendSubsystem; - email: EmailSystem; - event: EventSystem; - reverseProxy: ReverseProxySystem; - terminalUI: TerminalUISystem; -} & { [key: string]: System }; - -export default abstract class System { - instance: Instance; - readonly log: Logger; - readonly id: string; - - protected constructor(id: string, instance: Instance) { - this.instance = instance; - this.id = id; - this.log = instance.log.createLogger(this.id); - } - - async startup(): Promise { - this.log.info("Starting up..."); - return true; - } - - stop() { - this.log.info(`Stopping System ${this.id}`); - return this; - } -} +import type { Instance } from "./index.ts"; +import type { Logger } from "./log.ts"; +import type ApplicationsSubsystem from "./systems/applications.ts"; +import type AuthorizationSubsystem from "./systems/authorization.ts"; +import type ConfigurationSubsystem from "./systems/configuration.ts"; +import type ConsoleCommandsSubsystem from "./systems/consoleCommands.ts"; +import type DatabaseSubsystem from "./systems/database.ts"; +import type EmailSystem from "./systems/email.ts"; +import type EventSystem from "./systems/events.ts"; +import type FilesystemSubsystem from "./systems/filesystem.ts"; +import type ImageSubsystem from "./systems/image.ts"; +import type NotificationsSubsystem from "./systems/notifications.ts"; +import type ReverseProxySystem from "./systems/reverseProxy.ts"; +import type SettingsSubsystem from "./systems/settings.ts"; +import type TerminalSystem from "./systems/terminal.ts"; +import type TRPCSubsystem from "./systems/trpc.ts"; +import type UsersSubsystem from "./systems/users.ts"; +import type WebFrontendSubsystem from "./systems/webFrontend.ts"; + +export type Sys = { + users: UsersSubsystem; + notifications: NotificationsSubsystem; + filesystem: FilesystemSubsystem; + configuration: ConfigurationSubsystem; + consoleCommands: ConsoleCommandsSubsystem; + authorization: AuthorizationSubsystem; + database: DatabaseSubsystem; + applications: ApplicationsSubsystem; + tRPC: TRPCSubsystem; + image: ImageSubsystem; + settings: SettingsSubsystem; + webFrontend: WebFrontendSubsystem; + email: EmailSystem; + event: EventSystem; + reverseProxy: ReverseProxySystem; + terminal: TerminalSystem; +} & { [key: string]: System }; + +export default abstract class System { + instance: Instance; + readonly log: Logger; + readonly id: string; + + protected constructor(id: string, instance: Instance) { + this.instance = instance; + this.id = id; + this.log = instance.log.createLogger(this.id); + } + + async startup(): Promise { + this.log.info("Starting up..."); + return true; + } + + stop() { + this.log.info(`Stopping System ${this.id}`); + return this; + } +} diff --git a/instance/src/systems/terminal.ts b/instance/src/systems/terminal.ts new file mode 100644 index 0000000..29cb3ec --- /dev/null +++ b/instance/src/systems/terminal.ts @@ -0,0 +1,224 @@ +import { CliRenderer, createCliRenderer, FrameBufferRenderable, type KeyEvent, OptimizedBuffer, RGBA } from "@opentui/core"; +import type { Instance } from "../index.ts"; +import System from "../system.ts"; +import { LogType } from "../log.ts"; + +export default class TerminalUISystem extends System { + constructor(instance: Instance) { + super("terminal_ui", instance); + } + + override async startup(): Promise { + if (!process.stdout) return false; + + await super.startup(); + + const self = this; + let framebuffer: OptimizedBuffer | null = null; + let keyListener: ((key: KeyEvent) => void) | null = null; + let resizeListener: ((width: number, height: number) => void) | null = null; + + let currentInput = ""; + + let cursorVisible = true; + let lastCursorToggle = Date.now(); + + function todo(textContent: string) { + if (!textContent.trim()) return; + + // do nothing right now + + self.instance.log.system.info(textContent); + } + + async function run(renderer: CliRenderer): Promise { + renderer.start(); + + let WIDTH = renderer.terminalWidth; + let HEIGHT = renderer.terminalHeight; + + const framebufferRenderable = new FrameBufferRenderable(renderer, { + id: "tui-container", + width: WIDTH, + height: HEIGHT, + zIndex: 0, + }); + renderer.root.add(framebufferRenderable); + framebuffer = framebufferRenderable.frameBuffer; + + function renderUI(): void { + if (!framebuffer) return; + + const fb = framebuffer; + const w = fb.width; + const h = fb.height; + + const bg = RGBA.fromInts(15, 15, 20, 255); + const borderMuted = RGBA.fromInts(60, 60, 75, 255); + const borderActive = RGBA.fromInts(100, 150, 240, 255); + const textMain = RGBA.fromInts(220, 220, 225, 255); + + const messageTypeColors: { [type in LogType]: RGBA } = { + [LogType.INFO]: RGBA.fromInts(50, 180, 255, 255), + [LogType.WARNING]: RGBA.fromInts(240, 190, 60, 255), + [LogType.ERROR]: RGBA.fromInts(240, 80, 80, 255), + [LogType.SUCCESS]: RGBA.fromInts(240, 80, 80, 255), + [LogType.DEBUG]: RGBA.fromInts(240, 80, 80, 255), + [LogType.RAW]: RGBA.fromInts(240, 80, 80, 255), + [LogType.PROMPT]: RGBA.fromInts(240, 80, 80, 255), + }; + + const messageLevel = RGBA.fromInts(250, 163, 7, 255); + + fb.fillRect(0, 0, w, h, bg); + + const inputAreaHeight = 3; + const inputY = h - inputAreaHeight; + const logAreaHeight = inputY - 1; + + const maxVisibleLogs = Math.max(1, logAreaHeight); + const startingLogIndex = Math.max(0, self.instance.log.allLogHistory.length - maxVisibleLogs); + const visibleLogs = self.instance.log.allLogHistory.slice(startingLogIndex); + + const COMPACT_LOG_TYPE = false; + + for (let i = 0; i < visibleLogs.length; i++) { + const log = visibleLogs[i]; + const currentTypeColor = messageTypeColors[log.type]; + + if (COMPACT_LOG_TYPE) fb.setCell(1, i, "▌", currentTypeColor, bg); + + let typeString = ""; + + switch (log.type) { + case LogType.INFO: + typeString = `${"INF"}`; + break; + case LogType.WARNING: + typeString = `${"WAR"}`; + break; + case LogType.ERROR: + typeString = `${"ERR"}`; + break; + case LogType.SUCCESS: + typeString = `${"SUC"}`; + break; + case LogType.DEBUG: + typeString = `${"DBG"}`; + break; + case LogType.PROMPT: + typeString = `${"PRM"} `; + break; + case LogType.RAW: + typeString = ` `; + break; + } + + if (!COMPACT_LOG_TYPE) + for (let charIdx = 0; charIdx < typeString.length; charIdx++) { + fb.setCell(1 + charIdx, i, typeString[charIdx], messageTypeColors[log.type], bg); + } + + for (let charIdx = 0; charIdx < log.level.length; charIdx++) { + fb.setCell(COMPACT_LOG_TYPE ? 2 + charIdx : 1 + typeString.length + 1 + charIdx, i, log.level[charIdx], messageLevel, bg); + } + + for (let charIdx = 0; charIdx < log.message.length; charIdx++) { + fb.setCell(COMPACT_LOG_TYPE ? 2 + 16 + charIdx : 1 + typeString.length + 1 + 16 + charIdx, i, log.message[charIdx], textMain, bg); + } + } + + for (let x = 0; x < w; x++) { + fb.setCell(x, inputY, "─", borderMuted, bg); + fb.setCell(x, h - 1, "─", borderMuted, bg); + } + for (let y = inputY; y < h; y++) { + fb.setCell(0, y, "│", borderMuted, bg); + fb.setCell(w - 1, y, "│", borderMuted, bg); + } + fb.setCell(0, inputY, "╭", borderMuted, bg); + fb.setCell(w - 1, inputY, "╮", borderMuted, bg); + fb.setCell(0, h - 1, "╰", borderMuted, bg); + fb.setCell(w - 1, h - 1, "╯", borderMuted, bg); + + const textY = inputY + 1; + const prompt = `OnlineWorkspace ${self.instance.versionString} > `; + + for (let i = 0; i < prompt.length; i++) { + fb.setCell(1 + i, textY, prompt[i], borderActive, bg); + } + + const maxInputDisplayWidth = w - prompt.length - 4; + const viewInput = currentInput.substring(Math.max(0, currentInput.length - maxInputDisplayWidth)); + + for (let i = 0; i < viewInput.length; i++) { + fb.setCell(1 + prompt.length + i, textY, viewInput[i], textMain, bg); + } + + if (Date.now() - lastCursorToggle > 500) { + cursorVisible = !cursorVisible; + lastCursorToggle = Date.now(); + } + + if (cursorVisible) { + const cursorX = 1 + prompt.length + viewInput.length; + if (cursorX < w - 1) { + fb.setCell(cursorX, textY, "█", borderActive, bg); + } + } + } + + keyListener = (key: KeyEvent) => { + if (key.name === "return" || key.name === "enter") { + todo(currentInput); + currentInput = ""; + cursorVisible = true; + } else if (key.name === "backspace") { + currentInput = currentInput.slice(0, -1); + cursorVisible = true; + } else if (key.sequence && key.sequence.length === 1) { + currentInput += key.sequence; + cursorVisible = true; + } + }; + renderer.keyInput.on("keypress", keyListener); + + resizeListener = (width: number, height: number) => { + WIDTH = width; + HEIGHT = height; + if (framebuffer) { + framebuffer.resize(width, height); + } + }; + renderer.on("resize", resizeListener); + + renderer.setFrameCallback(async () => { + renderUI(); + }); + } + + function destroy(renderer: CliRenderer): void { + renderer.clearFrameCallbacks(); + + if (resizeListener) { + renderer.off("resize", resizeListener); + resizeListener = null; + } + + if (keyListener) { + renderer.keyInput.off("keypress", keyListener); + keyListener = null; + } + + renderer.root.remove("tui-container"); + framebuffer = null; + } + + const renderer = await createCliRenderer({ + exitOnCtrlC: true, + }); + await run(renderer); + + return true; + } +} diff --git a/instance/src/systems/terminalUI.ts b/instance/src/systems/terminalUI.ts deleted file mode 100644 index a3f8375..0000000 --- a/instance/src/systems/terminalUI.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { CliRenderer, createCliRenderer, FrameBufferRenderable, type KeyEvent, OptimizedBuffer, RGBA } from "@opentui/core"; -import type { Instance } from "../index.ts"; -import System from "../system.ts"; - -export default class TerminalUISystem extends System { - constructor(instance: Instance) { - super("terminal_ui", instance); - } - - override async startup(): Promise { - if (!process.stdout) return false; - - await super.startup(); - - let framebuffer: OptimizedBuffer | null = null; - let resizeListener: ((width: number, height: number) => void) | null = null; - - async function run(renderer: CliRenderer): Promise { - renderer.start(); - - let WIDTH = renderer.terminalWidth; - let HEIGHT = renderer.terminalHeight; - - const framebufferRenderable = new FrameBufferRenderable(renderer, { - id: "display", - width: WIDTH, - height: HEIGHT, - zIndex: 0, - }); - renderer.root.add(framebufferRenderable); - framebuffer = framebufferRenderable.frameBuffer; - - function renderWelcome(): void { - if (!framebuffer) return; - - const fb = framebuffer; - const totalWidth = fb.width; - const totalHeight = fb.height; - - const bgColor = RGBA.fromInts(15, 15, 20, 255); - const welcomeColor = RGBA.fromInts(50, 220, 100, 255); - - fb.fillRect(0, 0, totalWidth, totalHeight, bgColor); - - const message = "Welcome to the System!"; - - const startX = Math.floor((totalWidth - message.length) / 2); - const startY = Math.floor(totalHeight / 2); - - if (startX >= 0 && startY >= 0) { - for (let i = 0; i < message.length; i++) { - fb.setCell(startX + i, startY, message[i], welcomeColor, bgColor); - } - } - } - - resizeListener = (width: number, height: number) => { - WIDTH = width; - HEIGHT = height; - if (framebuffer) { - framebuffer.resize(width, height); - } - }; - renderer.on("resize", resizeListener); - - renderer.setFrameCallback(async () => { - renderWelcome(); - }); - } - - function destroy(renderer: CliRenderer): void { - renderer.clearFrameCallbacks(); - - if (resizeListener) { - renderer.off("resize", resizeListener); - resizeListener = null; - } - - renderer.root.remove("display"); - framebuffer = null; - } - - const renderer = await createCliRenderer({ - exitOnCtrlC: true, - targetFps: 30, - }); - await run(renderer); - - return true; - } -} -- 2.51.2