mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-04 19:27:50 +08:00
try to wordwrap, but failed
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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,7 +404,12 @@ const AccessControlTUI = () => {
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Users list
|
// Users list
|
||||||
For({ each: () => getSelectedGroup().groupUsers ?? [] }, (user) =>
|
For(
|
||||||
|
{
|
||||||
|
class: "flex flex-col",
|
||||||
|
each: () => getSelectedGroup().groupUsers ?? [],
|
||||||
|
},
|
||||||
|
(user) =>
|
||||||
div(
|
div(
|
||||||
{ class: "flex flex-row items-center" },
|
{ class: "flex flex-row items-center" },
|
||||||
label({}, user),
|
label({}, user),
|
||||||
@@ -532,22 +538,19 @@ 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" },
|
|
||||||
label({}, () => errorState().message),
|
|
||||||
button(
|
button(
|
||||||
{
|
{
|
||||||
class: "mt-2 bg-white text-black",
|
class: "bg-white text-black",
|
||||||
onClick: hideError,
|
onClick: hideError,
|
||||||
},
|
},
|
||||||
"OK",
|
"OK",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -597,7 +600,14 @@ 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(
|
||||||
@@ -620,6 +630,8 @@ const AccessControlTUI = () => {
|
|||||||
"Close",
|
"Close",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Error dialog overlay
|
// Error dialog overlay
|
||||||
ErrorDialog(),
|
ErrorDialog(),
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
21
src/lib/ccTUI/context.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user