Compare commits

..

7 Commits

25 changed files with 4709 additions and 4479 deletions

View File

@@ -5,6 +5,7 @@ A collection of advanced utilities and libraries for Minecraft ComputerCraft, wr
## Features ## Features
### 1. Access Control System ### 1. Access Control System
A comprehensive system for managing player access to a specific area. It uses a `playerDetector` to monitor for players in range and a `chatBox` to interact with them and administrators. A comprehensive system for managing player access to a specific area. It uses a `playerDetector` to monitor for players in range and a `chatBox` to interact with them and administrators.
- **Player Detection:** Monitors a configurable range for players. - **Player Detection:** Monitors a configurable range for players.
@@ -15,6 +16,7 @@ A comprehensive system for managing player access to a specific area. It uses a
- **Logging:** Detailed logging of events, viewable with the included `logviewer` program. - **Logging:** Detailed logging of events, viewable with the included `logviewer` program.
### 2. AutoCraft System ### 2. AutoCraft System
An automated crafting solution designed to work with the Create mod's packaged recipes. An automated crafting solution designed to work with the Create mod's packaged recipes.
- **Automated Crafting:** Detects cardboard packages in a chest and automatically crafts the recipes they contain. - **Automated Crafting:** Detects cardboard packages in a chest and automatically crafts the recipes they contain.
@@ -22,6 +24,7 @@ An automated crafting solution designed to work with the Create mod's packaged r
- **Inventory Management:** Manages pulling ingredients from a source inventory and pushing crafted items to a destination. - **Inventory Management:** Manages pulling ingredients from a source inventory and pushing crafted items to a destination.
### 3. ccTUI Framework ### 3. ccTUI Framework
A declarative, reactive TUI (Terminal User Interface) framework inspired by [SolidJS](https://www.solidjs.com/) for building complex and interactive interfaces in ComputerCraft. A declarative, reactive TUI (Terminal User Interface) framework inspired by [SolidJS](https://www.solidjs.com/) for building complex and interactive interfaces in ComputerCraft.
- **Declarative Syntax:** Build UIs with simple, composable functions like `div`, `label`, `button`, and `input`. - **Declarative Syntax:** Build UIs with simple, composable functions like `div`, `label`, `button`, and `input`.
@@ -31,6 +34,7 @@ A declarative, reactive TUI (Terminal User Interface) framework inspired by [Sol
- **Component-Based:** Structure your UI into reusable components. See `src/tuiExample/main.ts` for a demo. - **Component-Based:** Structure your UI into reusable components. See `src/tuiExample/main.ts` for a demo.
### 4. ccCLI Framework ### 4. ccCLI Framework
A lightweight, functional-style framework for building command-line interfaces (CLIs) within CC:Tweaked. It supports nested commands, arguments, options, and automatic help generation. See the [ccCLI Documentation](./docs/ccCLI.md) for more details. A lightweight, functional-style framework for building command-line interfaces (CLIs) within CC:Tweaked. It supports nested commands, arguments, options, and automatic help generation. See the [ccCLI Documentation](./docs/ccCLI.md) for more details.
- **Declarative API:** Define commands, arguments, and options using a simple, object-based structure. - **Declarative API:** Define commands, arguments, and options using a simple, object-based structure.
@@ -40,6 +44,7 @@ A lightweight, functional-style framework for building command-line interfaces (
- **Type-Safe:** Built with TypeScript for robust development. - **Type-Safe:** Built with TypeScript for robust development.
### 5. Core Libraries ### 5. Core Libraries
- **`ChatManager`:** A powerful manager for `chatBox` peripherals that handles message queuing, cooldowns, and asynchronous sending/receiving. See the [ChatManager Documentation](./docs/ChatManager.md) for more details. - **`ChatManager`:** A powerful manager for `chatBox` peripherals that handles message queuing, cooldowns, and asynchronous sending/receiving. See the [ChatManager Documentation](./docs/ChatManager.md) for more details.
- **`ccStructLog`:** A modern, structured logging library inspired by Python's `structlog`. It provides a flexible, extensible framework based on processors, renderers, and streams, designed for CC:Tweaked. See the [ccStructLog Documentation](./docs/ccStructLog.md) for more details. - **`ccStructLog`:** A modern, structured logging library inspired by Python's `structlog`. It provides a flexible, extensible framework based on processors, renderers, and streams, designed for CC:Tweaked. See the [ccStructLog Documentation](./docs/ccStructLog.md) for more details.
- **`PeripheralManager`:** A utility for easily finding and requiring peripherals by name or type. - **`PeripheralManager`:** A utility for easily finding and requiring peripherals by name or type.
@@ -54,6 +59,7 @@ A lightweight, functional-style framework for building command-line interfaces (
## Setup & Installation ## Setup & Installation
1. **Clone the repository:** 1. **Clone the repository:**
```sh ```sh
git clone <repository-url> git clone <repository-url>
cd cc-utils cd cc-utils
@@ -69,6 +75,7 @@ A lightweight, functional-style framework for building command-line interfaces (
This project uses `just` to manage build tasks. The compiled Lua files will be placed in the `build/` directory. This project uses `just` to manage build tasks. The compiled Lua files will be placed in the `build/` directory.
- **Build all modules:** - **Build all modules:**
```sh ```sh
just build just build
``` ```
@@ -105,44 +112,30 @@ To deploy the built programs to your in-game computer, you need to configure the
### Access Control ### Access Control
- **Start the system:** - **Start the system:**
```sh ```sh
accesscontrol start accesscontrol start
``` ```
- **Open the configuration TUI:** - **Open the configuration TUI:**
```sh ```sh
accesscontrol config accesscontrol config
``` ```
Alternatively, press `c` while the main program is running.
- **View logs:** Alternatively, press `c` while the main program is running.
```sh
logviewer accesscontrol.log
```
- **Admin Commands (in-game chat):** - **Admin Commands (in-game chat):**
``` ```
@AC /help @AC help
@AC /add user Notch @AC add user Notch
@AC /list @AC list
``` ```
### AutoCraft ### AutoCraft
The autocraft program runs in the background. Simply run it on a turtle with the correct peripheral setup (see `src/autocraft/main.ts`). It will automatically process packages placed in the designated chest. The autocraft program runs in the background. Simply run it on a turtle with the correct peripheral setup (see `src/autocraft/main.ts`). It will automatically process packages placed in the designated chest.
```sh
autocraft
```
### TUI Example
Run the example program to see a demonstration of the `ccTUI` framework.
```sh
tuiExample
```
## Development ## Development
- **Lint and format the code:** - **Lint and format the code:**

View File

@@ -11,16 +11,37 @@ A modern, structured logging library for CC:Tweaked, inspired by Python's struct
## Quick Start ## Quick Start
```typescript The easiest way to get started is to create a `Logger` instance and configure it with processors, a renderer, and streams.
import { createDevLogger } from "@/lib/ccStructLog";
// Create a development logger Here's a simple example of a logger that prints colored, human-readable messages to the console:
const logger = createDevLogger();
```typescript
import {
Logger,
LogLevel,
processor,
textRenderer,
ConsoleStream,
} from "@/lib/ccStructLog";
// Create a logger
const logger = new Logger({
processors: [
processor.addTimestamp({ format: "%T" }), // Add HH:MM:SS timestamp
processor.filterByLevel(LogLevel.Info), // Log Info and higher
processor.addSource("MyApp"),
],
renderer: textRenderer,
streams: [new ConsoleStream()],
});
// Log messages with context // Log messages with context
logger.info("Server started", { port: 8080, version: "1.0.0" }); logger.info("Server started", { port: 8080, version: "1.0.0" });
logger.warn("Low disk space", { available: 1024, threshold: 2048 }); logger.warn("Low disk space", { available: 1024, threshold: 2048 });
logger.error("Connection failed", { host: "example.com", retries: 3 }); logger.error("Connection failed", { host: "example.com", retries: 3 });
// This debug message will be filtered out by `filterByLevel`
logger.debug("This is a debug message.");
``` ```
## Core Concepts ## Core Concepts
@@ -46,48 +67,69 @@ export enum LogLevel {
4. **Render**: The final event is converted to a string by a renderer (e.g., `textRenderer`, `jsonRenderer`). 4. **Render**: The final event is converted to a string by a renderer (e.g., `textRenderer`, `jsonRenderer`).
5. **Output**: The string is sent to one or more streams (e.g., console, file). 5. **Output**: The string is sent to one or more streams (e.g., console, file).
## Pre-configured Loggers ## Common Configurations
### Development Logger ### Development Logger
Optimized for development and debugging with human-readable console output. A typical development logger is configured for human-readable console output with timestamps and colors.
```typescript ```typescript
import { createDevLogger, LogLevel } from "@/lib/ccStructLog"; import {
Logger,
processor,
textRenderer,
ConsoleStream,
} from "@/lib/ccStructLog";
const logger = createDevLogger({ const devLogger = new Logger({
source: "MyApp", processors: [
includeComputerId: true, processor.addTimestamp({ format: "%F %T" }), // YYYY-MM-DD HH:MM:SS
processor.addSource("DevApp"),
processor.addComputerId(),
],
renderer: textRenderer,
streams: [new ConsoleStream()],
}); });
logger.debug("This is a debug message."); devLogger.debug("This is a debug message.", { user: "dev" });
``` ```
### Production Logger ### Production Logger
Optimized for production with JSON-formatted file output and daily rotation. A production logger is often configured to write machine-readable JSON logs to a file with daily rotation.
```typescript
import { createProdLogger, DAY } from "@/lib/ccStructLog";
const logger = createProdLogger("app.log", {
source: "MyApp",
rotationInterval: DAY, // Rotate daily
includeConsole: false, // Don't log to console
});
logger.info("Application is running in production.");
```
## Custom Configuration
You can create a logger with a completely custom setup.
```typescript ```typescript
import { import {
Logger, Logger,
LogLevel, LogLevel,
addFullTimestamp, processor,
addComputerId, jsonRenderer,
addSource, FileStream,
DAY,
} from "@/lib/ccStructLog";
const prodLogger = new Logger({
processors: [
processor.addTimestamp(), // Default format is %F %T
processor.filterByLevel(LogLevel.Info),
processor.addSource("ProdApp"),
processor.addComputerId(),
],
renderer: jsonRenderer,
streams: [
new FileStream("app.log", DAY), // Rotate daily
],
});
prodLogger.info("Application is running in production.");
```
## Custom Configuration
You can create a logger with any combination of processors, renderers, and streams.
```typescript
import {
Logger,
processor,
jsonRenderer, jsonRenderer,
FileStream, FileStream,
ConsoleStream, ConsoleStream,
@@ -96,9 +138,9 @@ import {
const logger = new Logger({ const logger = new Logger({
processors: [ processors: [
addFullTimestamp(), processor.addTimestamp(),
addComputerId(), processor.addComputerId(),
addSource("MyApplication"), processor.addSource("MyApplication"),
], ],
renderer: jsonRenderer, renderer: jsonRenderer,
streams: [ streams: [
@@ -112,31 +154,40 @@ logger.info("Custom logger reporting for duty.", { user: "admin" });
## Processors ## Processors
Processors are functions that modify, enrich, or filter log events before they are rendered. Processors are functions that modify, enrich, or filter log events before they are rendered. They are all available under the `processor` namespace.
### Built-in Processors ### Built-in Processors
```typescript ```typescript
import { import { Logger, LogLevel, processor } from "@/lib/ccStructLog";
addTimestamp, // Add structured timestamp
addFormattedTimestamp, // Add HH:MM:SS string
addFullTimestamp, // Add YYYY-MM-DD HH:MM:SS string
filterByLevel, // Filter by minimum level
filterBy, // Filter based on a custom predicate
addSource, // Add source/logger name
addComputerId, // Add computer ID
addComputerLabel, // Add computer label
addStaticFields, // Add static fields to all events
transformField, // Transform a specific field's value
removeFields, // Remove sensitive fields
} from "@/lib/ccStructLog";
// Usage example // Usage example
const logger = new Logger({ const logger = new Logger({
processors: [ processors: [
addTimestamp(), // Adds a timestamp. Format is compatible with os.date().
addSource("MyApp"), // Default: "%F %T" (e.g., "2023-10-27 15:30:00")
filterByLevel(LogLevel.Warn), // Only allow Warn, Error, Fatal processor.addTimestamp({ format: "%T" }), // e.g., "15:30:00"
removeFields(["password", "token"]),
// Filter by minimum level
processor.filterByLevel(LogLevel.Warn), // Only allow Warn, Error, Fatal
// Filter based on a custom predicate
processor.filterBy((event) => event.get("user") === "admin"),
// Add source/logger name
processor.addSource("MyApp"),
// Add computer ID or label
processor.addComputerId(),
processor.addComputerLabel(),
// Add static fields to all events
processor.addStaticFields({ env: "production", version: "1.2.3" }),
// Transform a specific field's value
processor.transformField("user_id", (id) => `user_${id}`),
// Remove sensitive fields
processor.removeFields(["password", "token"]),
], ],
// ... other config // ... other config
}); });
@@ -172,10 +223,10 @@ Renderers convert the final `LogEvent` object into a string.
import { textRenderer, jsonRenderer } from "@/lib/ccStructLog"; import { textRenderer, jsonRenderer } from "@/lib/ccStructLog";
// textRenderer: Human-readable, colored output for the console. // textRenderer: Human-readable, colored output for the console.
// Example: 15:30:45 [INFO] Message key=value // Example: [15:30:45] [INFO] Message key=value
// jsonRenderer: Machine-readable JSON output. // jsonRenderer: Machine-readable JSON output.
// Example: {"level":"info","message":"Message","key":"value","timestamp":"..."} // Example: {"level":2,"message":"Message","key":"value","timestamp":"15:30:45"}
``` ```
## Streams ## Streams
@@ -185,12 +236,14 @@ Streams handle the final output destination. You can use multiple streams to sen
### Built-in Streams ### Built-in Streams
```typescript ```typescript
import { import {
ConsoleStream, // Output to CC:Tweaked terminal with colors ConsoleStream,
FileStream, // Output to file with rotation support FileStream,
BufferStream, // Store in an in-memory buffer BufferStream,
NullStream, // Discard all output NullStream,
ConditionalStream,
LogLevel,
DAY,
} from "@/lib/ccStructLog"; } from "@/lib/ccStructLog";
import { ConditionalStream } from "@/lib/ccStructLog/streams"; // Note direct import
// File stream with daily rotation // File stream with daily rotation
const fileStream = new FileStream("app.log", DAY); const fileStream = new FileStream("app.log", DAY);
@@ -207,7 +260,7 @@ const errorStream = new ConditionalStream(
## File Rotation ## File Rotation
`FileStream` supports automatic file rotation based on time intervals. `FileStream` supports automatic file rotation based on time intervals. The rotation interval is specified in seconds as the second argument to the constructor.
```typescript ```typescript
import { FileStream, HOUR, DAY, WEEK } from "@/lib/ccStructLog"; import { FileStream, HOUR, DAY, WEEK } from "@/lib/ccStructLog";
@@ -221,7 +274,7 @@ const dailyLog = new FileStream("app_daily.log", DAY);
// Rotate weekly // Rotate weekly
const weeklyLog = new FileStream("app_weekly.log", WEEK); const weeklyLog = new FileStream("app_weekly.log", WEEK);
// No rotation // No rotation (pass 0 or undefined)
const permanentLog = new FileStream("permanent.log", 0); const permanentLog = new FileStream("permanent.log", 0);
``` ```
@@ -243,15 +296,22 @@ const permanentLog = new FileStream("permanent.log", 0);
- `error`: Errors that affect a single operation but not the whole app. - `error`: Errors that affect a single operation but not the whole app.
- `fatal`: Critical errors that require the application to shut down. - `fatal`: Critical errors that require the application to shut down.
3. **Use a `source`**: Identify which component generated the log. 3. **Use a `source`**: Identify which component generated the log using `processor.addSource`.
```typescript ```typescript
const logger = createDevLogger({ source: "UserService" }); import { Logger, processor } from "@/lib/ccStructLog";
const logger = new Logger({
processors: [processor.addSource("UserService")],
// ...
});
``` ```
4. **Sanitize Sensitive Data**: Use a processor to remove passwords, API keys, etc. 4. **Sanitize Sensitive Data**: Use a processor to remove passwords, API keys, etc.
```typescript ```typescript
import { Logger, processor } from "@/lib/ccStructLog";
const secureLogger = new Logger({ const secureLogger = new Logger({
processors: [ removeFields(["password", "token"]) ], processors: [ processor.removeFields(["password", "token"]) ],
//... //...
}); });
``` ```

View File

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

View File

@@ -1,4 +1,3 @@
import { CCLog, DAY, LogLevel } from "@/lib/ccLog";
import { ToastConfig, UserGroupConfig, loadConfig } from "./config"; import { ToastConfig, UserGroupConfig, loadConfig } from "./config";
import { createAccessControlCli } from "./cli"; import { createAccessControlCli } from "./cli";
import { launchAccessControlTUI } from "./tui"; import { launchAccessControlTUI } from "./tui";
@@ -7,15 +6,45 @@ import { ReadWriteLock } from "@/lib/mutex/ReadWriteLock";
import { ChatManager } from "@/lib/ChatManager"; import { ChatManager } from "@/lib/ChatManager";
import { gTimerManager } from "@/lib/TimerManager"; import { gTimerManager } from "@/lib/TimerManager";
import { KeyEvent, pullEventAs } from "@/lib/event"; import { KeyEvent, pullEventAs } from "@/lib/event";
import {
ConditionalStream,
ConsoleStream,
DAY,
FileStream,
getStructLogger,
LoggerOptions,
LogLevel,
MB,
processor,
setStructLoggerConfig,
textRenderer,
} from "@/lib/ccStructLog";
const args = [...$vararg]; const args = [...$vararg];
// Init Log // Init Log
const logger = new CCLog("accesscontrol.log", { let isOnConsoleStream = true;
printTerminal: true, const loggerConfig: LoggerOptions = {
logInterval: DAY, processors: [
outputMinLevel: LogLevel.Info, processor.filterByLevel(LogLevel.Info),
}); processor.addTimestamp(),
],
renderer: textRenderer,
streams: [
new ConditionalStream(new ConsoleStream(), () => isOnConsoleStream),
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`;
@@ -54,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(
@@ -203,7 +237,9 @@ function watchLoop() {
continue; continue;
} }
const watchPlayerNames = gWatchPlayersInfo.flatMap((value) => value.name); const watchPlayerNames = gWatchPlayersInfo.flatMap(
(value) => value.name,
);
logger.debug(`Watch Players [ ${watchPlayerNames.join(", ")} ]`); logger.debug(`Watch Players [ ${watchPlayerNames.join(", ")} ]`);
for (const player of gWatchPlayersInfo) { for (const player of gWatchPlayersInfo) {
const playerInfo = playerDetector.getPlayerPos(player.name); const playerInfo = playerDetector.getPlayerPos(player.name);
@@ -329,13 +365,15 @@ function keyboardLoop() {
if (event.key === keys.c) { if (event.key === keys.c) {
logger.info("Launching Access Control TUI..."); logger.info("Launching Access Control TUI...");
try { try {
logger.setInTerminal(false); isOnConsoleStream = false;
launchAccessControlTUI(); launchAccessControlTUI();
logger.info("TUI closed, resuming normal operation"); logger.info("TUI closed, resuming normal operation");
} catch (error) { } catch (error) {
logger.error(`TUI error: ${textutils.serialise(error as object)}`); logger.error(
`TUI error: ${textutils.serialise(error as object)}`,
);
} finally { } finally {
logger.setInTerminal(true); isOnConsoleStream = true;
reloadConfig(); reloadConfig();
} }
} else if (event.key === keys.r) { } else if (event.key === keys.r) {
@@ -379,7 +417,9 @@ function cliLoop() {
releaser = configLock.tryAcquireRead(); releaser = configLock.tryAcquireRead();
} }
const isAdmin = config.adminGroupConfig.groupUsers.includes(ev.username); const isAdmin = config.adminGroupConfig.groupUsers.includes(
ev.username,
);
releaser.release(); releaser.release();
if (!isAdmin) continue; if (!isAdmin) continue;
@@ -422,11 +462,14 @@ function main(args: string[]) {
return; return;
} else if (args[0] == "config") { } else if (args[0] == "config") {
logger.info("Launching Access Control TUI..."); logger.info("Launching Access Control TUI...");
logger.setInTerminal(false); isOnConsoleStream = false;
try { try {
launchAccessControlTUI(); launchAccessControlTUI();
} catch (error) { } catch (error) {
logger.error(`TUI error: ${textutils.serialise(error as object)}`); logger.error(
`TUI error: ${textutils.serialise(error as object)}`,
);
} }
return; return;
} }

View File

@@ -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,

View File

@@ -3,10 +3,35 @@ import {
CraftRecipe, CraftRecipe,
CreatePackageTag, CreatePackageTag,
} from "@/lib/CraftManager"; } from "@/lib/CraftManager";
import { CCLog, LogLevel } from "@/lib/ccLog";
import { Queue } from "@/lib/datatype/Queue"; import { Queue } from "@/lib/datatype/Queue";
import {
ConsoleStream,
DAY,
FileStream,
Logger,
LogLevel,
processor,
textRenderer,
} from "@/lib/ccStructLog";
const logger = new CCLog("autocraft.log", { outputMinLevel: LogLevel.Info }); const logger = new Logger({
processors: [
processor.filterByLevel(LogLevel.Info),
processor.addTimestamp(),
],
renderer: textRenderer,
streams: [
new ConsoleStream(),
new FileStream({
filePath: "autocraft.log",
rotationInterval: DAY,
autoCleanup: {
enabled: true,
maxFiles: 3,
},
}),
],
});
const peripheralsNames = { const peripheralsNames = {
// packsInventory: "minecraft:chest_14", // packsInventory: "minecraft:chest_14",
@@ -20,22 +45,12 @@ const peripheralsNames = {
packageExtractor: "create:packager_0", packageExtractor: "create:packager_0",
}; };
const packsInventory = peripheral.wrap( let packsInventory: InventoryPeripheral;
peripheralsNames.packsInventory, let itemsInventory: InventoryPeripheral;
) as InventoryPeripheral; let packageExtractor: InventoryPeripheral;
const itemsInventory = peripheral.wrap( let blockReader: BlockReaderPeripheral;
peripheralsNames.itemsInventory, let wiredModem: WiredModemPeripheral;
) as InventoryPeripheral; let turtleLocalName: string;
const packageExtractor = peripheral.wrap(
peripheralsNames.packageExtractor,
) as InventoryPeripheral;
const blockReader = peripheral.wrap(
peripheralsNames.blockReader,
) as BlockReaderPeripheral;
const wiredModem = peripheral.wrap(
peripheralsNames.wiredModem,
) as WiredModemPeripheral;
const turtleLocalName = wiredModem.getNameLocal();
enum State { enum State {
IDLE, IDLE,
@@ -44,9 +59,40 @@ enum State {
} }
function main() { function main() {
let isFinishedInitPeripheral = false;
while (!isFinishedInitPeripheral) {
try {
packsInventory = peripheral.wrap(
peripheralsNames.packsInventory,
) as InventoryPeripheral;
itemsInventory = peripheral.wrap(
peripheralsNames.itemsInventory,
) as InventoryPeripheral;
packageExtractor = peripheral.wrap(
peripheralsNames.packageExtractor,
) as InventoryPeripheral;
blockReader = peripheral.wrap(
peripheralsNames.blockReader,
) as BlockReaderPeripheral;
wiredModem = peripheral.wrap(
peripheralsNames.wiredModem,
) as WiredModemPeripheral;
turtleLocalName = wiredModem.getNameLocal();
logger.info("Peripheral initialization complete...");
isFinishedInitPeripheral = true;
} catch (error) {
logger.warn(
`Peripheral initialization failed for ${String(error)}, try again...`,
);
sleep(1);
}
}
const craftManager = new CraftManager(turtleLocalName, itemsInventory); const craftManager = new CraftManager(turtleLocalName, itemsInventory);
const recipesQueue = new Queue<CraftRecipe>(); const recipesQueue = new Queue<CraftRecipe>();
const recipesWaitingMap = new Map<number, CraftRecipe[] | CraftRecipe>(); const recipesWaitingMap = new Map<number, CraftRecipe[] | CraftRecipe>();
let currentState = State.IDLE; let currentState = State.IDLE;
let nextState = State.IDLE; let nextState = State.IDLE;
let hasPackage = redstone.getInput(peripheralsNames.redstone); let hasPackage = redstone.getInput(peripheralsNames.redstone);
@@ -103,17 +149,20 @@ function main() {
logger.debug( logger.debug(
`Turtle:\n${textutils.serialise(blockReader.getBlockData()!, { allow_repetitions: true })}`, `Turtle:\n${textutils.serialise(blockReader.getBlockData()!, { allow_repetitions: true })}`,
); );
const packageDetailInfo = blockReader.getBlockData()?.Items[1]; const packageDetailInfo =
blockReader.getBlockData()?.Items[1];
if (packageDetailInfo === undefined) { if (packageDetailInfo === undefined) {
logger.error(`Package detail info not found`); logger.error(`Package detail info not found`);
continue; continue;
} }
// Get OrderId and isFinal // Get OrderId and isFinal
const packageOrderId = (packageDetailInfo.tag as CreatePackageTag) const packageOrderId = (
.Fragment.OrderId; packageDetailInfo.tag as CreatePackageTag
).Fragment.OrderId;
const packageIsFinal = const packageIsFinal =
(packageDetailInfo.tag as CreatePackageTag).Fragment.IsFinal > 0 (packageDetailInfo.tag as CreatePackageTag).Fragment
.IsFinal > 0
? true ? true
: false; : false;
@@ -121,11 +170,21 @@ function main() {
const packageRecipes = const packageRecipes =
CraftManager.getPackageRecipe(packageDetailInfo); CraftManager.getPackageRecipe(packageDetailInfo);
if (packageRecipes.isSome()) { if (packageRecipes.isSome()) {
if (packageIsFinal) recipesQueue.enqueue(packageRecipes.value); if (packageIsFinal)
else recipesWaitingMap.set(packageOrderId, packageRecipes.value); recipesQueue.enqueue(packageRecipes.value);
else
recipesWaitingMap.set(
packageOrderId,
packageRecipes.value,
);
} else { } else {
if (packageIsFinal && recipesWaitingMap.has(packageOrderId)) { if (
recipesQueue.enqueue(recipesWaitingMap.get(packageOrderId)!); packageIsFinal &&
recipesWaitingMap.has(packageOrderId)
) {
recipesQueue.enqueue(
recipesWaitingMap.get(packageOrderId)!,
);
recipesWaitingMap.delete(packageOrderId); recipesWaitingMap.delete(packageOrderId);
} else { } else {
logger.debug(`No recipe, just pass`); logger.debug(`No recipe, just pass`);
@@ -166,7 +225,8 @@ function main() {
}); });
if (craftCnt == 0) break; if (craftCnt == 0) break;
if (craftCnt < maxSignleCraftCnt) maxSignleCraftCnt = craftCnt; if (craftCnt < maxSignleCraftCnt)
maxSignleCraftCnt = craftCnt;
const craftRet = craftManager.craft(maxSignleCraftCnt); const craftRet = craftManager.craft(maxSignleCraftCnt);
craftItemDetail ??= craftRet; craftItemDetail ??= craftRet;
logger.info(`Craft ${craftCnt} times`); logger.info(`Craft ${craftCnt} times`);

View File

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

View File

@@ -7,150 +7,14 @@
*/ */
// Re-export all core types and classes // Re-export all core types and classes
export { export * from "./types";
LogLevel, export * from "./Logger";
LogEvent,
Processor,
Renderer,
Stream,
LoggerOptions,
ILogger,
} from "./types";
export { Logger } from "./Logger";
// Re-export all processors // Re-export all processors
export { export * from "./processors";
addTimestamp,
addFormattedTimestamp,
addFullTimestamp,
filterByLevel,
addSource,
addComputerId,
addComputerLabel,
filterBy,
transformField,
removeFields,
addStaticFields,
} from "./processors";
// Re-export all renderers // Re-export all renderers
export { jsonRenderer, textRenderer } from "./renderers"; export * from "./renderers";
// Re-export all streams // Re-export all streams
export { export * from "./streams";
ConsoleStream,
FileStream,
BufferStream,
NullStream,
SECOND,
MINUTE,
HOUR,
DAY,
WEEK,
} from "./streams";
import { Logger } from "./Logger";
import { LogLevel, LogEvent } from "./types";
import {
addFormattedTimestamp,
addFullTimestamp,
addComputerId,
} from "./processors";
import { textRenderer, jsonRenderer } from "./renderers";
import { ConsoleStream, FileStream, DAY } from "./streams";
/**
* Create a development logger with console output and colored formatting.
*
* This logger is optimized for development and debugging, with:
* - Debug level and above
* - Formatted timestamps
* - Computer ID tracking
* - Human-readable console output with colors
*
* @param options - Optional configuration to override defaults
* @returns A configured Logger instance for development
*/
export function createDevLogger(
options: {
level?: LogLevel;
source?: string;
includeComputerId?: boolean;
} = {},
): Logger {
const processors = [addFormattedTimestamp()];
if (options.includeComputerId !== false) {
processors.push(addComputerId());
}
if (options.source) {
processors.push((event: LogEvent) => {
event.set("source", options.source);
return event;
});
}
return new Logger({
processors,
renderer: textRenderer,
streams: [new ConsoleStream()],
});
}
/**
* Create a production logger with file output and JSON formatting.
*
* This logger is optimized for production environments, with:
* - Info level and above
* - Full timestamps
* - Computer ID and label tracking
* - JSON output for machine processing
* - Daily file rotation
*
* @param filename - Base filename for log files
* @param options - Optional configuration to override defaults
* @returns A configured Logger instance for production
*/
export function createProdLogger(
filename: string,
options: {
level?: LogLevel;
source?: string;
rotationInterval?: number;
includeConsole?: boolean;
} = {},
): Logger {
const processors = [
addFullTimestamp(),
addComputerId(),
(event: LogEvent) => {
const label = os.getComputerLabel();
if (label) {
event.set("computer_label", label);
}
return event;
},
];
if (options.source) {
processors.push((event: LogEvent) => {
event.set("source", options.source);
return event;
});
}
const streams: Array<ConsoleStream | FileStream> = [
new FileStream(filename, options.rotationInterval ?? DAY),
];
if (options.includeConsole) {
streams.push(new ConsoleStream());
}
return new Logger({
processors,
renderer: jsonRenderer,
streams,
});
}

View File

@@ -8,64 +8,42 @@
import { LogEvent, Processor, LogLevel } from "./types"; import { LogEvent, Processor, LogLevel } from "./types";
/** export namespace processor {
* Adds a timestamp to the log event. /**
* * Configuration options for the timestamp processor.
* This processor adds the current time as a structured timestamp object
* using CC:Tweaked's os.date() function. The timestamp includes year,
* month, day, hour, minute, and second components.
*
* Performance note: os.date() is relatively expensive, so this should
* typically be placed early in the processor chain and used only once.
*
* @param event - The log event to process
* @returns The event with timestamp added
*/ */
export function addTimestamp(): Processor { interface TimestampConfig {
/**
* The format string takes the same formats as C's strftime function.
*/
format?: string;
}
/**
* Adds a timestamp to each log event.
*
* This processor adds a "time" field to each log event with the current
* timestamp. The timestamp format can be customized using the `format`
* option.
*
* @param config - Configuration options for the timestamp processor.
* @returns A processor function that adds a timestamp to each log event.
*/
export function addTimestamp(config: TimestampConfig = {}): Processor {
return (event) => { return (event) => {
const timestamp = os.date("!*t") as LuaDate; let time: string;
event.set("timestamp", timestamp); if (config.format === undefined) {
time = os.date("%F %T") as string;
} else {
time = os.date(config.format) as string;
}
event.set("timestamp", time);
return event; return event;
}; };
} }
/** /**
* Adds a human-readable timestamp string to the log event.
*
* This processor adds a formatted timestamp string that's easier to read
* in log output. The format is "HH:MM:SS" in UTC time.
*
* @param event - The log event to process
* @returns The event with formatted timestamp added
*/
export function addFormattedTimestamp(): Processor {
return (event) => {
const timestamp = os.date("!*t") as LuaDate;
const timeStr = `${string.format("%02d", timestamp.hour)}:${string.format("%02d", timestamp.min)}:${string.format("%02d", timestamp.sec)}`;
event.set("time", timeStr);
return event;
};
}
/**
* Adds a full ISO-like timestamp string to the log event.
*
* This processor adds a complete timestamp in YYYY-MM-DD HH:MM:SS format
* which is useful for file logging and structured output.
*
* @param event - The log event to process
* @returns The event with full timestamp added
*/
export function addFullTimestamp(): Processor {
return (event) => {
const timestamp = os.date("!*t") as LuaDate;
const fullTimeStr = `${timestamp.year}-${string.format("%02d", timestamp.month)}-${string.format("%02d", timestamp.day)} ${string.format("%02d", timestamp.hour)}:${string.format("%02d", timestamp.min)}:${string.format("%02d", timestamp.sec)}`;
event.set("datetime", fullTimeStr);
return event;
};
}
/**
* Filters log events by minimum level. * Filters log events by minimum level.
* *
* This processor drops log events that are below the specified minimum level. * This processor drops log events that are below the specified minimum level.
@@ -76,38 +54,22 @@ export function addFullTimestamp(): Processor {
* @param minLevel - The minimum log level to allow through * @param minLevel - The minimum log level to allow through
* @returns A processor function that filters by level * @returns A processor function that filters by level
*/ */
export function filterByLevel(minLevel: LogLevel): Processor { export function filterByLevel(minLevel: LogLevel): Processor {
return (event) => { return (event) => {
const eventLevel = event.get("level") as LogLevel | undefined; const eventLevel = event.get("level") as LogLevel | undefined;
if (eventLevel === undefined) { if (eventLevel === undefined) {
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
} }
return event; return event;
}; };
} }
/** /**
* 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.
* *
* In CC:Tweaked environments, this can help identify which computer * In CC:Tweaked environments, this can help identify which computer
@@ -116,14 +78,14 @@ export function addSource(name: string): Processor {
* @param event - The log event to process * @param event - The log event to process
* @returns The event with computer ID added * @returns The event with computer ID added
*/ */
export function addComputerId(): Processor { export function addComputerId(): Processor {
return (event) => { return (event) => {
event.set("computer_id", os.getComputerID()); event.set("computer_id", os.getComputerID());
return event; return event;
}; };
} }
/** /**
* Adds the current computer label to the log event. * Adds the current computer label to the log event.
* *
* If the computer has a label set, this adds it to the log event. * If the computer has a label set, this adds it to the log event.
@@ -132,7 +94,7 @@ export function addComputerId(): Processor {
* @param event - The log event to process * @param event - The log event to process
* @returns The event with computer label added (if available) * @returns The event with computer label added (if available)
*/ */
export function addComputerLabel(): Processor { export function addComputerLabel(): Processor {
return (event) => { return (event) => {
const label = os.getComputerLabel(); const label = os.getComputerLabel();
if (label !== undefined && label !== null) { if (label !== undefined && label !== null) {
@@ -140,9 +102,9 @@ export function addComputerLabel(): Processor {
} }
return event; return event;
}; };
} }
/** /**
* Filters out events that match a specific condition. * Filters out events that match a specific condition.
* *
* This is a generic processor that allows you to filter events based on * This is a generic processor that allows you to filter events based on
@@ -152,13 +114,15 @@ export function addComputerLabel(): Processor {
* @param predicate - Function that returns true to keep the event * @param predicate - Function that returns true to keep the event
* @returns A processor function that filters based on the predicate * @returns A processor function that filters based on the predicate
*/ */
export function filterBy(predicate: (event: LogEvent) => boolean): Processor { export function filterBy(
predicate: (event: LogEvent) => boolean,
): Processor {
return (event) => { return (event) => {
return predicate(event) ? event : undefined; return predicate(event) ? event : undefined;
}; };
} }
/** /**
* Transforms a specific field in the log event. * Transforms a specific field in the log event.
* *
* This processor allows you to modify the value of a specific field * This processor allows you to modify the value of a specific field
@@ -168,10 +132,10 @@ export function filterBy(predicate: (event: LogEvent) => boolean): Processor {
* @param transformer - Function to transform the field value * @param transformer - Function to transform the field value
* @returns A processor function that transforms the specified field * @returns A processor function that transforms the specified field
*/ */
export function transformField( export function transformField(
fieldName: string, fieldName: string,
transformer: (value: unknown) => unknown, transformer: (value: unknown) => unknown,
): Processor { ): Processor {
return (event) => { return (event) => {
if (event.has(fieldName)) { if (event.has(fieldName)) {
const currentValue = event.get(fieldName); const currentValue = event.get(fieldName);
@@ -180,9 +144,9 @@ export function transformField(
} }
return event; return event;
}; };
} }
/** /**
* Removes specified fields from the log event. * Removes specified fields from the log event.
* *
* This processor can be used to strip sensitive or unnecessary information * This processor can be used to strip sensitive or unnecessary information
@@ -191,16 +155,16 @@ export function transformField(
* @param fieldNames - Array of field names to remove * @param fieldNames - Array of field names to remove
* @returns A processor function that removes the specified fields * @returns A processor function that removes the specified fields
*/ */
export function removeFields(fieldNames: string[]): Processor { export function removeFields(fieldNames: string[]): Processor {
return (event) => { return (event) => {
for (const fieldName of fieldNames) { for (const fieldName of fieldNames) {
event.delete(fieldName); event.delete(fieldName);
} }
return event; return event;
}; };
} }
/** /**
* Adds static fields to every log event. * Adds static fields to every log event.
* *
* This processor adds the same set of fields to every log event that * This processor adds the same set of fields to every log event that
@@ -210,11 +174,14 @@ export function removeFields(fieldNames: string[]): Processor {
* @param fields - Object containing the static fields to add * @param fields - Object containing the static fields to add
* @returns A processor function that adds the static fields * @returns A processor function that adds the static fields
*/ */
export function addStaticFields(fields: Record<string, unknown>): Processor { export function addStaticFields(
fields: Record<string, unknown>,
): Processor {
return (event) => { return (event) => {
for (const [key, value] of Object.entries(fields)) { for (const [key, value] of Object.entries(fields)) {
event.set(key, value); event.set(key, value);
} }
return event; return event;
}; };
}
} }

View File

@@ -6,7 +6,7 @@
* different output formats (JSON, console-friendly, etc.). * different output formats (JSON, console-friendly, etc.).
*/ */
import { Renderer } from "./types"; import { LogLevel, Renderer } from "./types";
/** /**
* Renders log events as JSON strings. * Renders log events as JSON strings.
@@ -43,44 +43,36 @@ export const jsonRenderer: Renderer = (event) => {
* timestamp, level, message, and additional context fields formatted * timestamp, level, message, and additional context fields formatted
* in a readable way. * in a readable way.
* *
* Format: [HH:MM:SS] [LEVEL] message { key=value, key2=value2 } * Format: [YYYY-MM-DD HH:MM:SS] [LEVEL] message key=value, key2=value2
* *
* @param event - The log event to render * @param event - The log event to render
* @returns Human-readable string representation * @returns Human-readable string representation
*/ */
export const textRenderer: Renderer = (event) => { export const textRenderer: Renderer = (event) => {
// Extract core components // Extract core components
const timestamp = event.get("timestamp") as LuaDate | undefined; const timeStr = event.get("timestamp") as string | undefined;
const timeStr = event.get("time") as string | undefined; const level: string | undefined = LogLevel[event.get("level") as LogLevel];
const level = (event.get("level") as string)?.toUpperCase() ?? "UNKNOWN";
const message = (event.get("message") as string) ?? ""; const message = (event.get("message") as string) ?? "";
const loggerName = event.get("loggerName") as string | undefined;
// Format timestamp
let timestampStr = "";
if (timeStr) {
timestampStr = timeStr;
} else if (timestamp) {
timestampStr = `${string.format("%02d", timestamp.hour)}:${string.format("%02d", timestamp.min)}:${string.format("%02d", timestamp.sec)}`;
}
// Start building the output // Start building the output
let output = `[${timestampStr}] [${level}] ${message}`; 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 ( if (
key !== "timestamp" && key !== "timestamp" &&
key !== "time" &&
key !== "level" && key !== "level" &&
key !== "message" key !== "message" &&
key !== "loggerName"
) { ) {
contextFields.push(`${key}=${tostring(value)}`); contextFields.push(`${key}=${tostring(value)}`);
} }
} }
if (contextFields.length > 0) { if (contextFields.length > 0) {
output += ` { ${contextFields.join(", ")} }`; output += contextFields.join(", ");
} }
return output; return output;

View File

@@ -6,7 +6,34 @@
* implements the Stream interface and handles its own output logic. * implements the Stream interface and handles its own output logic.
*/ */
import { 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.
@@ -16,14 +43,14 @@ import { Stream, LogEvent } from "./types";
* color after writing each message. * color after writing each message.
*/ */
export class ConsoleStream implements Stream { export class ConsoleStream implements Stream {
private levelColors: Map<string, number> = new Map([ private levelColors: { [key: string]: number } = {
["trace", colors.lightGray], Trace: colors.lightGray,
["debug", colors.gray], Debug: colors.gray,
["info", colors.white], Info: colors.green,
["warn", colors.orange], Warn: colors.orange,
["error", colors.red], Error: colors.red,
["fatal", colors.red], Fatal: colors.red,
]); };
/** /**
* Write a formatted log message to the terminal. * Write a formatted log message to the terminal.
@@ -32,8 +59,9 @@ export class ConsoleStream implements Stream {
* @param event - The original log event for context (used for level-based coloring) * @param event - The original log event for context (used for level-based coloring)
*/ */
public write(message: string, event: LogEvent): void { public write(message: string, event: LogEvent): void {
const level = event.get("level") as string | undefined; const level: string | undefined =
const color = level ? this.levelColors.get(level) : undefined; LogLevel[event.get("level") as LogLevel];
const color = level !== undefined ? this.levelColors[level] : undefined;
if (color !== undefined) { if (color !== undefined) {
const originalColor = term.getTextColor(); const originalColor = term.getTextColor();
@@ -58,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();
} }
@@ -93,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];
@@ -125,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.
* *
@@ -162,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}`);
}
}
}
} }
/** /**
@@ -306,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;

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

@@ -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) {

View File

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

View File

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

View File

View File

@@ -7,56 +7,16 @@
import { import {
Logger, Logger,
createDevLogger, processor,
createProdLogger,
// Processors
addTimestamp,
addFormattedTimestamp,
addFullTimestamp,
addSource,
addComputerId,
addStaticFields,
transformField,
// Renderers
textRenderer,
jsonRenderer,
// Streams
ConsoleStream, ConsoleStream,
FileStream, FileStream,
BufferStream, BufferStream,
DAY, ConditionalStream,
HOUR, HOUR,
jsonRenderer,
LogLevel, LogLevel,
textRenderer,
} from "../lib/ccStructLog"; } from "../lib/ccStructLog";
import { ConditionalStream } from "@/lib/ccStructLog/streams";
// =============================================================================
// Basic Usage Examples
// =============================================================================
print("=== Basic Usage Examples ===");
// 1. Quick start with pre-configured loggers
const devLog = createDevLogger();
devLog.info("Application started", { version: "1.0.0", port: 8080 });
devLog.debug("Debug information", { userId: 123, action: "login" });
devLog.error("Something went wrong", {
error: "Connection failed",
retries: 3,
});
// 2. Production logging to file
const prodLog = createProdLogger("app.log", {
source: "MyApplication",
rotationInterval: DAY,
includeConsole: true,
});
prodLog.info("User action", { userId: 456, action: "purchase", amount: 29.99 });
prodLog.warn("Low disk space", { available: 1024, threshold: 2048 });
// ============================================================================= // =============================================================================
// Custom Logger Configurations // Custom Logger Configurations
@@ -67,16 +27,22 @@ print("\n=== Custom Logger Configurations ===");
// 4. Custom logger with specific processors and renderer // 4. Custom logger with specific processors and renderer
const customLogger = new Logger({ const customLogger = new Logger({
processors: [ processors: [
addFullTimestamp(), processor.addTimestamp(),
addComputerId(), processor.addComputerId(),
addSource("CustomApp"), processor.addSource("CustomApp"),
addStaticFields({ processor.addStaticFields({
environment: "development", environment: "development",
version: "2.1.0", version: "2.1.0",
}), }),
], ],
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", {
@@ -109,10 +75,10 @@ const sanitizePasswords = (event: Map<string, unknown>) => {
const secureLogger = new Logger({ const secureLogger = new Logger({
processors: [ processors: [
addTimestamp(), processor.addTimestamp(),
addRequestId, addRequestId,
sanitizePasswords, sanitizePasswords,
transformField("message", (msg) => `[SECURE] ${msg}`), processor.transformField("message", (msg) => `[SECURE] ${msg}`),
], ],
renderer: jsonRenderer, renderer: jsonRenderer,
streams: [new ConsoleStream()], streams: [new ConsoleStream()],
@@ -133,7 +99,7 @@ print("\n=== Stream Examples ===");
// 11. Buffer stream for batch processing // 11. Buffer stream for batch processing
const bufferStream = new BufferStream(100); // Keep last 100 messages const bufferStream = new BufferStream(100); // Keep last 100 messages
const bufferLogger = new Logger({ const bufferLogger = new Logger({
processors: [addFormattedTimestamp()], processors: [processor.addTimestamp()],
renderer: textRenderer, renderer: textRenderer,
streams: [ streams: [
new ConditionalStream(new ConsoleStream(), (msg, event) => { new ConditionalStream(new ConsoleStream(), (msg, event) => {
@@ -162,7 +128,7 @@ for (const msg of bufferedMessages) {
// 12. Multi-stream with different formats // 12. Multi-stream with different formats
const multiFormatLogger = new Logger({ const multiFormatLogger = new Logger({
processors: [addFullTimestamp(), addComputerId()], processors: [processor.addTimestamp(), processor.addComputerId()],
renderer: (event) => "default", // This won't be used renderer: (event) => "default", // This won't be used
streams: [ streams: [
// Console with human-readable format // Console with human-readable format
@@ -176,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,
);
}, },
}, },
], ],
@@ -196,7 +165,7 @@ print("\n=== Error Handling Examples ===");
// 13. Robust error handling // 13. Robust error handling
const robustLogger = new Logger({ const robustLogger = new Logger({
processors: [ processors: [
addTimestamp(), processor.addTimestamp(),
// Processor that might fail // Processor that might fail
(event) => { (event) => {
try { try {
@@ -231,9 +200,9 @@ print("\n=== Cleanup Examples ===");
// 14. Proper cleanup // 14. Proper cleanup
const fileLogger = new Logger({ const fileLogger = new Logger({
processors: [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");
@@ -249,37 +218,3 @@ print("- all.log (complete log)");
print("- debug.log (detailed debug info)"); print("- debug.log (detailed debug info)");
print("- structured.log (JSON format)"); print("- structured.log (JSON format)");
print("- temp.log (temporary file, now closed)"); print("- temp.log (temporary file, now closed)");
// =============================================================================
// Performance Comparison (commented out to avoid noise)
// =============================================================================
/*
print("\n=== Performance Comparison ===");
const iterations = 1000;
// Test simple console logging
const startTime1 = os.clock();
const simpleLogger = createMinimalLogger();
for (let i = 0; i < iterations; i++) {
simpleLogger.info(`Simple message ${i}`);
}
const endTime1 = os.clock();
print(`Simple Console Logger: ${endTime1 - startTime1} seconds`);
// Test complex processor chain
const startTime2 = os.clock();
const complexLogger = createDetailedLogger("perf_test.log", {
source: "PerfTest"
});
for (let i = 0; i < iterations; i++) {
complexLogger.info(`Complex message ${i}`, {
iteration: i,
data: { nested: { value: i * 2 } }
});
}
complexLogger.close();
const endTime2 = os.clock();
print(`Complex Processor Chain: ${endTime2 - startTime2} seconds`);
*/