mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-29 12:57:50 +08:00
Compare commits
3 Commits
82a9fec46d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66b46c6d70 | ||
|
|
de97fb4858 | ||
|
|
0612477325 |
@@ -1,6 +1,5 @@
|
|||||||
import { Command, createCli } from "@/lib/ccCLI";
|
import { Command, createCli } from "@/lib/ccCLI";
|
||||||
import { Ok } from "@/lib/thirdparty/ts-result-es";
|
import { Ok } from "@/lib/thirdparty/ts-result-es";
|
||||||
import { CCLog } from "@/lib/ccLog";
|
|
||||||
import {
|
import {
|
||||||
AccessConfig,
|
AccessConfig,
|
||||||
UserGroupConfig,
|
UserGroupConfig,
|
||||||
@@ -8,12 +7,13 @@ import {
|
|||||||
saveConfig,
|
saveConfig,
|
||||||
} from "./config";
|
} from "./config";
|
||||||
import { parseBoolean } from "@/lib/common";
|
import { parseBoolean } from "@/lib/common";
|
||||||
|
import { Logger } from "@/lib/ccStructLog";
|
||||||
|
|
||||||
// 1. Define AppContext
|
// 1. Define AppContext
|
||||||
export interface AppContext {
|
export interface AppContext {
|
||||||
configFilepath: string;
|
configFilepath: string;
|
||||||
reloadConfig: () => void;
|
reloadConfig: () => void;
|
||||||
logger: CCLog;
|
logger: Logger;
|
||||||
print: (
|
print: (
|
||||||
message: string | MinecraftTextComponent | MinecraftTextComponent[],
|
message: string | MinecraftTextComponent | MinecraftTextComponent[],
|
||||||
) => void;
|
) => void;
|
||||||
@@ -48,7 +48,9 @@ const addCommand: Command<AppContext> = {
|
|||||||
config.adminGroupConfig.groupUsers.push(playerName);
|
config.adminGroupConfig.groupUsers.push(playerName);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const group = config.usersGroups.find((g) => g.groupName === groupName);
|
const group = config.usersGroups.find(
|
||||||
|
(g) => g.groupName === groupName,
|
||||||
|
);
|
||||||
if (!group) {
|
if (!group) {
|
||||||
const groupNames = getGroupNames(config);
|
const groupNames = getGroupNames(config);
|
||||||
context.print({
|
context.print({
|
||||||
@@ -107,7 +109,9 @@ const delCommand: Command<AppContext> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (group.groupUsers !== undefined) {
|
if (group.groupUsers !== undefined) {
|
||||||
group.groupUsers = group.groupUsers.filter((user) => user !== playerName);
|
group.groupUsers = group.groupUsers.filter(
|
||||||
|
(user) => user !== playerName,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
saveConfig(config, context.configFilepath);
|
saveConfig(config, context.configFilepath);
|
||||||
@@ -238,7 +242,10 @@ const configCommand: Command<AppContext> = {
|
|||||||
{ name: "value", description: "要设置的值", required: true },
|
{ name: "value", description: "要设置的值", required: true },
|
||||||
],
|
],
|
||||||
action: ({ args, context }) => {
|
action: ({ args, context }) => {
|
||||||
const [option, valueStr] = [args.option as string, args.value as string];
|
const [option, valueStr] = [
|
||||||
|
args.option as string,
|
||||||
|
args.value as string,
|
||||||
|
];
|
||||||
const config = loadConfig(context.configFilepath)!;
|
const config = loadConfig(context.configFilepath)!;
|
||||||
|
|
||||||
// Check if it's a group property (contains a dot)
|
// Check if it's a group property (contains a dot)
|
||||||
@@ -251,7 +258,9 @@ const configCommand: Command<AppContext> = {
|
|||||||
if (groupName === "admin") {
|
if (groupName === "admin") {
|
||||||
groupConfig = config.adminGroupConfig;
|
groupConfig = config.adminGroupConfig;
|
||||||
} else {
|
} else {
|
||||||
groupConfig = config.usersGroups.find((g) => g.groupName === groupName);
|
groupConfig = config.usersGroups.find(
|
||||||
|
(g) => g.groupName === groupName,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groupConfig) {
|
if (!groupConfig) {
|
||||||
@@ -321,7 +330,9 @@ const configCommand: Command<AppContext> = {
|
|||||||
const value = parseInt(valueStr);
|
const value = parseInt(valueStr);
|
||||||
|
|
||||||
if (isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
context.print({ text: `无效的值: ${valueStr}. 必须是一个数字。` });
|
context.print({
|
||||||
|
text: `无效的值: ${valueStr}. 必须是一个数字。`,
|
||||||
|
});
|
||||||
return Ok.EMPTY;
|
return Ok.EMPTY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ import {
|
|||||||
ConsoleStream,
|
ConsoleStream,
|
||||||
DAY,
|
DAY,
|
||||||
FileStream,
|
FileStream,
|
||||||
Logger,
|
getStructLogger,
|
||||||
|
LoggerOptions,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
|
MB,
|
||||||
processor,
|
processor,
|
||||||
|
setStructLoggerConfig,
|
||||||
textRenderer,
|
textRenderer,
|
||||||
} from "@/lib/ccStructLog";
|
} from "@/lib/ccStructLog";
|
||||||
|
|
||||||
@@ -21,7 +24,7 @@ const args = [...$vararg];
|
|||||||
|
|
||||||
// Init Log
|
// Init Log
|
||||||
let isOnConsoleStream = true;
|
let isOnConsoleStream = true;
|
||||||
const logger = new Logger({
|
const loggerConfig: LoggerOptions = {
|
||||||
processors: [
|
processors: [
|
||||||
processor.filterByLevel(LogLevel.Info),
|
processor.filterByLevel(LogLevel.Info),
|
||||||
processor.addTimestamp(),
|
processor.addTimestamp(),
|
||||||
@@ -29,9 +32,19 @@ const logger = new Logger({
|
|||||||
renderer: textRenderer,
|
renderer: textRenderer,
|
||||||
streams: [
|
streams: [
|
||||||
new ConditionalStream(new ConsoleStream(), () => isOnConsoleStream),
|
new ConditionalStream(new ConsoleStream(), () => isOnConsoleStream),
|
||||||
new FileStream("accesscontrol.log", DAY),
|
new FileStream({
|
||||||
|
filePath: "accesscontrol.log",
|
||||||
|
rotationInterval: DAY,
|
||||||
|
autoCleanup: {
|
||||||
|
enabled: true,
|
||||||
|
maxFiles: 7,
|
||||||
|
maxSizeBytes: MB,
|
||||||
|
},
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
});
|
};
|
||||||
|
setStructLoggerConfig(loggerConfig);
|
||||||
|
const logger = getStructLogger();
|
||||||
|
|
||||||
// Load Config
|
// Load Config
|
||||||
const configFilepath = `${shell.dir()}/access.config.json`;
|
const configFilepath = `${shell.dir()}/access.config.json`;
|
||||||
@@ -70,6 +83,11 @@ function reloadConfig() {
|
|||||||
gWatchPlayersInfo = [];
|
gWatchPlayersInfo = [];
|
||||||
releaser.release();
|
releaser.release();
|
||||||
logger.info("Reload config successfully!");
|
logger.info("Reload config successfully!");
|
||||||
|
const tutorial: string[] = [];
|
||||||
|
tutorial.push("Access Control System started.");
|
||||||
|
tutorial.push("\tPress 'c' to open configuration TUI.");
|
||||||
|
tutorial.push("\tPress 'r' to reload configuration.");
|
||||||
|
print(tutorial.join("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function safeParseTextComponent(
|
function safeParseTextComponent(
|
||||||
|
|||||||
@@ -99,25 +99,31 @@ const AccessControlTUI = () => {
|
|||||||
|
|
||||||
// Validate numbers
|
// Validate numbers
|
||||||
if (
|
if (
|
||||||
validateNumber(currentConfig.detectInterval?.toString() ?? "") === null
|
validateNumber(
|
||||||
|
currentConfig.detectInterval?.toString() ?? "",
|
||||||
|
) === null
|
||||||
) {
|
) {
|
||||||
showError("Invalid Detect Interval: must be a number");
|
showError("Invalid Detect Interval: must be a number");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
validateNumber(currentConfig.watchInterval?.toString() ?? "") === null
|
validateNumber(
|
||||||
|
currentConfig.watchInterval?.toString() ?? "",
|
||||||
|
) === null
|
||||||
) {
|
) {
|
||||||
showError("Invalid Watch Interval: must be a number");
|
showError("Invalid Watch Interval: must be a number");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
validateNumber(currentConfig.noticeTimes?.toString() ?? "") === null
|
validateNumber(currentConfig.noticeTimes?.toString() ?? "") ===
|
||||||
|
null
|
||||||
) {
|
) {
|
||||||
showError("Invalid Notice Times: must be a number");
|
showError("Invalid Notice Times: must be a number");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
validateNumber(currentConfig.detectRange?.toString() ?? "") === null
|
validateNumber(currentConfig.detectRange?.toString() ?? "") ===
|
||||||
|
null
|
||||||
) {
|
) {
|
||||||
showError("Invalid Detect Range: must be a number");
|
showError("Invalid Detect Range: must be a number");
|
||||||
return;
|
return;
|
||||||
@@ -153,7 +159,9 @@ const AccessControlTUI = () => {
|
|||||||
|
|
||||||
for (const toastConfig of toastConfigs) {
|
for (const toastConfig of toastConfigs) {
|
||||||
if (toastConfig.value != undefined) {
|
if (toastConfig.value != undefined) {
|
||||||
const serialized = textutils.serialiseJSON(toastConfig.value);
|
const serialized = textutils.serialiseJSON(
|
||||||
|
toastConfig.value,
|
||||||
|
);
|
||||||
if (!validateTextComponent(serialized)) {
|
if (!validateTextComponent(serialized)) {
|
||||||
showError(
|
showError(
|
||||||
`Invalid ${toastConfig.name}: must be valid MinecraftTextComponent JSON`,
|
`Invalid ${toastConfig.name}: must be valid MinecraftTextComponent JSON`,
|
||||||
@@ -210,7 +218,9 @@ const AccessControlTUI = () => {
|
|||||||
const currentAdmin = config().adminGroupConfig;
|
const currentAdmin = config().adminGroupConfig;
|
||||||
setConfig("adminGroupConfig", {
|
setConfig("adminGroupConfig", {
|
||||||
...currentAdmin,
|
...currentAdmin,
|
||||||
groupUsers: currentAdmin.groupUsers.filter((user) => user !== userName),
|
groupUsers: currentAdmin.groupUsers.filter(
|
||||||
|
(user) => user !== userName,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Regular group
|
// Regular group
|
||||||
@@ -268,7 +278,10 @@ const AccessControlTUI = () => {
|
|||||||
onFocusChanged: () => {
|
onFocusChanged: () => {
|
||||||
const num = validateNumber(getDetectInterval());
|
const num = validateNumber(getDetectInterval());
|
||||||
if (num !== null) setConfig("detectInterval", num);
|
if (num !== null) setConfig("detectInterval", num);
|
||||||
else setDetectInterval(config().detectInterval.toString());
|
else
|
||||||
|
setDetectInterval(
|
||||||
|
config().detectInterval.toString(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -282,7 +295,8 @@ const AccessControlTUI = () => {
|
|||||||
onFocusChanged: () => {
|
onFocusChanged: () => {
|
||||||
const num = validateNumber(getWatchInterval());
|
const num = validateNumber(getWatchInterval());
|
||||||
if (num !== null) setConfig("watchInterval", num);
|
if (num !== null) setConfig("watchInterval", num);
|
||||||
else setWatchInterval(config().watchInterval.toString());
|
else
|
||||||
|
setWatchInterval(config().watchInterval.toString());
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -347,11 +361,15 @@ const AccessControlTUI = () => {
|
|||||||
div(
|
div(
|
||||||
{ class: "flex flex-col" },
|
{ class: "flex flex-col" },
|
||||||
label({}, "Groups:"),
|
label({}, "Groups:"),
|
||||||
For({ each: () => groups, class: "flex flex-col" }, (group, index) =>
|
For(
|
||||||
|
{ each: () => groups, class: "flex flex-col" },
|
||||||
|
(group, index) =>
|
||||||
button(
|
button(
|
||||||
{
|
{
|
||||||
class:
|
class:
|
||||||
selectedGroupIndex() === index() ? "bg-blue text-white" : "",
|
selectedGroupIndex() === index()
|
||||||
|
? "bg-blue text-white"
|
||||||
|
: "",
|
||||||
onClick: () => setSelectedGroupIndex(index()),
|
onClick: () => setSelectedGroupIndex(index()),
|
||||||
},
|
},
|
||||||
group.groupName,
|
group.groupName,
|
||||||
@@ -489,7 +507,10 @@ const AccessControlTUI = () => {
|
|||||||
* Toast Configuration Tab Factory
|
* Toast Configuration Tab Factory
|
||||||
*/
|
*/
|
||||||
const createToastTab = (
|
const createToastTab = (
|
||||||
toastType: "welcomeToastConfig" | "warnToastConfig" | "noticeToastConfig",
|
toastType:
|
||||||
|
| "welcomeToastConfig"
|
||||||
|
| "warnToastConfig"
|
||||||
|
| "noticeToastConfig",
|
||||||
) => {
|
) => {
|
||||||
return () => {
|
return () => {
|
||||||
const toastConfig = config()[toastType];
|
const toastConfig = config()[toastType];
|
||||||
@@ -533,7 +554,9 @@ const AccessControlTUI = () => {
|
|||||||
} catch {
|
} catch {
|
||||||
setTempToastConfig({
|
setTempToastConfig({
|
||||||
...getTempToastConfig(),
|
...getTempToastConfig(),
|
||||||
title: textutils.serialiseJSON(currentToastConfig.title),
|
title: textutils.serialiseJSON(
|
||||||
|
currentToastConfig.title,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -545,7 +568,10 @@ const AccessControlTUI = () => {
|
|||||||
type: "text",
|
type: "text",
|
||||||
value: () => getTempToastConfig().msg,
|
value: () => getTempToastConfig().msg,
|
||||||
onInput: (value) =>
|
onInput: (value) =>
|
||||||
setTempToastConfig({ ...getTempToastConfig(), msg: value }),
|
setTempToastConfig({
|
||||||
|
...getTempToastConfig(),
|
||||||
|
msg: value,
|
||||||
|
}),
|
||||||
onFocusChanged: () => {
|
onFocusChanged: () => {
|
||||||
const currentToastConfig = config()[toastType];
|
const currentToastConfig = config()[toastType];
|
||||||
|
|
||||||
@@ -566,7 +592,9 @@ const AccessControlTUI = () => {
|
|||||||
} catch {
|
} catch {
|
||||||
setTempToastConfig({
|
setTempToastConfig({
|
||||||
...getTempToastConfig(),
|
...getTempToastConfig(),
|
||||||
msg: textutils.serialiseJSON(currentToastConfig.msg),
|
msg: textutils.serialiseJSON(
|
||||||
|
currentToastConfig.msg,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
// Invalid JSON, ignore
|
// Invalid JSON, ignore
|
||||||
}
|
}
|
||||||
@@ -579,13 +607,19 @@ const AccessControlTUI = () => {
|
|||||||
input({
|
input({
|
||||||
type: "text",
|
type: "text",
|
||||||
value: () => {
|
value: () => {
|
||||||
const str = textutils.serialiseJSON(getTempToastConfig().prefix, {
|
const str = textutils.serialiseJSON(
|
||||||
|
getTempToastConfig().prefix,
|
||||||
|
{
|
||||||
unicode_strings: true,
|
unicode_strings: true,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
return str.substring(1, str.length - 1);
|
return str.substring(1, str.length - 1);
|
||||||
},
|
},
|
||||||
onInput: (value) =>
|
onInput: (value) =>
|
||||||
setTempToastConfig({ ...getTempToastConfig(), prefix: value }),
|
setTempToastConfig({
|
||||||
|
...getTempToastConfig(),
|
||||||
|
prefix: value,
|
||||||
|
}),
|
||||||
onFocusChanged: () => {
|
onFocusChanged: () => {
|
||||||
const currentToastConfig = config()[toastType];
|
const currentToastConfig = config()[toastType];
|
||||||
setConfig(toastType, {
|
setConfig(toastType, {
|
||||||
@@ -603,7 +637,10 @@ const AccessControlTUI = () => {
|
|||||||
type: "text",
|
type: "text",
|
||||||
value: () => getTempToastConfig().brackets,
|
value: () => getTempToastConfig().brackets,
|
||||||
onInput: (value) =>
|
onInput: (value) =>
|
||||||
setTempToastConfig({ ...getTempToastConfig(), brackets: value }),
|
setTempToastConfig({
|
||||||
|
...getTempToastConfig(),
|
||||||
|
brackets: value,
|
||||||
|
}),
|
||||||
onFocusChanged: () => {
|
onFocusChanged: () => {
|
||||||
const currentToastConfig = config()[toastType];
|
const currentToastConfig = config()[toastType];
|
||||||
setConfig(toastType, {
|
setConfig(toastType, {
|
||||||
@@ -678,7 +715,10 @@ const AccessControlTUI = () => {
|
|||||||
{ when: () => currentTab() === TABS.WELCOME_TOAST },
|
{ when: () => currentTab() === TABS.WELCOME_TOAST },
|
||||||
WelcomeToastTab(),
|
WelcomeToastTab(),
|
||||||
),
|
),
|
||||||
Match({ when: () => currentTab() === TABS.WARN_TOAST }, WarnToastTab()),
|
Match(
|
||||||
|
{ when: () => currentTab() === TABS.WARN_TOAST },
|
||||||
|
WarnToastTab(),
|
||||||
|
),
|
||||||
Match(
|
Match(
|
||||||
{ when: () => currentTab() === TABS.NOTICE_TOAST },
|
{ when: () => currentTab() === TABS.NOTICE_TOAST },
|
||||||
NoticeToastTab(),
|
NoticeToastTab(),
|
||||||
@@ -703,7 +743,10 @@ const AccessControlTUI = () => {
|
|||||||
For({ each: () => tabNames }, (tabName, index) =>
|
For({ each: () => tabNames }, (tabName, index) =>
|
||||||
button(
|
button(
|
||||||
{
|
{
|
||||||
class: currentTab() === index() ? "bg-blue text-white" : "",
|
class:
|
||||||
|
currentTab() === index()
|
||||||
|
? "bg-blue text-white"
|
||||||
|
: "",
|
||||||
onClick: () => setCurrentTab(index() as TabIndex),
|
onClick: () => setCurrentTab(index() as TabIndex),
|
||||||
},
|
},
|
||||||
tabName,
|
tabName,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
import { Queue } from "@/lib/datatype/Queue";
|
import { Queue } from "@/lib/datatype/Queue";
|
||||||
import {
|
import {
|
||||||
ConsoleStream,
|
ConsoleStream,
|
||||||
|
DAY,
|
||||||
|
FileStream,
|
||||||
Logger,
|
Logger,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
processor,
|
processor,
|
||||||
@@ -18,7 +20,17 @@ const logger = new Logger({
|
|||||||
processor.addTimestamp(),
|
processor.addTimestamp(),
|
||||||
],
|
],
|
||||||
renderer: textRenderer,
|
renderer: textRenderer,
|
||||||
streams: [new ConsoleStream()],
|
streams: [
|
||||||
|
new ConsoleStream(),
|
||||||
|
new FileStream({
|
||||||
|
filePath: "autocraft.log",
|
||||||
|
rotationInterval: DAY,
|
||||||
|
autoCleanup: {
|
||||||
|
enabled: true,
|
||||||
|
maxFiles: 3,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const peripheralsNames = {
|
const peripheralsNames = {
|
||||||
@@ -47,7 +59,8 @@ enum State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
function main() {
|
||||||
while (true) {
|
let isFinishedInitPeripheral = false;
|
||||||
|
while (!isFinishedInitPeripheral) {
|
||||||
try {
|
try {
|
||||||
packsInventory = peripheral.wrap(
|
packsInventory = peripheral.wrap(
|
||||||
peripheralsNames.packsInventory,
|
peripheralsNames.packsInventory,
|
||||||
@@ -67,7 +80,7 @@ function main() {
|
|||||||
turtleLocalName = wiredModem.getNameLocal();
|
turtleLocalName = wiredModem.getNameLocal();
|
||||||
|
|
||||||
logger.info("Peripheral initialization complete...");
|
logger.info("Peripheral initialization complete...");
|
||||||
break;
|
isFinishedInitPeripheral = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Peripheral initialization failed for ${String(error)}, try again...`,
|
`Peripheral initialization failed for ${String(error)}, try again...`,
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { LogLevel, LoggerOptions, LogEvent, ILogger } from "./types";
|
import { LogLevel, LoggerOptions, LogEvent, ILogger } from "./types";
|
||||||
|
import { processor } from "./processors";
|
||||||
|
import { ConsoleStream } from "./streams";
|
||||||
|
import { textRenderer } from "./renderers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main Logger class that orchestrates the logging pipeline.
|
* The main Logger class that orchestrates the logging pipeline.
|
||||||
@@ -13,26 +16,19 @@ import { LogLevel, LoggerOptions, LogEvent, ILogger } from "./types";
|
|||||||
*/
|
*/
|
||||||
export class Logger implements ILogger {
|
export class Logger implements ILogger {
|
||||||
private options: LoggerOptions;
|
private options: LoggerOptions;
|
||||||
|
private loggerName?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new Logger instance.
|
* Create a new Logger instance.
|
||||||
*
|
*
|
||||||
* @param options - Configuration options for the logger
|
* @param options - Configuration options for the logger
|
||||||
|
* @param name - The name of the logger
|
||||||
*/
|
*/
|
||||||
constructor(options: Partial<LoggerOptions>) {
|
constructor(options: LoggerOptions, name?: string) {
|
||||||
this.options = {
|
this.options = options;
|
||||||
processors: options.processors ?? [],
|
this.loggerName = name;
|
||||||
renderer: options.renderer ?? this.defaultRenderer,
|
|
||||||
streams: options.streams ?? [],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Default renderer that returns an empty string.
|
|
||||||
* Used as fallback when no renderer is provided.
|
|
||||||
*/
|
|
||||||
private defaultRenderer = (): string => "";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main logging method that handles the complete logging pipeline.
|
* Main logging method that handles the complete logging pipeline.
|
||||||
*
|
*
|
||||||
@@ -51,6 +47,8 @@ export class Logger implements ILogger {
|
|||||||
["message", message],
|
["message", message],
|
||||||
...Object.entries(context),
|
...Object.entries(context),
|
||||||
]);
|
]);
|
||||||
|
if (this.loggerName !== undefined)
|
||||||
|
event.set("loggerName", this.loggerName);
|
||||||
|
|
||||||
// 2. Process through the processor chain
|
// 2. Process through the processor chain
|
||||||
for (const processor of this.options.processors) {
|
for (const processor of this.options.processors) {
|
||||||
@@ -62,12 +60,11 @@ export class Logger implements ILogger {
|
|||||||
|
|
||||||
// 3. Render and output if event wasn't dropped
|
// 3. Render and output if event wasn't dropped
|
||||||
if (event !== undefined) {
|
if (event !== undefined) {
|
||||||
const finalEvent = event;
|
const output = this.options.renderer(event);
|
||||||
const output = this.options.renderer(finalEvent);
|
|
||||||
|
|
||||||
// Send to all configured streams
|
// Send to all configured streams
|
||||||
for (const stream of this.options.streams) {
|
for (const stream of this.options.streams) {
|
||||||
stream.write(output, finalEvent);
|
stream.write(output, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,3 +160,17 @@ export class Logger implements ILogger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let globalLoggerConfig: LoggerOptions = {
|
||||||
|
processors: [processor.addTimestamp()],
|
||||||
|
renderer: textRenderer,
|
||||||
|
streams: [new ConsoleStream()],
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getStructLogger(name?: string): Logger {
|
||||||
|
return new Logger(globalLoggerConfig, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setStructLoggerConfig(config: LoggerOptions): void {
|
||||||
|
globalLoggerConfig = config;
|
||||||
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export namespace processor {
|
|||||||
return event; // Pass through if no level is set
|
return event; // Pass through if no level is set
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventLevel !== undefined && eventLevel < minLevel) {
|
if (eventLevel < minLevel) {
|
||||||
return undefined; // Drop the log event
|
return undefined; // Drop the log event
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,22 +69,6 @@ export namespace processor {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a logger name/source to the log event.
|
|
||||||
*
|
|
||||||
* This processor is useful when you have multiple loggers in your application
|
|
||||||
* and want to identify which component generated each log entry.
|
|
||||||
*
|
|
||||||
* @param name - The name/source to add to log events
|
|
||||||
* @returns A processor function that adds the source name
|
|
||||||
*/
|
|
||||||
export function addSource(name: string): Processor {
|
|
||||||
return (event) => {
|
|
||||||
event.set("source", name);
|
|
||||||
return event;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the current computer ID to the log event.
|
* Adds the current computer ID to the log event.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -53,14 +53,20 @@ export const textRenderer: Renderer = (event) => {
|
|||||||
const timeStr = event.get("timestamp") as string | undefined;
|
const timeStr = event.get("timestamp") as string | undefined;
|
||||||
const level: string | undefined = LogLevel[event.get("level") as LogLevel];
|
const level: string | undefined = LogLevel[event.get("level") as LogLevel];
|
||||||
const message = (event.get("message") as string) ?? "";
|
const message = (event.get("message") as string) ?? "";
|
||||||
|
const loggerName = event.get("loggerName") as string | undefined;
|
||||||
|
|
||||||
// Start building the output
|
// Start building the output
|
||||||
let output = `[${timeStr}] [${level}] ${message} \t`;
|
let output = `${timeStr} [${level}] ${message} \t ${loggerName !== undefined ? "[" + loggerName + "]" : ""}`;
|
||||||
|
|
||||||
// Add context fields (excluding the core fields we already used)
|
// Add context fields (excluding the core fields we already used)
|
||||||
const contextFields: string[] = [];
|
const contextFields: string[] = [];
|
||||||
for (const [key, value] of event.entries()) {
|
for (const [key, value] of event.entries()) {
|
||||||
if (key !== "timestamp" && key !== "level" && key !== "message") {
|
if (
|
||||||
|
key !== "timestamp" &&
|
||||||
|
key !== "level" &&
|
||||||
|
key !== "message" &&
|
||||||
|
key !== "loggerName"
|
||||||
|
) {
|
||||||
contextFields.push(`${key}=${tostring(value)}`);
|
contextFields.push(`${key}=${tostring(value)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,33 @@
|
|||||||
|
|
||||||
import { LogLevel, Stream, LogEvent } from "./types";
|
import { LogLevel, Stream, LogEvent } from "./types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration interface for FileStream with auto-cleanup options.
|
||||||
|
*/
|
||||||
|
interface FileStreamConfig {
|
||||||
|
/** Path to the log file */
|
||||||
|
filePath: string;
|
||||||
|
/**
|
||||||
|
* Time in seconds between file rotations (0 = no rotation)
|
||||||
|
* Time must larger than one DAY
|
||||||
|
* @default 0
|
||||||
|
*/
|
||||||
|
rotationInterval?: number;
|
||||||
|
/** Auto-cleanup configuration */
|
||||||
|
autoCleanup?: {
|
||||||
|
/** Whether to enable auto-cleanup */
|
||||||
|
enabled: boolean;
|
||||||
|
/** Maximum number of log files to keep */
|
||||||
|
maxFiles?: number;
|
||||||
|
/** Maximum total size in bytes for all log files */
|
||||||
|
maxSizeBytes?: number;
|
||||||
|
/** Directory to search for log files (defaults to log file directory) */
|
||||||
|
logDir?: string;
|
||||||
|
/** File pattern to match (defaults to base filename pattern) */
|
||||||
|
pattern?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Console stream that outputs to the CC:Tweaked terminal.
|
* Console stream that outputs to the CC:Tweaked terminal.
|
||||||
*
|
*
|
||||||
@@ -59,20 +86,20 @@ export class FileStream implements Stream {
|
|||||||
private filePath: string;
|
private filePath: string;
|
||||||
private rotationInterval: number;
|
private rotationInterval: number;
|
||||||
private lastRotationTime: number;
|
private lastRotationTime: number;
|
||||||
private baseFilename: string;
|
private autoCleanupConfig?: FileStreamConfig["autoCleanup"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new file stream.
|
* Create a new file stream with configuration object.
|
||||||
*
|
*
|
||||||
* @param filePath - Path to the log file
|
* @param config - FileStream configuration object
|
||||||
* @param rotationInterval - Time in seconds between file rotations (0 = no rotation)
|
|
||||||
*/
|
*/
|
||||||
constructor(filePath: string, rotationInterval: number = 0) {
|
constructor(config: FileStreamConfig) {
|
||||||
this.filePath = filePath;
|
this.filePath = config.filePath;
|
||||||
this.rotationInterval = rotationInterval;
|
this.rotationInterval = config.rotationInterval || 0;
|
||||||
|
if (this.rotationInterval !== 0 && this.rotationInterval < DAY)
|
||||||
|
throw Error("Rotation interval must be at least one day");
|
||||||
|
this.autoCleanupConfig = config.autoCleanup;
|
||||||
this.lastRotationTime = os.time();
|
this.lastRotationTime = os.time();
|
||||||
this.baseFilename = filePath;
|
|
||||||
|
|
||||||
this.openFile();
|
this.openFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,24 +121,27 @@ export class FileStream implements Stream {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.fileHandle = handle;
|
this.fileHandle = handle;
|
||||||
|
|
||||||
|
// Perform auto-cleanup when opening file
|
||||||
|
this.performAutoCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a filename with timestamp for file rotation.
|
* Generate a filename with timestamp for file rotation.
|
||||||
*/
|
*/
|
||||||
private getRotatedFilename(): string {
|
private getRotatedFilename(): string {
|
||||||
const currentTime = os.time();
|
const currentTime = os.time(os.date("*t"));
|
||||||
const rotationPeriod =
|
const rotationPeriod =
|
||||||
Math.floor(currentTime / this.rotationInterval) *
|
Math.floor(currentTime / this.rotationInterval) *
|
||||||
this.rotationInterval;
|
this.rotationInterval;
|
||||||
const date = os.date("*t", rotationPeriod) as LuaDate;
|
const date = os.date("*t", rotationPeriod) as LuaDate;
|
||||||
|
|
||||||
const timestamp = `${date.year}-${string.format("%02d", date.month)}-${string.format("%02d", date.day)}_${string.format("%02d", date.hour)}-${string.format("%02d", date.min)}`;
|
const timestamp = `${date.year}-${string.format("%02d", date.month)}-${string.format("%02d", date.day)}`;
|
||||||
|
|
||||||
// Split filename and extension
|
// Split filename and extension
|
||||||
const splitStrs = this.baseFilename.split(".");
|
const splitStrs = this.filePath.split(".");
|
||||||
if (splitStrs.length === 1) {
|
if (splitStrs.length === 1) {
|
||||||
return `${this.baseFilename}_${timestamp}.log`;
|
return `${this.filePath}_${timestamp}.log`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = splitStrs[0];
|
const name = splitStrs[0];
|
||||||
@@ -126,19 +156,50 @@ export class FileStream implements Stream {
|
|||||||
if (this.rotationInterval <= 0) return;
|
if (this.rotationInterval <= 0) return;
|
||||||
|
|
||||||
const currentTime = os.time();
|
const currentTime = os.time();
|
||||||
const currentPeriod = Math.floor(currentTime / this.rotationInterval);
|
if (
|
||||||
const lastPeriod = Math.floor(
|
Math.floor(
|
||||||
this.lastRotationTime / this.rotationInterval,
|
(currentTime - this.lastRotationTime) / this.rotationInterval,
|
||||||
);
|
) > 0
|
||||||
|
) {
|
||||||
if (currentPeriod > lastPeriod) {
|
|
||||||
// Time to rotate
|
// Time to rotate
|
||||||
this.close();
|
this.close();
|
||||||
this.lastRotationTime = currentTime;
|
this.lastRotationTime = currentTime;
|
||||||
this.openFile();
|
this.openFile();
|
||||||
|
// Auto-cleanup is performed in openFile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform auto-cleanup based on configuration.
|
||||||
|
* This method is called automatically when opening files or rotating.
|
||||||
|
*/
|
||||||
|
private performAutoCleanup(): void {
|
||||||
|
if (!this.autoCleanupConfig || !this.autoCleanupConfig.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this.autoCleanupConfig;
|
||||||
|
|
||||||
|
// Cleanup by file count if configured
|
||||||
|
if (config.maxFiles !== undefined && config.maxFiles > 0) {
|
||||||
|
this.cleanupOldLogFiles(config.maxFiles, config.logDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup by total size if configured
|
||||||
|
if (config.maxSizeBytes !== undefined && config.maxSizeBytes > 0) {
|
||||||
|
this.cleanupLogFilesBySize(config.maxSizeBytes, config.logDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable or update auto-cleanup configuration at runtime.
|
||||||
|
*
|
||||||
|
* @param config - Auto-cleanup configuration
|
||||||
|
*/
|
||||||
|
public setAutoCleanup(config: FileStreamConfig["autoCleanup"]): void {
|
||||||
|
this.autoCleanupConfig = config;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write a formatted log message to the file.
|
* Write a formatted log message to the file.
|
||||||
*
|
*
|
||||||
@@ -163,6 +224,123 @@ export class FileStream implements Stream {
|
|||||||
this.fileHandle = undefined;
|
this.fileHandle = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for log files matching the specified pattern in a directory.
|
||||||
|
*
|
||||||
|
* @param logDir - Directory containing log files (defaults to directory of current log file)
|
||||||
|
* @returns Array of log file information including path, size, and modification time
|
||||||
|
*/
|
||||||
|
private searchLogFiles(
|
||||||
|
logDir?: string,
|
||||||
|
): Array<{ path: string; size: number; modified: number }> {
|
||||||
|
const directory = logDir || fs.getDir(this.filePath);
|
||||||
|
const splitStrs = this.filePath.split(".");
|
||||||
|
|
||||||
|
const name = splitStrs[0] + "_";
|
||||||
|
const ext = splitStrs.length > 1 ? splitStrs[1] : "log";
|
||||||
|
|
||||||
|
if (!fs.exists(directory) || !fs.isDir(directory)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const logFiles: Array<{
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
modified: number;
|
||||||
|
}> = [];
|
||||||
|
const files = fs.list(directory);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = fs.combine(directory, file);
|
||||||
|
if (
|
||||||
|
fs.isDir(fullPath) ||
|
||||||
|
!file.startsWith(name) ||
|
||||||
|
!file.endsWith(ext)
|
||||||
|
)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const attributes = fs.attributes(fullPath);
|
||||||
|
if (attributes !== undefined) {
|
||||||
|
logFiles.push({
|
||||||
|
path: fullPath,
|
||||||
|
size: attributes.size,
|
||||||
|
modified: attributes.modified,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return logFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old log files by keeping only the specified number of most recent files.
|
||||||
|
*
|
||||||
|
* @param maxFiles - Maximum number of log files to keep
|
||||||
|
* @param logDir - Directory containing log files (defaults to directory of current log file)
|
||||||
|
*/
|
||||||
|
public cleanupOldLogFiles(maxFiles: number, logDir?: string): void {
|
||||||
|
if (maxFiles <= 0) return;
|
||||||
|
|
||||||
|
const logFiles = this.searchLogFiles(logDir);
|
||||||
|
if (logFiles.length <= maxFiles) return;
|
||||||
|
|
||||||
|
// Sort by modification time (newest first)
|
||||||
|
logFiles.sort((a, b) => b.modified - a.modified);
|
||||||
|
|
||||||
|
// Delete files beyond the limit
|
||||||
|
for (let i = maxFiles; i < logFiles.length; i++) {
|
||||||
|
try {
|
||||||
|
fs.delete(logFiles[i].path);
|
||||||
|
} catch (err) {
|
||||||
|
printError(
|
||||||
|
`Failed to delete old log file ${logFiles[i].path}: ${err}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up log files by total size, deleting oldest files until total size is under limit.
|
||||||
|
*
|
||||||
|
* @param maxSizeBytes - Maximum total size in bytes for all log files
|
||||||
|
* @param logDir - Directory containing log files (defaults to directory of current log file)
|
||||||
|
* @param fileName - Base File Name
|
||||||
|
*/
|
||||||
|
public cleanupLogFilesBySize(maxSizeBytes: number, logDir?: string): void {
|
||||||
|
if (maxSizeBytes <= 0) return;
|
||||||
|
|
||||||
|
const logFiles = this.searchLogFiles(logDir);
|
||||||
|
if (logFiles.length === 0) return;
|
||||||
|
|
||||||
|
// Calculate total size
|
||||||
|
let totalSize = 0;
|
||||||
|
for (const logFile of logFiles) {
|
||||||
|
totalSize += logFile.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If total size is within limit, no cleanup needed
|
||||||
|
if (totalSize <= maxSizeBytes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by modification time (oldest first for deletion)
|
||||||
|
logFiles.sort((a, b) => a.modified - b.modified);
|
||||||
|
|
||||||
|
// Delete oldest files until we're under the size limit
|
||||||
|
for (const logFile of logFiles) {
|
||||||
|
if (totalSize <= maxSizeBytes) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.delete(logFile.path);
|
||||||
|
totalSize -= logFile.size;
|
||||||
|
} catch (err) {
|
||||||
|
printError(`Failed to delete log file ${logFile.path}: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -307,3 +485,7 @@ export const MINUTE = 60 * SECOND;
|
|||||||
export const HOUR = 60 * MINUTE;
|
export const HOUR = 60 * MINUTE;
|
||||||
export const DAY = 24 * HOUR;
|
export const DAY = 24 * HOUR;
|
||||||
export const WEEK = 7 * DAY;
|
export const WEEK = 7 * DAY;
|
||||||
|
|
||||||
|
// Byte constants for file rotation
|
||||||
|
export const MB = 1024 * 1024;
|
||||||
|
export const KB = 1024;
|
||||||
|
|||||||
@@ -398,11 +398,17 @@ export class UIObject {
|
|||||||
|
|
||||||
const newScrollX = Math.max(
|
const newScrollX = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(this.scrollProps.maxScrollX, this.scrollProps.scrollX + deltaX),
|
Math.min(
|
||||||
|
this.scrollProps.maxScrollX,
|
||||||
|
this.scrollProps.scrollX + deltaX,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const newScrollY = Math.max(
|
const newScrollY = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(this.scrollProps.maxScrollY, this.scrollProps.scrollY + deltaY),
|
Math.min(
|
||||||
|
this.scrollProps.maxScrollY,
|
||||||
|
this.scrollProps.scrollY + deltaY,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.scrollProps.scrollX = newScrollX;
|
this.scrollProps.scrollX = newScrollX;
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
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, DAY, LogLevel } from "../ccLog";
|
|
||||||
import { setLogger } from "./context";
|
|
||||||
import { InputProps } from "./components";
|
import { InputProps } from "./components";
|
||||||
import { Setter } from "./reactivity";
|
import { Setter } from "./reactivity";
|
||||||
|
import { getStructLogger, Logger } from "@/lib/ccStructLog";
|
||||||
|
import { setLogger } from "./context";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main application class
|
* Main application class
|
||||||
@@ -21,7 +21,7 @@ export class Application {
|
|||||||
private focusedNode?: UIObject;
|
private focusedNode?: UIObject;
|
||||||
private termWidth: number;
|
private termWidth: number;
|
||||||
private termHeight: number;
|
private termHeight: number;
|
||||||
private logger: CCLog;
|
private logger: Logger;
|
||||||
private cursorBlinkState = false;
|
private cursorBlinkState = false;
|
||||||
private lastBlinkTime = 0;
|
private lastBlinkTime = 0;
|
||||||
private readonly BLINK_INTERVAL = 0.5; // seconds
|
private readonly BLINK_INTERVAL = 0.5; // seconds
|
||||||
@@ -30,11 +30,7 @@ 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", {
|
this.logger = getStructLogger("ccTUI");
|
||||||
printTerminal: false,
|
|
||||||
logInterval: DAY,
|
|
||||||
outputMinLevel: LogLevel.Info,
|
|
||||||
});
|
|
||||||
setLogger(this.logger);
|
setLogger(this.logger);
|
||||||
this.logger.debug("Application constructed.");
|
this.logger.debug("Application constructed.");
|
||||||
}
|
}
|
||||||
@@ -99,7 +95,6 @@ export class Application {
|
|||||||
this.root.unmount();
|
this.root.unmount();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.close();
|
|
||||||
clearScreen();
|
clearScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,8 +219,10 @@ export class Application {
|
|||||||
| undefined;
|
| undefined;
|
||||||
if (type === "checkbox") {
|
if (type === "checkbox") {
|
||||||
// Toggle checkbox
|
// Toggle checkbox
|
||||||
const onChangeProp = (this.focusedNode.props as InputProps).onChange;
|
const onChangeProp = (this.focusedNode.props as InputProps)
|
||||||
const checkedProp = (this.focusedNode.props as InputProps).checked;
|
.onChange;
|
||||||
|
const checkedProp = (this.focusedNode.props as InputProps)
|
||||||
|
.checked;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof onChangeProp === "function" &&
|
typeof onChangeProp === "function" &&
|
||||||
@@ -260,7 +257,10 @@ export class Application {
|
|||||||
const valueProp = (this.focusedNode.props as InputProps).value;
|
const valueProp = (this.focusedNode.props as InputProps).value;
|
||||||
const onInputProp = (this.focusedNode.props as InputProps).onInput;
|
const onInputProp = (this.focusedNode.props as InputProps).onInput;
|
||||||
|
|
||||||
if (typeof valueProp !== "function" || typeof onInputProp !== "function") {
|
if (
|
||||||
|
typeof valueProp !== "function" ||
|
||||||
|
typeof onInputProp !== "function"
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +273,10 @@ export class Application {
|
|||||||
this.needsRender = true;
|
this.needsRender = true;
|
||||||
} else if (key === keys.right) {
|
} else if (key === keys.right) {
|
||||||
// Move cursor right
|
// Move cursor right
|
||||||
this.focusedNode.cursorPos = math.min(currentValue.length, cursorPos + 1);
|
this.focusedNode.cursorPos = math.min(
|
||||||
|
currentValue.length,
|
||||||
|
cursorPos + 1,
|
||||||
|
);
|
||||||
this.needsRender = true;
|
this.needsRender = true;
|
||||||
} else if (key === keys.backspace) {
|
} else if (key === keys.backspace) {
|
||||||
// Delete character before cursor
|
// Delete character before cursor
|
||||||
@@ -301,11 +304,15 @@ export class Application {
|
|||||||
* Handle character input events
|
* Handle character input events
|
||||||
*/
|
*/
|
||||||
private handleCharEvent(char: string): void {
|
private handleCharEvent(char: string): void {
|
||||||
if (this.focusedNode !== undefined && this.focusedNode.type === "input") {
|
if (
|
||||||
|
this.focusedNode !== undefined &&
|
||||||
|
this.focusedNode.type === "input"
|
||||||
|
) {
|
||||||
const type = (this.focusedNode.props as InputProps).type;
|
const type = (this.focusedNode.props as InputProps).type;
|
||||||
if (type !== "checkbox") {
|
if (type !== "checkbox") {
|
||||||
// Insert character at cursor position
|
// Insert character at cursor position
|
||||||
const onInputProp = (this.focusedNode.props as InputProps).onInput;
|
const onInputProp = (this.focusedNode.props as InputProps)
|
||||||
|
.onInput;
|
||||||
const valueProp = (this.focusedNode.props as InputProps).value;
|
const valueProp = (this.focusedNode.props as InputProps).value;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -338,7 +345,10 @@ export class Application {
|
|||||||
|
|
||||||
if (clicked !== undefined) {
|
if (clicked !== undefined) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
string.format("handleMouseClick: Found node of type %s.", clicked.type),
|
string.format(
|
||||||
|
"handleMouseClick: Found node of type %s.",
|
||||||
|
clicked.type,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
// Set focus
|
// Set focus
|
||||||
if (
|
if (
|
||||||
@@ -351,7 +361,8 @@ export class Application {
|
|||||||
}
|
}
|
||||||
this.focusedNode = clicked;
|
this.focusedNode = clicked;
|
||||||
if (typeof clicked.props.onFocusChanged === "function") {
|
if (typeof clicked.props.onFocusChanged === "function") {
|
||||||
const onFocusChanged = clicked.props.onFocusChanged as Setter<boolean>;
|
const onFocusChanged = clicked.props
|
||||||
|
.onFocusChanged as Setter<boolean>;
|
||||||
onFocusChanged(true);
|
onFocusChanged(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,11 +386,15 @@ export class Application {
|
|||||||
"handleMouseClick: onClick handler found, executing.",
|
"handleMouseClick: onClick handler found, executing.",
|
||||||
);
|
);
|
||||||
(onClick as () => void)();
|
(onClick as () => void)();
|
||||||
this.logger.debug("handleMouseClick: onClick handler finished.");
|
this.logger.debug(
|
||||||
|
"handleMouseClick: onClick handler finished.",
|
||||||
|
);
|
||||||
this.needsRender = true;
|
this.needsRender = true;
|
||||||
}
|
}
|
||||||
} else if (clicked.type === "input") {
|
} else if (clicked.type === "input") {
|
||||||
const type = (clicked.props as InputProps).type as string | undefined;
|
const type = (clicked.props as InputProps).type as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
if (type === "checkbox") {
|
if (type === "checkbox") {
|
||||||
const onChangeProp = (clicked.props as InputProps).onChange;
|
const onChangeProp = (clicked.props as InputProps).onChange;
|
||||||
const checkedProp = (clicked.props as InputProps).checked;
|
const checkedProp = (clicked.props as InputProps).checked;
|
||||||
@@ -397,7 +412,9 @@ export class Application {
|
|||||||
|
|
||||||
this.needsRender = true;
|
this.needsRender = true;
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug("handleMouseClick: No node found at click position.");
|
this.logger.debug(
|
||||||
|
"handleMouseClick: No node found at click position.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,7 +449,9 @@ export class Application {
|
|||||||
);
|
);
|
||||||
// Only return interactive elements
|
// Only return interactive elements
|
||||||
if (node.type === "button" || node.type === "input") {
|
if (node.type === "button" || node.type === "input") {
|
||||||
this.logger.debug("findNodeAt: Node is interactive, returning.");
|
this.logger.debug(
|
||||||
|
"findNodeAt: Node is interactive, returning.",
|
||||||
|
);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -502,7 +521,9 @@ export class Application {
|
|||||||
);
|
);
|
||||||
// Only return scrollable elements
|
// Only return scrollable elements
|
||||||
if (node.type === "scroll-container") {
|
if (node.type === "scroll-container") {
|
||||||
this.logger.debug("findNodeAt: Node is scrollable, returning.");
|
this.logger.debug(
|
||||||
|
"findNodeAt: Node is scrollable, returning.",
|
||||||
|
);
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,12 +167,15 @@ export function label(
|
|||||||
const sentences = createMemo(() => {
|
const sentences = createMemo(() => {
|
||||||
const words = splitByWhitespace(text());
|
const words = splitByWhitespace(text());
|
||||||
const ret = concatSentence(words, 40);
|
const ret = concatSentence(words, 40);
|
||||||
context.logger?.debug(`label words changed : [ ${ret.join(",")} ]`);
|
context.logger?.debug(
|
||||||
|
`label words changed : [ ${ret.join(",")} ]`,
|
||||||
|
);
|
||||||
return ret;
|
return ret;
|
||||||
});
|
});
|
||||||
|
|
||||||
const forNode = For({ class: `flex flex-col`, each: sentences }, (word) =>
|
const forNode = For(
|
||||||
label({ class: p.class }, word),
|
{ class: `flex flex-col`, each: sentences },
|
||||||
|
(word) => label({ class: p.class }, word),
|
||||||
);
|
);
|
||||||
|
|
||||||
return forNode;
|
return forNode;
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
* to all components without prop drilling.
|
* to all components without prop drilling.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { CCLog } from "../ccLog";
|
import { Logger } from "@/lib/ccStructLog";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The global context object for the TUI application.
|
* The global context object for the TUI application.
|
||||||
* This will be set by the Application instance on creation.
|
* This will be set by the Application instance on creation.
|
||||||
*/
|
*/
|
||||||
export const context: { logger: CCLog | undefined } = {
|
export const context: { logger: Logger | undefined } = {
|
||||||
logger: undefined,
|
logger: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -18,6 +18,6 @@ export const context: { logger: CCLog | undefined } = {
|
|||||||
* Sets the global logger instance.
|
* Sets the global logger instance.
|
||||||
* @param l The logger instance.
|
* @param l The logger instance.
|
||||||
*/
|
*/
|
||||||
export function setLogger(l: CCLog): void {
|
export function setLogger(l: Logger): void {
|
||||||
context.logger = l;
|
context.logger = l;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ function measureNode(
|
|||||||
if (node.styleProps.width === "screen") {
|
if (node.styleProps.width === "screen") {
|
||||||
const termSize = getTerminalSize();
|
const termSize = getTerminalSize();
|
||||||
measuredWidth = termSize.width;
|
measuredWidth = termSize.width;
|
||||||
} else if (node.styleProps.width === "full" && parentWidth !== undefined) {
|
} else if (
|
||||||
|
node.styleProps.width === "full" &&
|
||||||
|
parentWidth !== undefined
|
||||||
|
) {
|
||||||
measuredWidth = parentWidth;
|
measuredWidth = parentWidth;
|
||||||
} else if (typeof node.styleProps.width === "number") {
|
} else if (typeof node.styleProps.width === "number") {
|
||||||
measuredWidth = node.styleProps.width;
|
measuredWidth = node.styleProps.width;
|
||||||
@@ -297,7 +300,13 @@ export function calculateLayout(
|
|||||||
const childY = startY + scrollOffsetY;
|
const childY = startY + scrollOffsetY;
|
||||||
|
|
||||||
// Recursively calculate layout for child with its natural size
|
// Recursively calculate layout for child with its natural size
|
||||||
calculateLayout(child, childSize.width, childSize.height, childX, childY);
|
calculateLayout(
|
||||||
|
child,
|
||||||
|
childSize.width,
|
||||||
|
childSize.height,
|
||||||
|
childX,
|
||||||
|
childY,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -368,7 +377,8 @@ export function calculateLayout(
|
|||||||
|
|
||||||
// Cross axis (vertical) alignment
|
// Cross axis (vertical) alignment
|
||||||
if (align === "center") {
|
if (align === "center") {
|
||||||
childY = startY + math.floor((availableHeight - measure.height) / 2);
|
childY =
|
||||||
|
startY + math.floor((availableHeight - measure.height) / 2);
|
||||||
} else if (align === "end") {
|
} else if (align === "end") {
|
||||||
childY = startY + (availableHeight - measure.height);
|
childY = startY + (availableHeight - measure.height);
|
||||||
} else {
|
} else {
|
||||||
@@ -385,7 +395,8 @@ export function calculateLayout(
|
|||||||
|
|
||||||
// Cross axis (horizontal) alignment
|
// Cross axis (horizontal) alignment
|
||||||
if (align === "center") {
|
if (align === "center") {
|
||||||
childX = startX + math.floor((availableWidth - measure.width) / 2);
|
childX =
|
||||||
|
startX + math.floor((availableWidth - measure.width) / 2);
|
||||||
} else if (align === "end") {
|
} else if (align === "end") {
|
||||||
childX = startX + (availableWidth - measure.width);
|
childX = startX + (availableWidth - measure.width);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ export function createSignal<T>(initialValue: T): Signal<T> {
|
|||||||
// Notify all subscribed listeners
|
// Notify all subscribed listeners
|
||||||
if (batchDepth > 0) {
|
if (batchDepth > 0) {
|
||||||
// In batch mode, collect effects to run later
|
// In batch mode, collect effects to run later
|
||||||
listeners.forEach(listener => pendingEffects.add(listener));
|
listeners.forEach((listener) => pendingEffects.add(listener));
|
||||||
} else {
|
} else {
|
||||||
// Run effects immediately
|
// Run effects immediately
|
||||||
listeners.forEach(listener => {
|
listeners.forEach((listener) => {
|
||||||
try {
|
try {
|
||||||
listener();
|
listener();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -152,7 +152,7 @@ export function batch(fn: () => void): void {
|
|||||||
const effects = Array.from(pendingEffects);
|
const effects = Array.from(pendingEffects);
|
||||||
pendingEffects.clear();
|
pendingEffects.clear();
|
||||||
|
|
||||||
effects.forEach(effect => {
|
effects.forEach((effect) => {
|
||||||
try {
|
try {
|
||||||
effect();
|
effect();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ function getTextContent(node: UIObject): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For nodes with text children, get their content
|
// For nodes with text children, get their content
|
||||||
if (node.children.length > 0 && node.children[0].textContent !== undefined) {
|
if (
|
||||||
|
node.children.length > 0 &&
|
||||||
|
node.children[0].textContent !== undefined
|
||||||
|
) {
|
||||||
const child = node.children[0];
|
const child = node.children[0];
|
||||||
if (typeof child.textContent === "function") {
|
if (typeof child.textContent === "function") {
|
||||||
return child.textContent();
|
return child.textContent();
|
||||||
@@ -39,7 +42,11 @@ function isPositionVisible(
|
|||||||
): boolean {
|
): boolean {
|
||||||
let current = node.parent;
|
let current = node.parent;
|
||||||
while (current) {
|
while (current) {
|
||||||
if (isScrollContainer(current) && current.layout && current.scrollProps) {
|
if (
|
||||||
|
isScrollContainer(current) &&
|
||||||
|
current.layout &&
|
||||||
|
current.scrollProps
|
||||||
|
) {
|
||||||
const { x: containerX, y: containerY } = current.layout;
|
const { x: containerX, y: containerY } = current.layout;
|
||||||
const { viewportWidth, viewportHeight } = current.scrollProps;
|
const { viewportWidth, viewportHeight } = current.scrollProps;
|
||||||
|
|
||||||
@@ -189,7 +196,9 @@ function drawNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "input": {
|
case "input": {
|
||||||
const type = (node.props as InputProps).type as string | undefined;
|
const type = (node.props as InputProps).type as
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
|
||||||
if (type === "checkbox") {
|
if (type === "checkbox") {
|
||||||
// Draw checkbox
|
// Draw checkbox
|
||||||
@@ -224,7 +233,11 @@ function drawNode(
|
|||||||
const focusedBgColor = bgColor ?? colors.white;
|
const focusedBgColor = bgColor ?? colors.white;
|
||||||
const unfocusedBgColor = bgColor ?? colors.black;
|
const unfocusedBgColor = bgColor ?? colors.black;
|
||||||
|
|
||||||
if (displayText === "" && placeholder !== undefined && !focused) {
|
if (
|
||||||
|
displayText === "" &&
|
||||||
|
placeholder !== undefined &&
|
||||||
|
!focused
|
||||||
|
) {
|
||||||
displayText = placeholder;
|
displayText = placeholder;
|
||||||
showPlaceholder = true;
|
showPlaceholder = true;
|
||||||
currentTextColor = currentTextColor ?? colors.gray;
|
currentTextColor = currentTextColor ?? colors.gray;
|
||||||
@@ -235,7 +248,9 @@ function drawNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set background and clear the input area, creating a 1-character padding on the left
|
// Set background and clear the input area, creating a 1-character padding on the left
|
||||||
term.setBackgroundColor(focused ? focusedBgColor : unfocusedBgColor);
|
term.setBackgroundColor(
|
||||||
|
focused ? focusedBgColor : unfocusedBgColor,
|
||||||
|
);
|
||||||
term.setCursorPos(x, y);
|
term.setCursorPos(x, y);
|
||||||
term.write(" ".repeat(width));
|
term.write(" ".repeat(width));
|
||||||
|
|
||||||
@@ -247,7 +262,9 @@ function drawNode(
|
|||||||
|
|
||||||
// Move text if it's too long for the padded area
|
// Move text if it's too long for the padded area
|
||||||
const startDisPos =
|
const startDisPos =
|
||||||
cursorPos >= renderWidth ? cursorPos - renderWidth + 1 : 0;
|
cursorPos >= renderWidth
|
||||||
|
? cursorPos - renderWidth + 1
|
||||||
|
: 0;
|
||||||
const stopDisPos = startDisPos + renderWidth;
|
const stopDisPos = startDisPos + renderWidth;
|
||||||
|
|
||||||
if (focused && !showPlaceholder && cursorBlinkState) {
|
if (focused && !showPlaceholder && cursorBlinkState) {
|
||||||
@@ -271,7 +288,10 @@ function drawNode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Draw cursor at the end of the text if applicable
|
// Draw cursor at the end of the text if applicable
|
||||||
if (cursorPos === textToRender.length && cursorPos < renderWidth) {
|
if (
|
||||||
|
cursorPos === textToRender.length &&
|
||||||
|
cursorPos < renderWidth
|
||||||
|
) {
|
||||||
term.setBackgroundColor(currentTextColor);
|
term.setBackgroundColor(currentTextColor);
|
||||||
term.setTextColor(focusedBgColor);
|
term.setTextColor(focusedBgColor);
|
||||||
term.write(" ");
|
term.write(" ");
|
||||||
@@ -281,7 +301,9 @@ function drawNode(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Not focused or no cursor, just write the text
|
// Not focused or no cursor, just write the text
|
||||||
term.write(textToRender.substring(startDisPos, stopDisPos));
|
term.write(
|
||||||
|
textToRender.substring(startDisPos, stopDisPos),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ export interface SetStoreFunction<T> {
|
|||||||
* setTodos([{ title: "First", done: false }]);
|
* setTodos([{ title: "First", done: false }]);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function createStore<T extends object>(initialValue: T): [Accessor<T>, SetStoreFunction<T>] {
|
export function createStore<T extends object>(
|
||||||
|
initialValue: T,
|
||||||
|
): [Accessor<T>, SetStoreFunction<T>] {
|
||||||
// Use a signal to track the entire state
|
// Use a signal to track the entire state
|
||||||
const [get, set] = createSignal(initialValue);
|
const [get, set] = createSignal(initialValue);
|
||||||
|
|
||||||
@@ -88,8 +90,11 @@ export function createStore<T extends object>(initialValue: T): [Accessor<T>, Se
|
|||||||
|
|
||||||
if (Array.isArray(current)) {
|
if (Array.isArray(current)) {
|
||||||
const newArray = [...current] as unknown[];
|
const newArray = [...current] as unknown[];
|
||||||
if (typeof newArray[index] === "object" && newArray[index] !== undefined) {
|
if (
|
||||||
newArray[index] = { ...(newArray[index]!), [key]: value };
|
typeof newArray[index] === "object" &&
|
||||||
|
newArray[index] !== undefined
|
||||||
|
) {
|
||||||
|
newArray[index] = { ...newArray[index]!, [key]: value };
|
||||||
}
|
}
|
||||||
set(newArray as T);
|
set(newArray as T);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,13 @@ const customLogger = new Logger({
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
renderer: jsonRenderer,
|
renderer: jsonRenderer,
|
||||||
streams: [new ConsoleStream(), new FileStream("custom.log", HOUR)],
|
streams: [
|
||||||
|
new ConsoleStream(),
|
||||||
|
new FileStream({
|
||||||
|
filePath: "custom.log",
|
||||||
|
rotationInterval: HOUR,
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
customLogger.info("Custom logger example", {
|
customLogger.info("Custom logger example", {
|
||||||
@@ -136,7 +142,10 @@ const multiFormatLogger = new Logger({
|
|||||||
{
|
{
|
||||||
write: (_, event) => {
|
write: (_, event) => {
|
||||||
const formatted = jsonRenderer(event);
|
const formatted = jsonRenderer(event);
|
||||||
new FileStream("structured.log").write(formatted, event);
|
new FileStream({ filePath: "structured.log" }).write(
|
||||||
|
formatted,
|
||||||
|
event,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -193,7 +202,7 @@ print("\n=== Cleanup Examples ===");
|
|||||||
const fileLogger = new Logger({
|
const fileLogger = new Logger({
|
||||||
processors: [processor.addTimestamp()],
|
processors: [processor.addTimestamp()],
|
||||||
renderer: jsonRenderer,
|
renderer: jsonRenderer,
|
||||||
streams: [new FileStream("temp.log")],
|
streams: [new FileStream({ filePath: "structured.log" })],
|
||||||
});
|
});
|
||||||
|
|
||||||
fileLogger.info("Temporary log entry");
|
fileLogger.info("Temporary log entry");
|
||||||
|
|||||||
Reference in New Issue
Block a user