mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-29 12:57:50 +08:00
Compare commits
7 Commits
6d5cf11f2b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66b46c6d70 | ||
|
|
de97fb4858 | ||
|
|
0612477325 | ||
|
|
82a9fec46d | ||
|
|
94f0de4c90 | ||
|
|
cf7ddefc2e | ||
|
|
3287661318 |
33
README.md
33
README.md
@@ -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:**
|
||||||
|
|||||||
@@ -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"]) ],
|
||||||
//...
|
//...
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,59 +8,37 @@
|
|||||||
|
|
||||||
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 {
|
||||||
return (event) => {
|
/**
|
||||||
const timestamp = os.date("!*t") as LuaDate;
|
* The format string takes the same formats as C's strftime function.
|
||||||
event.set("timestamp", timestamp);
|
*/
|
||||||
return event;
|
format?: string;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a human-readable timestamp string to the log event.
|
* Adds a timestamp to each log event.
|
||||||
*
|
*
|
||||||
* This processor adds a formatted timestamp string that's easier to read
|
* This processor adds a "time" field to each log event with the current
|
||||||
* in log output. The format is "HH:MM:SS" in UTC time.
|
* timestamp. The timestamp format can be customized using the `format`
|
||||||
|
* option.
|
||||||
*
|
*
|
||||||
* @param event - The log event to process
|
* @param config - Configuration options for the timestamp processor.
|
||||||
* @returns The event with formatted timestamp added
|
* @returns A processor function that adds a timestamp to each log event.
|
||||||
*/
|
*/
|
||||||
export function addFormattedTimestamp(): Processor {
|
export function addTimestamp(config: TimestampConfig = {}): Processor {
|
||||||
return (event) => {
|
return (event) => {
|
||||||
const timestamp = os.date("!*t") as LuaDate;
|
let time: string;
|
||||||
const timeStr = `${string.format("%02d", timestamp.hour)}:${string.format("%02d", timestamp.min)}:${string.format("%02d", timestamp.sec)}`;
|
if (config.format === undefined) {
|
||||||
event.set("time", timeStr);
|
time = os.date("%F %T") as string;
|
||||||
return event;
|
} else {
|
||||||
};
|
time = os.date(config.format) as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
event.set("timestamp", time);
|
||||||
* 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;
|
return event;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -83,7 +61,7 @@ export function filterByLevel(minLevel: LogLevel): 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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,22 +69,6 @@ export function filterByLevel(minLevel: LogLevel): 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.
|
||||||
*
|
*
|
||||||
@@ -152,7 +114,9 @@ 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;
|
||||||
};
|
};
|
||||||
@@ -210,7 +174,9 @@ 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);
|
||||||
@@ -218,3 +184,4 @@ export function addStaticFields(fields: Record<string, unknown>): Processor {
|
|||||||
return event;
|
return event;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`);
|
|
||||||
*/
|
|
||||||
|
|||||||
Reference in New Issue
Block a user