try to wordwrap, but failed

This commit is contained in:
2025-10-12 22:31:35 +08:00
parent bd8e1f9b8d
commit 0ccafa2e2e
7 changed files with 188 additions and 69 deletions

View File

@@ -222,6 +222,7 @@ function main(args: string[]) {
return; return;
} else if (args[0] == "config") { } else if (args[0] == "config") {
log.info("Launching Access Control TUI..."); log.info("Launching Access Control TUI...");
log.setInTerminal(false);
try { try {
launchAccessControlTUI(); launchAccessControlTUI();
} catch (error) { } catch (error) {

View File

@@ -16,6 +16,7 @@ import {
For, For,
Switch, Switch,
Match, Match,
ScrollContainer,
} from "../lib/ccTUI"; } from "../lib/ccTUI";
import { import {
AccessConfig, AccessConfig,
@@ -65,7 +66,7 @@ const AccessControlTUI = () => {
setConfig(() => loadedConfig); setConfig(() => loadedConfig);
// Tab navigation functions // Tab navigation functions
const tabNames = ["Basic", "Groups", "Welcome", "Warn", "Notice Toast"]; const tabNames = ["Basic", "Groups", "Welcome", "Warn", "Notice"];
const showError = (message: string) => { const showError = (message: string) => {
setErrorState("show", true); setErrorState("show", true);
@@ -403,18 +404,23 @@ const AccessControlTUI = () => {
), ),
// Users list // Users list
For({ each: () => getSelectedGroup().groupUsers ?? [] }, (user) => For(
div( {
{ class: "flex flex-row items-center" }, class: "flex flex-col",
label({}, user), each: () => getSelectedGroup().groupUsers ?? [],
button( },
{ (user) =>
class: "ml-1 bg-red text-white", div(
onClick: () => removeUser(user), { class: "flex flex-row items-center" },
}, label({}, user),
"X", button(
{
class: "ml-1 bg-red text-white",
onClick: () => removeUser(user),
},
"X",
),
), ),
),
), ),
), ),
); );
@@ -532,20 +538,17 @@ const AccessControlTUI = () => {
return Show( return Show(
{ when: () => errorState().show }, { when: () => errorState().show },
div( div(
{ { class: "flex flex-col bg-red " },
class: label(
"fixed top-1/4 left-1/4 right-1/4 bottom-1/4 bg-red text-white border", { class: "w-25 text-white", wordWrap: true },
}, () => errorState().message,
div( ),
{ class: "flex flex-col p-2" }, button(
label({}, () => errorState().message), {
button( class: "bg-white text-black",
{ onClick: hideError,
class: "mt-2 bg-white text-black", },
onClick: hideError, "OK",
},
"OK",
),
), ),
), ),
); );
@@ -597,27 +600,36 @@ const AccessControlTUI = () => {
), ),
// Content area // 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 // Action buttons
div( div(
{ class: "flex flex-row justify-center p-2" }, { class: "flex flex-row justify-center p-2" },
button( button(
{ {
class: "bg-green text-white mr-2", class: "bg-green text-white mr-2",
onClick: handleSave, onClick: handleSave,
}, },
"Save", "Save",
), ),
button( button(
{ {
class: "bg-gray text-white", class: "bg-gray text-white",
onClick: () => { onClick: () => {
// Close TUI - this will be handled by the application framework // Close TUI - this will be handled by the application framework
error("TUI_CLOSE"); error("TUI_CLOSE");
}, },
}, },
"Close", "Close",
),
),
), ),
), ),

View File

@@ -49,31 +49,30 @@ export class CCLog {
* For SECOND interval: YYYY-MM-DD-HH-MM-SS * For SECOND interval: YYYY-MM-DD-HH-MM-SS
*/ */
private getTimePeriodString(time: number): string { private getTimePeriodString(time: number): string {
// Calculate which time period this timestamp falls into
const periodStart = Math.floor(time / this.interval) * this.interval; 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) { if (this.interval >= DAY) {
return `${periodDate.year}-${String(periodDate.month).padStart(2, "0")}-${String(periodDate.day).padStart(2, "0")}`; return `${d.year}-${string.format("%02d", d.month)}-${string.format("%02d", d.day)}`;
} else { } else if (this.interval >= HOUR) {
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)}_${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 { private generateFilePath(baseFilename: string, timePeriod: string): string {
// Extract file extension if present const scriptDir = shell.dir() ?? "";
const fileNameSubStrings = baseFilename.split(".");
let filenameWithoutExt: string;
let extension = "";
if (fileNameSubStrings.length > 1) { const [filenameWithoutExt, extension] = baseFilename.includes(".")
filenameWithoutExt = fileNameSubStrings[0]; ? baseFilename.split(".")
extension = fileNameSubStrings[1]; : [baseFilename, "log"];
} else {
filenameWithoutExt = baseFilename;
}
return `${shell.dir()}/${filenameWithoutExt}[${timePeriod}].${extension}`; return fs.combine(
scriptDir,
`${filenameWithoutExt}_${timePeriod}.${extension}`,
);
} }
private checkAndRotateLogFile() { private checkAndRotateLogFile() {
@@ -154,6 +153,10 @@ export class CCLog {
this.writeLine(this.getFormatMsg(msg, LogLevel.Error), colors.red); this.writeLine(this.getFormatMsg(msg, LogLevel.Error), colors.red);
} }
public setInTerminal(value: boolean) {
this.inTerm = value;
}
public close() { public close() {
if (this.fp !== undefined) { if (this.fp !== undefined) {
this.fp.close(); this.fp.close();

View File

@@ -5,8 +5,8 @@
import { UIObject } from "./UIObject"; import { UIObject } from "./UIObject";
import { calculateLayout } from "./layout"; import { calculateLayout } from "./layout";
import { render as renderTree, clearScreen } from "./renderer"; import { render as renderTree, clearScreen } from "./renderer";
import { CCLog } from "../ccLog"; import { CCLog, HOUR } from "../ccLog";
import { findScrollContainer } from "./scrollContainer"; import { setLogger } from "./context";
/** /**
* Main application class * Main application class
@@ -28,7 +28,8 @@ export class Application {
const [width, height] = term.getSize(); const [width, height] = term.getSize();
this.termWidth = width; this.termWidth = width;
this.termHeight = height; 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."); this.logger.debug("Application constructed.");
} }

View File

@@ -4,7 +4,16 @@
*/ */
import { UIObject, BaseProps, createTextNode } from "./UIObject"; 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 * Props for div component
@@ -14,7 +23,10 @@ export type DivProps = BaseProps & Record<string, unknown>;
/** /**
* Props for label component * Props for label component
*/ */
export type LabelProps = BaseProps & Record<string, unknown>; export type LabelProps = BaseProps & {
/** Whether to automatically wrap long text. Defaults to false. */
wordWrap?: boolean;
} & Record<string, unknown>;
/** /**
* Props for button component * Props for button component
@@ -95,10 +107,78 @@ export function div(
* label({}, () => `Hello, ${name()}!`) * 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( export function label(
props: LabelProps, props: LabelProps,
text: string | Accessor<string>, text: string | Accessor<string>,
): UIObject { ): 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<string>)
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 textNode = createTextNode(text);
const node = new UIObject("label", props, [textNode]); const node = new UIObject("label", props, [textNode]);
textNode.parent = node; textNode.parent = node;

21
src/lib/ccTUI/context.ts Normal file
View File

@@ -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;
}

View File

@@ -332,8 +332,9 @@ function drawNode(
? node.textContent() ? node.textContent()
: node.textContent; : node.textContent;
term.setTextColor(textColor ?? colors.white); if (bgColor !== undefined) {
term.setBackgroundColor(bgColor ?? colors.black); term.setBackgroundColor(bgColor);
}
term.setCursorPos(x, y); term.setCursorPos(x, y);
term.write(text.substring(0, width)); term.write(text.substring(0, width));
} }