add tui framework but not finish, reconstruct accesscontrol

This commit is contained in:
2025-10-10 13:20:00 +08:00
parent 1fb26cfb71
commit 57e7868c30
11 changed files with 960 additions and 156 deletions

View File

@@ -18,12 +18,14 @@ export class CCLog {
private interval: number;
private startTime: number;
private currentTimePeriod: string;
private inTerm: boolean;
constructor(filename?: string, interval: number = DAY) {
constructor(filename?: string, inTerm = true, interval: number = DAY) {
term.clear();
term.setCursorPos(1, 1);
this.interval = interval;
this.inTerm = inTerm;
this.startTime = os.time(os.date("*t"));
this.currentTimePeriod = this.getTimePeriodString(this.startTime);
@@ -117,21 +119,23 @@ export class CCLog {
// Check if we need to rotate the log file
this.checkAndRotateLogFile();
let originalColor: Color = 0;
if (color != undefined) {
originalColor = term.getTextColor();
term.setTextColor(color);
if (this.inTerm) {
let originalColor: Color = 0;
if (color != undefined) {
originalColor = term.getTextColor();
term.setTextColor(color);
}
print(msg);
if (color != undefined) {
term.setTextColor(originalColor);
}
}
// Log
print(msg);
if (this.fp != undefined) {
this.fp.write(msg + "\r\n");
}
if (color != undefined) {
term.setTextColor(originalColor);
}
}
public debug(msg: string) {

708
src/lib/ccTUI.ts Normal file
View File

@@ -0,0 +1,708 @@
/**
* ComputerCraft TUI (Terminal User Interface) Framework
* Based on Qt signal/slot principles for event handling
* Provides input/output, option selection and keyboard event handling
*/
// Import required types from the ComputerCraft environment
import { CharEvent, KeyEvent, pullEventAs } from "./event";
import { CCLog, DAY } from "./ccLog";
/**
* Signal and Slot system similar to Qt
* Allows components to communicate with each other
*/
class Signal<T = void> {
private slots: ((data?: T) => void)[] = [];
connect(slot: (data?: T) => void): void {
this.slots.push(slot);
}
disconnect(slot: (data?: T) => void): void {
const index = this.slots.indexOf(slot);
if (index !== -1) {
this.slots.splice(index, 1);
}
}
emit(data?: T): void {
for (const slot of this.slots) {
try {
slot(data);
} catch (e) {
printError(e);
}
}
}
}
abstract class UIObject {
private objectName: string;
private parent?: UIObject;
private children: Record<string, UIObject> = {};
private log?: CCLog;
constructor(name: string) {
this.objectName = name;
}
public setParent(parent: UIObject) {
this.parent = parent;
}
public addChild(child: UIObject) {
this.children[child.objectName] = child;
}
public removeChild(child: UIObject) {}
}
/**
* Base class for all UI components
*/
abstract class UIComponent extends UIObject {
protected x: number;
protected y: number;
protected width: number;
protected height: number;
protected visible = true;
protected focused = false;
// Signals for UI events
public onFocus = new Signal<void>();
public onBlur = new Signal<void>();
public onKeyPress = new Signal<KeyEvent>();
public onMouseClick = new Signal<{ x: number; y: number }>();
constructor(x: number, y: number, width = 0, height = 0) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
// Render the component to the terminal
abstract render(): void;
// Handle input events
// Key
abstract handleKeyInput(event: KeyEvent): void;
// Char
abstract handleCharInput(event: CharEvent): void;
// Get/set focus for the component
focus(): void {
this.focused = true;
this.onFocus.emit();
}
unfocus(): void {
this.focused = false;
this.onBlur.emit();
}
// Show/hide the component
show(): void {
this.visible = true;
}
hide(): void {
this.visible = false;
}
// Check if a point is inside the component
contains(pointX: number, pointY: number): boolean {
return (
pointX >= this.x &&
pointX < this.x + this.width &&
pointY >= this.y &&
pointY < this.y + this.height
);
}
// Getter methods
getX(): number {
return this.x;
}
getY(): number {
return this.y;
}
getWidth(): number {
return this.width;
}
getHeight(): number {
return this.height;
}
isVisible(): boolean {
return this.visible;
}
isFocused(): boolean {
return this.focused;
}
}
/**
* Text output component
*/
class TextLabel extends UIComponent {
private text: string;
private textColor: number;
private bgColor: number;
constructor(
x: number,
y: number,
text: string,
textColor: number = colors.white,
bgColor: number = colors.black,
) {
super(x, y, text.length, 1);
this.text = text;
this.textColor = textColor;
this.bgColor = bgColor;
}
render(): void {
if (!this.visible) return;
const [originalX, originalY] = term.getCursorPos();
// Set colors
term.setTextColor(this.textColor);
term.setBackgroundColor(this.bgColor);
// Move cursor to position and draw text
term.setCursorPos(this.x, this.y);
term.write(this.text);
// Restore original cursor position
term.setCursorPos(originalX, originalY);
}
handleKeyInput(_event: KeyEvent): void {
// Do nothing
}
handleCharInput(_event: CharEvent): void {
// Do nothing
}
setText(newText: string): void {
this.text = newText;
this.width = newText.length; // Update width based on new text
}
getText(): string {
return this.text;
}
}
/**
* Input field component
*/
class InputField extends UIComponent {
private value: string;
private placeholder: string;
private maxLength: number;
private password: boolean;
private cursorPos = 0;
private isCursorBlink = false;
// Signal for when text changes
public onTextChanged = new Signal<string>();
constructor(
x: number,
y: number,
width: number,
value: "",
placeholder = "",
maxLength = 50,
password = false,
) {
super(x, y, width, 1);
this.value = value;
this.placeholder = placeholder;
this.maxLength = maxLength;
this.password = password;
this.cursorPos = value.length;
}
render(): void {
if (!this.visible) return;
const [originalX, originalY] = term.getCursorPos();
// Set colors (different for focused vs unfocused)
if (this.focused) {
term.setTextColor(colors.black);
term.setBackgroundColor(colors.white);
} else {
term.setTextColor(colors.white);
term.setBackgroundColor(colors.black);
}
// Move cursor to position
term.setCursorPos(this.x, this.y);
// Prepare text to display (mask if password)
let displayText = this.value;
if (this.password) {
displayText = "*".repeat(this.value.length);
} else if (this.value === "" && this.placeholder !== "") {
displayText = this.placeholder;
}
// Truncate or pad text to fit the field
if (displayText.length > this.width) {
displayText = displayText.substring(0, this.width);
} else {
displayText = displayText.padEnd(this.width);
}
// Draw the field
term.write(displayText);
// Move cursor to the correct position if focused
// if (this.focused) {
// const cursorX = Math.min(
// this.x + this.cursorPos,
// this.x + this.width - 1,
// );
// term.setCursorPos(cursorX, this.y);
// term.setCursorBlink(true);
// } else {
// term.setCursorBlink(false);
// }
// Restore original cursor position
term.setCursorPos(originalX, originalY);
}
handleKeyInput(event: KeyEvent): void {
if (!this.focused) return;
this.onKeyPress.emit(event);
const key = event.key;
log.debug(`[${InputField.name}]: Get key ${keys.getName(key)}`);
// Handle backspace
if (key === keys.backspace) {
if (this.cursorPos > 0) {
this.value =
this.value.substring(0, this.cursorPos - 1) +
this.value.substring(this.cursorPos);
this.cursorPos--;
this.onTextChanged.emit(this.value);
}
}
// Handle delete
else if (key === keys.delete) {
if (this.cursorPos < this.value.length) {
this.value =
this.value.substring(0, this.cursorPos) +
this.value.substring(this.cursorPos + 1);
this.onTextChanged.emit(this.value);
}
}
// Handle left arrow
else if (key === keys.left) {
if (this.cursorPos > 0) {
this.cursorPos--;
}
}
// Handle right arrow
else if (key === keys.right) {
if (this.cursorPos < this.value.length) {
this.cursorPos++;
}
}
// Handle enter (could be used to submit form)
else if (key === keys.enter) {
// Could emit a submit signal here
}
// Handle regular characters
else {
// For printable characters, we need to check if they are actual characters
// Since the event system is complex, we'll implement a more direct approach
}
}
handleCharInput(event: CharEvent): void {
if (!this.focused) return;
const character = event.character;
log.debug(`[${InputField.name}]: Get character ${character}`);
this.value += character;
this.cursorPos++;
this.onTextChanged.emit(this.value);
}
// Method to get user input directly (more suitable for ComputerCraft)
readInput(prompt = "", defaultValue = ""): string {
// Since we can't await for events in a standard way in this context,
// we'll use CC's read function which handles input internally
const oldX = this.x;
const oldY = this.y;
// Move cursor to the input field position
term.setCursorPos(oldX, oldY);
// Print the prompt
if (prompt != undefined) {
print(prompt);
}
// Use ComputerCraft's read function for actual input
const result = read(undefined, undefined, undefined, defaultValue);
return result;
}
getValue(): string {
return this.value;
}
setValue(value: string): void {
this.value = value.substring(0, this.maxLength);
this.cursorPos = value.length;
this.onTextChanged.emit(this.value);
}
}
/**
* Option selection component with prompt
*/
class OptionSelector extends UIComponent {
private options: string[];
private currentIndex: number;
private prompt: string;
private displayOption: string;
// Signal for when selection changes
public onSelectionChanged = new Signal<{ index: number; value: string }>();
constructor(
x: number,
y: number,
options: string[],
prompt = "Select:",
initialIndex = 0,
) {
super(x, y, 0, 1); // Width will be calculated dynamically
this.options = options;
this.currentIndex = initialIndex;
this.prompt = prompt;
// Calculate width based on prompt and longest option
const promptWidth = prompt.length + 1; // +1 for space
let maxOptionWidth = 0;
for (const option of options) {
if (option.length > maxOptionWidth) {
maxOptionWidth = option.length;
}
}
this.width = promptWidth + maxOptionWidth + 3; // +3 for brackets and space [ ]
this.displayOption = `[${this.options[this.currentIndex]}]`;
}
render(): void {
if (!this.visible) return;
const [originalX, originalY] = term.getCursorPos();
// Set colors
term.setTextColor(this.focused ? colors.yellow : colors.white);
term.setBackgroundColor(colors.black);
// Move cursor to position
term.setCursorPos(this.x, this.y);
// Draw prompt and selected option
const fullText = `${this.prompt} ${this.displayOption}`;
term.write(fullText.padEnd(this.width));
// Restore original cursor position
term.setCursorPos(originalX, originalY);
}
handleKeyInput(event: KeyEvent): void {
if (!this.focused) return;
this.onKeyPress.emit(event);
const key = event.key;
// Handle left arrow to go to previous option
if (key === keys.left) {
this.previousOption();
}
// Handle right arrow to go to next option
else if (key === keys.right) {
this.nextOption();
}
// Handle up arrow to go to previous option
else if (key === keys.up) {
this.previousOption();
}
// Handle down arrow to go to next option
else if (key === keys.down) {
this.nextOption();
}
}
handleCharInput(_event: CharEvent): void {
//Do nothing
}
private previousOption(): void {
this.currentIndex =
(this.currentIndex - 1 + this.options.length) % this.options.length;
this.updateDisplay();
this.onSelectionChanged.emit({
index: this.currentIndex,
value: this.options[this.currentIndex],
});
}
private nextOption(): void {
this.currentIndex = (this.currentIndex + 1) % this.options.length;
this.updateDisplay();
this.onSelectionChanged.emit({
index: this.currentIndex,
value: this.options[this.currentIndex],
});
}
private updateDisplay(): void {
this.displayOption = `[${this.options[this.currentIndex]}]`;
}
getSelectedIndex(): number {
return this.currentIndex;
}
getSelectedValue(): string {
return this.options[this.currentIndex];
}
setSelectedIndex(index: number): void {
if (index >= 0 && index < this.options.length) {
this.currentIndex = index;
this.updateDisplay();
this.onSelectionChanged.emit({
index: this.currentIndex,
value: this.options[this.currentIndex],
});
}
}
setOptions(newOptions: string[]): void {
this.options = newOptions;
if (this.currentIndex >= newOptions.length) {
this.currentIndex = 0;
}
this.updateDisplay();
}
}
/**
* Base Window class to manage UI components
*/
class UIWindow {
private components: UIComponent[] = [];
private focusedComponentIndex = -1;
addComponent(component: UIComponent, manager: GlobalManager): void {
this.components.push(component);
}
removeComponent(component: UIComponent): void {
const index = this.components.indexOf(component);
if (index !== -1) {
this.components.splice(index, 1);
}
}
render(): void {
// Clear the terminal
term.clear();
term.setCursorPos(1, 1);
// Render all visible components
for (const component of this.components) {
if (component.isVisible()) {
component.render();
}
}
}
handleKeyInput(event: KeyEvent): void {
// Handle input for the currently focused component
if (
this.focusedComponentIndex >= 0 &&
this.focusedComponentIndex < this.components.length
) {
const focusedComponent = this.components[this.focusedComponentIndex];
if (focusedComponent.isFocused()) {
focusedComponent.handleKeyInput(event);
}
}
}
handleCharInput(event: CharEvent): void {
// Handle input for the currently focused component
if (
this.focusedComponentIndex >= 0 &&
this.focusedComponentIndex < this.components.length
) {
const focusedComponent = this.components[this.focusedComponentIndex];
if (focusedComponent.isFocused()) {
focusedComponent.handleCharInput(event);
}
}
}
setFocus(index: number): void {
// Unfocus current component
if (
this.focusedComponentIndex >= 0 &&
this.focusedComponentIndex < this.components.length
) {
this.components[this.focusedComponentIndex].unfocus();
}
// Change focus
this.focusedComponentIndex = index;
// Focus new component
if (
this.focusedComponentIndex >= 0 &&
this.focusedComponentIndex < this.components.length
) {
this.components[this.focusedComponentIndex].focus();
}
}
setFocusFor(component: UIComponent): void {
const index = this.components.indexOf(component);
if (index !== -1) {
this.setFocus(index);
}
}
getComponent(index: number): UIComponent | undefined {
return this.components[index];
}
getComponents(): UIComponent[] {
return this.components;
}
clear(): void {
term.clear();
term.setCursorPos(1, 1);
}
}
/**
* Main TUI Application class
*/
class TUIApplication {
private log = new CCLog(`TUI.log`, false, DAY);
private manager: GlobalManager;
private window: UIWindow;
private running = false;
private keyEvent?: KeyEvent;
private charEvent?: CharEvent;
constructor() {
this.window = new UIWindow();
this.manager = {
log: this.log,
};
}
addComponent(component: UIComponent): void {
this.window.addComponent(component, this.manager);
}
run(): void {
this.running = true;
// Initial render
term.setCursorBlink(false);
this.window.render();
parallel.waitForAll(
() => {
this.mainLoop();
},
() => {
this.keyLoop();
},
() => {
this.charLoop();
},
);
}
stop(): void {
this.running = false;
this.manager.log.close();
}
mainLoop(): void {
// Main event loop
while (this.running) {
// Render the UI
this.window.render();
// Small delay to prevent excessive CPU usage
os.sleep(0.05);
}
}
keyLoop(): void {
while (this.running) {
// Handle input events
this.keyEvent = pullEventAs(KeyEvent, "key");
this.manager.log.debug(
`[${TUIApplication.name}]: Get Key Event: ${textutils.serialise(this.keyEvent ?? {})}`,
);
if (this.keyEvent == undefined) continue;
this.window.handleKeyInput(this.keyEvent);
}
}
charLoop(): void {
while (this.running) {
// Handle input events
this.charEvent = pullEventAs(CharEvent, "char");
this.manager.log.debug(
`[${TUIApplication.name}]: Get Char Event: ${textutils.serialise(this.charEvent ?? {})}`,
);
if (this.charEvent == undefined) continue;
this.window.handleCharInput(this.charEvent);
}
}
getWindow(): UIWindow {
return this.window;
}
}
// Export the main classes for use in other modules
export {
Signal,
UIComponent,
TextLabel,
InputField,
OptionSelector,
UIWindow,
TUIApplication,
};