diff --git a/src/accesscontrol/main.ts b/src/accesscontrol/main.ts index 38a08fc..a29959b 100644 --- a/src/accesscontrol/main.ts +++ b/src/accesscontrol/main.ts @@ -222,6 +222,7 @@ function main(args: string[]) { return; } else if (args[0] == "config") { log.info("Launching Access Control TUI..."); + log.setInTerminal(false); try { launchAccessControlTUI(); } catch (error) { diff --git a/src/accesscontrol/tui.ts b/src/accesscontrol/tui.ts index 2392033..125d9b6 100644 --- a/src/accesscontrol/tui.ts +++ b/src/accesscontrol/tui.ts @@ -16,6 +16,7 @@ import { For, Switch, Match, + ScrollContainer, } from "../lib/ccTUI"; import { AccessConfig, @@ -65,7 +66,7 @@ const AccessControlTUI = () => { setConfig(() => loadedConfig); // Tab navigation functions - const tabNames = ["Basic", "Groups", "Welcome", "Warn", "Notice Toast"]; + const tabNames = ["Basic", "Groups", "Welcome", "Warn", "Notice"]; const showError = (message: string) => { setErrorState("show", true); @@ -403,18 +404,23 @@ const AccessControlTUI = () => { ), // Users list - For({ each: () => getSelectedGroup().groupUsers ?? [] }, (user) => - div( - { class: "flex flex-row items-center" }, - label({}, user), - button( - { - class: "ml-1 bg-red text-white", - onClick: () => removeUser(user), - }, - "X", + For( + { + class: "flex flex-col", + each: () => getSelectedGroup().groupUsers ?? [], + }, + (user) => + div( + { class: "flex flex-row items-center" }, + label({}, user), + button( + { + class: "ml-1 bg-red text-white", + onClick: () => removeUser(user), + }, + "X", + ), ), - ), ), ), ); @@ -532,20 +538,17 @@ const AccessControlTUI = () => { return Show( { when: () => errorState().show }, div( - { - class: - "fixed top-1/4 left-1/4 right-1/4 bottom-1/4 bg-red text-white border", - }, - div( - { class: "flex flex-col p-2" }, - label({}, () => errorState().message), - button( - { - class: "mt-2 bg-white text-black", - onClick: hideError, - }, - "OK", - ), + { class: "flex flex-col bg-red " }, + label( + { class: "w-25 text-white", wordWrap: true }, + () => errorState().message, + ), + button( + { + class: "bg-white text-black", + onClick: hideError, + }, + "OK", ), ), ); @@ -597,27 +600,36 @@ const AccessControlTUI = () => { ), // Content area - div({ class: "flex-1 p-2 w-screen" }, TabContent()), + Show( + { when: () => !errorState().show }, + div( + { class: "flex flex-col" }, + ScrollContainer( + { class: "flex-1 p-2", width: 50, showScrollbar: true }, + TabContent(), + ), - // Action buttons - div( - { class: "flex flex-row justify-center p-2" }, - button( - { - class: "bg-green text-white mr-2", - onClick: handleSave, - }, - "Save", - ), - button( - { - class: "bg-gray text-white", - onClick: () => { - // Close TUI - this will be handled by the application framework - error("TUI_CLOSE"); - }, - }, - "Close", + // Action buttons + div( + { class: "flex flex-row justify-center p-2" }, + button( + { + class: "bg-green text-white mr-2", + onClick: handleSave, + }, + "Save", + ), + button( + { + class: "bg-gray text-white", + onClick: () => { + // Close TUI - this will be handled by the application framework + error("TUI_CLOSE"); + }, + }, + "Close", + ), + ), ), ), diff --git a/src/lib/ccLog.ts b/src/lib/ccLog.ts index e509d76..c2bb086 100644 --- a/src/lib/ccLog.ts +++ b/src/lib/ccLog.ts @@ -49,31 +49,30 @@ export class CCLog { * For SECOND interval: YYYY-MM-DD-HH-MM-SS */ private getTimePeriodString(time: number): string { - // Calculate which time period this timestamp falls into const periodStart = Math.floor(time / this.interval) * this.interval; - const periodDate = os.date("*t", periodStart); + const d = os.date("*t", periodStart); if (this.interval >= DAY) { - return `${periodDate.year}-${String(periodDate.month).padStart(2, "0")}-${String(periodDate.day).padStart(2, "0")}`; - } else { - return `[${periodDate.year}-${String(periodDate.month).padStart(2, "0")}-${String(periodDate.day).padStart(2, "0")}] - [${String(periodDate.hour).padStart(2, "0")}-${String(periodDate.min).padStart(2, "0")}-${String(periodDate.sec).padStart(2, "0")}]`; + return `${d.year}-${string.format("%02d", d.month)}-${string.format("%02d", d.day)}`; + } else if (this.interval >= HOUR) { + return `${d.year}-${string.format("%02d", d.month)}-${string.format("%02d", d.day)}_${string.format("%02d", d.hour)}`; + } else if (this.interval >= MINUTE) { + return `${d.year}-${string.format("%02d", d.month)}-${string.format("%02d", d.day)}_${string.format("%02d", d.hour)}-${string.format("%02d", d.min)}`; } + return `${d.year}-${string.format("%02d", d.month)}-${string.format("%02d", d.day)}_${string.format("%02d", d.hour)}-${string.format("%02d", d.min)}-${string.format("%02d", d.sec)}`; } private generateFilePath(baseFilename: string, timePeriod: string): string { - // Extract file extension if present - const fileNameSubStrings = baseFilename.split("."); - let filenameWithoutExt: string; - let extension = ""; + const scriptDir = shell.dir() ?? ""; - if (fileNameSubStrings.length > 1) { - filenameWithoutExt = fileNameSubStrings[0]; - extension = fileNameSubStrings[1]; - } else { - filenameWithoutExt = baseFilename; - } + const [filenameWithoutExt, extension] = baseFilename.includes(".") + ? baseFilename.split(".") + : [baseFilename, "log"]; - return `${shell.dir()}/${filenameWithoutExt}[${timePeriod}].${extension}`; + return fs.combine( + scriptDir, + `${filenameWithoutExt}_${timePeriod}.${extension}`, + ); } private checkAndRotateLogFile() { @@ -154,6 +153,10 @@ export class CCLog { this.writeLine(this.getFormatMsg(msg, LogLevel.Error), colors.red); } + public setInTerminal(value: boolean) { + this.inTerm = value; + } + public close() { if (this.fp !== undefined) { this.fp.close(); diff --git a/src/lib/ccTUI/application.ts b/src/lib/ccTUI/application.ts index 1ee6677..155cc25 100644 --- a/src/lib/ccTUI/application.ts +++ b/src/lib/ccTUI/application.ts @@ -5,8 +5,8 @@ import { UIObject } from "./UIObject"; import { calculateLayout } from "./layout"; import { render as renderTree, clearScreen } from "./renderer"; -import { CCLog } from "../ccLog"; -import { findScrollContainer } from "./scrollContainer"; +import { CCLog, HOUR } from "../ccLog"; +import { setLogger } from "./context"; /** * Main application class @@ -28,7 +28,8 @@ export class Application { const [width, height] = term.getSize(); this.termWidth = width; this.termHeight = height; - this.logger = new CCLog("tui_debug.log", false); + this.logger = new CCLog("tui_debug.log", false, HOUR); + setLogger(this.logger); this.logger.debug("Application constructed."); } diff --git a/src/lib/ccTUI/components.ts b/src/lib/ccTUI/components.ts index ce38fe3..ab9925c 100644 --- a/src/lib/ccTUI/components.ts +++ b/src/lib/ccTUI/components.ts @@ -4,7 +4,16 @@ */ import { UIObject, BaseProps, createTextNode } from "./UIObject"; -import { Accessor, Setter, Signal } from "./reactivity"; +import { + Accessor, + createEffect, + createMemo, + createSignal, + Setter, + Signal, +} from "./reactivity"; +import { For } from "./controlFlow"; +import { logger } from "./context"; /** * Props for div component @@ -14,7 +23,10 @@ export type DivProps = BaseProps & Record; /** * Props for label component */ -export type LabelProps = BaseProps & Record; +export type LabelProps = BaseProps & { + /** Whether to automatically wrap long text. Defaults to false. */ + wordWrap?: boolean; +} & Record; /** * Props for button component @@ -95,10 +107,78 @@ export function div( * label({}, () => `Hello, ${name()}!`) * ``` */ +/** + * Splits a string by whitespace, keeping the whitespace as separate elements. + * This is a TSTL-compatible replacement for `text.split(/(\s+)/)`. + * @param text The text to split. + * @returns An array of words and whitespace. + */ +function splitByWhitespace(text: string): string[] { + const parts: string[] = []; + let currentWord = ""; + let currentWhitespace = ""; + + for (const char of text) { + if (char === " " || char === "\t" || char === "\n" || char === "\r") { + if (currentWord.length > 0) { + parts.push(currentWord); + currentWord = ""; + } + currentWhitespace += char; + } else { + if (currentWhitespace.length > 0) { + parts.push(currentWhitespace); + currentWhitespace = ""; + } + currentWord += char; + } + } + + if (currentWord.length > 0) { + parts.push(currentWord); + } + if (currentWhitespace.length > 0) { + parts.push(currentWhitespace); + } + + return parts; +} + export function label( props: LabelProps, text: string | Accessor, ): UIObject { + if (props.wordWrap === true) { + logger?.debug(`label : ${textutils.serialiseJSON(props)}`); + const p = { ...props }; + delete p.wordWrap; + const containerProps: DivProps = { + ...p, + class: `${p.class ?? ""} flex flex-row flex-wrap`, + }; + + if (typeof text === "string") { + // Handle static strings + const words = splitByWhitespace(text); + const children = words.map((word) => createTextNode(word)); + const node = new UIObject("div", containerProps, children); + children.forEach((child) => (child.parent = node)); + return node; + } else { + // Handle reactive strings (Accessor) + const words = createMemo(() => splitByWhitespace(text())); + + const forNode = For( + { class: `${p.class ?? ""} flex flex-row flex-wrap`, each: words }, + (word) => createTextNode(word), + ); + + const node = new UIObject("div", containerProps, [forNode]); + forNode.parent = node; + return node; + } + } + const textNode = createTextNode(text); const node = new UIObject("label", props, [textNode]); textNode.parent = node; diff --git a/src/lib/ccTUI/context.ts b/src/lib/ccTUI/context.ts new file mode 100644 index 0000000..9f668ea --- /dev/null +++ b/src/lib/ccTUI/context.ts @@ -0,0 +1,21 @@ +/** + * Global context for the TUI application. + * This is a simple way to provide global instances like a logger + * to all components without prop drilling. + */ + +import type { CCLog } from "../ccLog"; + +/** + * The global logger instance for the TUI application. + * This will be set by the Application instance on creation. + */ +export let logger: CCLog | undefined; + +/** + * Sets the global logger instance. + * @param l The logger instance. + */ +export function setLogger(l: CCLog): void { + logger = l; +} diff --git a/src/lib/ccTUI/renderer.ts b/src/lib/ccTUI/renderer.ts index 02f1874..6838710 100644 --- a/src/lib/ccTUI/renderer.ts +++ b/src/lib/ccTUI/renderer.ts @@ -332,8 +332,9 @@ function drawNode( ? node.textContent() : node.textContent; - term.setTextColor(textColor ?? colors.white); - term.setBackgroundColor(bgColor ?? colors.black); + if (bgColor !== undefined) { + term.setBackgroundColor(bgColor); + } term.setCursorPos(x, y); term.write(text.substring(0, width)); }