mirror of
				https://github.com/SikongJueluo/cc-utils.git
				synced 2025-11-04 19:27:50 +08:00 
			
		
		
		
	reconstruct tui framework
This commit is contained in:
		@@ -1,100 +0,0 @@
 | 
			
		||||
import { UIComponent } from "./UIComponent";
 | 
			
		||||
import { Signal } from "./Signal";
 | 
			
		||||
import { KeyEvent } from "../event";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Button component
 | 
			
		||||
 */
 | 
			
		||||
export class Button extends UIComponent {
 | 
			
		||||
  private text: string;
 | 
			
		||||
  private textColor: number;
 | 
			
		||||
  private bgColor: number;
 | 
			
		||||
  private pressed = false;
 | 
			
		||||
 | 
			
		||||
  // Signal for when button is clicked
 | 
			
		||||
  public onClick = new Signal<void>();
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    objectName: string,
 | 
			
		||||
    x: number,
 | 
			
		||||
    y: number,
 | 
			
		||||
    text: string,
 | 
			
		||||
    textColor: number = colors.white,
 | 
			
		||||
    bgColor: number = colors.black,
 | 
			
		||||
  ) {
 | 
			
		||||
    // Width is based on text length plus 2 for the brackets ( [text] )
 | 
			
		||||
    super(objectName, x, y, text.length + 2, 1);
 | 
			
		||||
    this.text = text;
 | 
			
		||||
    this.textColor = textColor;
 | 
			
		||||
    this.bgColor = bgColor;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render(): void {
 | 
			
		||||
    if (!this.visible) return;
 | 
			
		||||
 | 
			
		||||
    const [originalX, originalY] = term.getCursorPos();
 | 
			
		||||
 | 
			
		||||
    // Set colors based on state (normal, focused, or pressed)
 | 
			
		||||
    let textColor = this.textColor;
 | 
			
		||||
    let backgroundColor = this.bgColor;
 | 
			
		||||
 | 
			
		||||
    if (this.pressed) {
 | 
			
		||||
      textColor = this.bgColor; // Swap text and background colors when pressed
 | 
			
		||||
      backgroundColor = this.textColor;
 | 
			
		||||
    } else if (this.focused) {
 | 
			
		||||
      textColor = colors.yellow; // Yellow text when focused
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    term.setTextColor(textColor);
 | 
			
		||||
    term.setBackgroundColor(backgroundColor);
 | 
			
		||||
 | 
			
		||||
    // Move cursor to position and draw button
 | 
			
		||||
    term.setCursorPos(this.x, this.y);
 | 
			
		||||
    term.write(`[${this.text}]`);
 | 
			
		||||
 | 
			
		||||
    // 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 button activation with space or enter
 | 
			
		||||
    if (key === keys.space || key === keys.enter) {
 | 
			
		||||
      this.pressButton();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleMouseClick(_event: { x: number; y: number }): void {
 | 
			
		||||
    // Button was clicked, trigger press
 | 
			
		||||
    this.pressButton();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private pressButton(): void {
 | 
			
		||||
    // Set pressed state and render
 | 
			
		||||
    this.pressed = true;
 | 
			
		||||
    this.render();
 | 
			
		||||
 | 
			
		||||
    // Trigger click signal
 | 
			
		||||
    this.onClick.emit();
 | 
			
		||||
 | 
			
		||||
    // Reset pressed state after a short delay to show visual feedback
 | 
			
		||||
    this.startTimer(() => {
 | 
			
		||||
      this.pressed = false;
 | 
			
		||||
      this.render();
 | 
			
		||||
    }, 100);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setText(newText: string): void {
 | 
			
		||||
    this.text = newText;
 | 
			
		||||
    this.width = newText.length + 2; // Update width based on new text
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getText(): string {
 | 
			
		||||
    return this.text;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,166 +0,0 @@
 | 
			
		||||
import { UIComponent } from "./UIComponent";
 | 
			
		||||
import { Signal } from "./Signal";
 | 
			
		||||
import { KeyEvent, CharEvent } from "../event";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Input field component
 | 
			
		||||
 */
 | 
			
		||||
export 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(
 | 
			
		||||
    objectName: string,
 | 
			
		||||
    x: number,
 | 
			
		||||
    y: number,
 | 
			
		||||
    width: number,
 | 
			
		||||
    value: "",
 | 
			
		||||
    placeholder = "",
 | 
			
		||||
    maxLength = 50,
 | 
			
		||||
    password = false,
 | 
			
		||||
  ) {
 | 
			
		||||
    super(objectName, 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);
 | 
			
		||||
 | 
			
		||||
    // Restore original cursor position
 | 
			
		||||
    term.setCursorPos(originalX, originalY);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleKeyInput(event: KeyEvent): void {
 | 
			
		||||
    if (!this.focused) return;
 | 
			
		||||
 | 
			
		||||
    this.onKeyPress.emit(event);
 | 
			
		||||
 | 
			
		||||
    const key = event.key;
 | 
			
		||||
    this.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;
 | 
			
		||||
    this.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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,137 +0,0 @@
 | 
			
		||||
import { UIComponent } from "./UIComponent";
 | 
			
		||||
import { Signal } from "./Signal";
 | 
			
		||||
import { KeyEvent } from "../event";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Option selection component with prompt
 | 
			
		||||
 */
 | 
			
		||||
export 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(
 | 
			
		||||
    objectName: string,
 | 
			
		||||
    x: number,
 | 
			
		||||
    y: number,
 | 
			
		||||
    options: string[],
 | 
			
		||||
    prompt = "Select:",
 | 
			
		||||
    initialIndex = 0,
 | 
			
		||||
  ) {
 | 
			
		||||
    super(objectName, 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();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,28 +0,0 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Signal and Slot system similar to Qt
 | 
			
		||||
 * Allows components to communicate with each other
 | 
			
		||||
 */
 | 
			
		||||
export 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);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,98 +0,0 @@
 | 
			
		||||
import { CCLog, DAY } from "../ccLog";
 | 
			
		||||
import { UIWindow } from "./UIWindow";
 | 
			
		||||
import { UIComponent } from "./UIComponent";
 | 
			
		||||
import { KeyEvent, CharEvent, TimerEvent, pullEventAs } from "../event";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Main TUI Application class
 | 
			
		||||
 */
 | 
			
		||||
export class TUIApplication {
 | 
			
		||||
  private log = new CCLog(`TUI.log`, false, DAY);
 | 
			
		||||
 | 
			
		||||
  private window: UIWindow;
 | 
			
		||||
  private running = false;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.window = new UIWindow();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addComponent(component: UIComponent): void {
 | 
			
		||||
    this.window.addComponent(component);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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.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
 | 
			
		||||
      const keyEvent = pullEventAs(KeyEvent, "key");
 | 
			
		||||
      this.log.debug(
 | 
			
		||||
        `[${TUIApplication.name}]: Get Key Event: ${textutils.serialise(keyEvent ?? {})}`,
 | 
			
		||||
      );
 | 
			
		||||
      if (keyEvent == undefined) continue;
 | 
			
		||||
      this.window.handleKeyInput(keyEvent);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  charLoop(): void {
 | 
			
		||||
    while (this.running) {
 | 
			
		||||
      // Handle input events
 | 
			
		||||
      const charEvent = pullEventAs(CharEvent, "char");
 | 
			
		||||
      this.log.debug(
 | 
			
		||||
        `[${TUIApplication.name}]: Get Char Event: ${textutils.serialise(charEvent ?? {})}`,
 | 
			
		||||
      );
 | 
			
		||||
      if (charEvent == undefined) continue;
 | 
			
		||||
      this.window.handleCharInput(charEvent);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  timerLoop(): void {
 | 
			
		||||
    while (this.running) {
 | 
			
		||||
      // Handle events
 | 
			
		||||
      const timerEvent = pullEventAs(TimerEvent, "timer");
 | 
			
		||||
      this.log.debug(
 | 
			
		||||
        `[${TUIApplication.name}]: Get Timer Event: ${textutils.serialise(timerEvent ?? {})}`,
 | 
			
		||||
      );
 | 
			
		||||
      if (timerEvent == undefined) continue;
 | 
			
		||||
      this.window.handleTimerTrigger(timerEvent);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getWindow(): UIWindow {
 | 
			
		||||
    return this.window;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,427 +0,0 @@
 | 
			
		||||
import { UIComponent } from "./UIComponent";
 | 
			
		||||
import { Signal } from "./Signal";
 | 
			
		||||
import { KeyEvent } from "../event";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tab component that allows switching between different pages
 | 
			
		||||
 * Similar to QT's TabWidget, currently implementing horizontal tabs only
 | 
			
		||||
 */
 | 
			
		||||
export class TabBar extends UIComponent {
 | 
			
		||||
  // Tab data structure - simple array of tab names
 | 
			
		||||
  private tabs: string[];
 | 
			
		||||
  private currentIndex: number;
 | 
			
		||||
  // Tracks visible range of tabs to handle overflow scenarios
 | 
			
		||||
  private firstVisibleIndex: number;
 | 
			
		||||
  private lastVisibleIndex: number;
 | 
			
		||||
 | 
			
		||||
  // Signal emitted when the current tab changes
 | 
			
		||||
  public onTabChanged = new Signal<{ index: number; name: string }>();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a new TabWidget component
 | 
			
		||||
   * @param objectName Unique name for the component
 | 
			
		||||
   * @param x X position on the terminal
 | 
			
		||||
   * @param y Y position on the terminal
 | 
			
		||||
   * @param width Width of the tab widget
 | 
			
		||||
   * @param tabNames Initial list of tab names
 | 
			
		||||
   * @param initialIndex Index of the initially selected tab (default: 0)
 | 
			
		||||
   */
 | 
			
		||||
  constructor(
 | 
			
		||||
    objectName: string,
 | 
			
		||||
    x: number,
 | 
			
		||||
    y: number,
 | 
			
		||||
    width: number,
 | 
			
		||||
    tabNames: string[],
 | 
			
		||||
    initialIndex = 0,
 | 
			
		||||
  ) {
 | 
			
		||||
    super(objectName, x, y, width, 1);
 | 
			
		||||
 | 
			
		||||
    // Initialize tabs as simple string array
 | 
			
		||||
    this.tabs = [...tabNames];
 | 
			
		||||
    this.currentIndex = Math.max(
 | 
			
		||||
      0,
 | 
			
		||||
      Math.min(initialIndex, tabNames.length - 1),
 | 
			
		||||
    );
 | 
			
		||||
    this.firstVisibleIndex = 0;
 | 
			
		||||
    this.lastVisibleIndex = -1;
 | 
			
		||||
 | 
			
		||||
    // Calculate which tabs can be displayed based on available width
 | 
			
		||||
    this.updateVisibleRange();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Updates the range of visible tabs based on available width
 | 
			
		||||
   * This method ensures the current tab is always visible and calculates
 | 
			
		||||
   * which other tabs can fit in the available space
 | 
			
		||||
   */
 | 
			
		||||
  private updateVisibleRange(): void {
 | 
			
		||||
    // If no tabs exist, nothing to update
 | 
			
		||||
    if (this.tabs.length === 0) {
 | 
			
		||||
      this.firstVisibleIndex = 0;
 | 
			
		||||
      this.lastVisibleIndex = -1;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Calculate visible tabs range based on current position
 | 
			
		||||
    this.calculateVisibleTabs();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Calculates visible tabs based on current position and available width
 | 
			
		||||
   * Follows the new core rendering logic
 | 
			
		||||
   */
 | 
			
		||||
  private calculateVisibleTabs(): void {
 | 
			
		||||
    if (this.tabs.length === 0) {
 | 
			
		||||
      this.firstVisibleIndex = 0;
 | 
			
		||||
      this.lastVisibleIndex = -1;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Start with all tabs and build the complete string
 | 
			
		||||
    let fullString = "| ";
 | 
			
		||||
    for (let i = 0; i < this.tabs.length; i++) {
 | 
			
		||||
      if (i > 0) {
 | 
			
		||||
        fullString += " | ";
 | 
			
		||||
      }
 | 
			
		||||
      fullString += this.tabs[i];
 | 
			
		||||
    }
 | 
			
		||||
    fullString += " |";
 | 
			
		||||
 | 
			
		||||
    // If the full string fits, show all tabs
 | 
			
		||||
    if (fullString.length <= this.width) {
 | 
			
		||||
      this.firstVisibleIndex = 0;
 | 
			
		||||
      this.lastVisibleIndex = this.tabs.length - 1;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Find the range that can fit around the current tab
 | 
			
		||||
    this.firstVisibleIndex = this.currentIndex;
 | 
			
		||||
    this.lastVisibleIndex = this.currentIndex;
 | 
			
		||||
 | 
			
		||||
    // Try to expand left and right alternately
 | 
			
		||||
    while (
 | 
			
		||||
      this.firstVisibleIndex > 0 ||
 | 
			
		||||
      this.lastVisibleIndex < this.tabs.length - 1
 | 
			
		||||
    ) {
 | 
			
		||||
      let expanded = false;
 | 
			
		||||
 | 
			
		||||
      // Try expanding left first
 | 
			
		||||
      if (this.firstVisibleIndex > 0) {
 | 
			
		||||
        const newTestString =
 | 
			
		||||
          "| " +
 | 
			
		||||
          this.tabs[this.firstVisibleIndex - 1] +
 | 
			
		||||
          " | " +
 | 
			
		||||
          this.tabs
 | 
			
		||||
            .slice(this.firstVisibleIndex, this.lastVisibleIndex + 1)
 | 
			
		||||
            .join(" | ") +
 | 
			
		||||
          " |";
 | 
			
		||||
 | 
			
		||||
        if (newTestString.length <= this.width) {
 | 
			
		||||
          this.firstVisibleIndex--;
 | 
			
		||||
          expanded = true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Try expanding right
 | 
			
		||||
      if (this.lastVisibleIndex < this.tabs.length - 1) {
 | 
			
		||||
        const newTestString =
 | 
			
		||||
          "| " +
 | 
			
		||||
          this.tabs
 | 
			
		||||
            .slice(this.firstVisibleIndex, this.lastVisibleIndex + 1)
 | 
			
		||||
            .join(" | ") +
 | 
			
		||||
          " | " +
 | 
			
		||||
          this.tabs[this.lastVisibleIndex + 1] +
 | 
			
		||||
          " |";
 | 
			
		||||
 | 
			
		||||
        if (newTestString.length <= this.width) {
 | 
			
		||||
          this.lastVisibleIndex++;
 | 
			
		||||
          expanded = true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // If no expansion was possible, break
 | 
			
		||||
      if (!expanded) break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Renders the tab widget to the terminal
 | 
			
		||||
   * Follows the new core rendering logic:
 | 
			
		||||
   * 1. Build complete string with all visible tabs
 | 
			
		||||
   * 2. Calculate what can be displayed
 | 
			
		||||
   * 3. Replace indicators based on hidden tabs
 | 
			
		||||
   * 4. Determine highlight range and render
 | 
			
		||||
   */
 | 
			
		||||
  render(): void {
 | 
			
		||||
    if (!this.visible) return;
 | 
			
		||||
 | 
			
		||||
    const [originalX, originalY] = term.getCursorPos();
 | 
			
		||||
 | 
			
		||||
    // Move cursor to the position of the tab widget
 | 
			
		||||
    term.setCursorPos(this.x, this.y);
 | 
			
		||||
 | 
			
		||||
    if (this.tabs.length === 0) {
 | 
			
		||||
      // Fill with spaces if no tabs
 | 
			
		||||
      term.setTextColor(colors.white);
 | 
			
		||||
      term.setBackgroundColor(colors.black);
 | 
			
		||||
      term.write(" ".repeat(this.width));
 | 
			
		||||
      term.setCursorPos(originalX, originalY);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Step 1: Build complete string for visible tabs with "| " at start and " |" at end
 | 
			
		||||
    let displayString = "| ";
 | 
			
		||||
    for (let i = this.firstVisibleIndex; i <= this.lastVisibleIndex; i++) {
 | 
			
		||||
      if (i > this.firstVisibleIndex) {
 | 
			
		||||
        displayString += " | ";
 | 
			
		||||
      }
 | 
			
		||||
      displayString += this.tabs[i];
 | 
			
		||||
    }
 | 
			
		||||
    displayString += " |";
 | 
			
		||||
 | 
			
		||||
    // Step 2: Check if the string fits, if not, truncate with "..."
 | 
			
		||||
    if (displayString.length > this.width) {
 | 
			
		||||
      // Need to truncate - find where to cut and add "..."
 | 
			
		||||
      const maxLength = this.width - 3; // Reserve space for "..."
 | 
			
		||||
      if (maxLength > 0) {
 | 
			
		||||
        // Find the last complete tab that can fit
 | 
			
		||||
        let cutPosition = maxLength;
 | 
			
		||||
        // Try to cut at a tab boundary if possible
 | 
			
		||||
        let lastPipePos = -1;
 | 
			
		||||
        for (let i = cutPosition; i >= 0; i--) {
 | 
			
		||||
          if (displayString.substring(i, i + 3) === " | ") {
 | 
			
		||||
            lastPipePos = i;
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        if (lastPipePos > 2) {
 | 
			
		||||
          // Make sure we don't cut before the first tab
 | 
			
		||||
          cutPosition = lastPipePos;
 | 
			
		||||
        }
 | 
			
		||||
        displayString = displayString.substring(0, cutPosition) + "...";
 | 
			
		||||
      } else {
 | 
			
		||||
        displayString = "...";
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Step 3: Replace boundary indicators based on hidden tabs
 | 
			
		||||
    if (this.firstVisibleIndex > 0) {
 | 
			
		||||
      // Left side has hidden tabs - replace "| " with "< "
 | 
			
		||||
      displayString = "< " + displayString.substring(2);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.lastVisibleIndex < this.tabs.length - 1) {
 | 
			
		||||
      // Right side has hidden tabs - replace " |" with " >"
 | 
			
		||||
      if (displayString.endsWith(" |")) {
 | 
			
		||||
        displayString =
 | 
			
		||||
          displayString.substring(0, displayString.length - 2) + " >";
 | 
			
		||||
      } else if (displayString.endsWith("...")) {
 | 
			
		||||
        // If we have "...", just ensure we show ">"
 | 
			
		||||
        displayString =
 | 
			
		||||
          displayString.substring(0, displayString.length - 3) + " >";
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Pad to maintain consistent width
 | 
			
		||||
    while (displayString.length < this.width) {
 | 
			
		||||
      displayString += " ";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Ensure we don't exceed the width
 | 
			
		||||
    if (displayString.length > this.width) {
 | 
			
		||||
      displayString = displayString.substring(0, this.width);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Step 4: Find current tab position for highlighting
 | 
			
		||||
    let currentTabStart = -1;
 | 
			
		||||
    let currentTabEnd = -1;
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      this.currentIndex >= this.firstVisibleIndex &&
 | 
			
		||||
      this.currentIndex <= this.lastVisibleIndex
 | 
			
		||||
    ) {
 | 
			
		||||
      // Calculate position of current tab in display string
 | 
			
		||||
      let searchPos = 2; // Start after "| " or "< "
 | 
			
		||||
 | 
			
		||||
      // Find current tab position by iterating through visible tabs
 | 
			
		||||
      for (let i = this.firstVisibleIndex; i <= this.lastVisibleIndex; i++) {
 | 
			
		||||
        if (i > this.firstVisibleIndex) {
 | 
			
		||||
          searchPos += 3; // " | " separator
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (i === this.currentIndex) {
 | 
			
		||||
          currentTabStart = searchPos;
 | 
			
		||||
 | 
			
		||||
          // Find the end of the current tab
 | 
			
		||||
          const tabName = this.tabs[i];
 | 
			
		||||
          const remainingString = displayString.substring(searchPos);
 | 
			
		||||
 | 
			
		||||
          // Check if the tab is fully displayed or truncated
 | 
			
		||||
          if (remainingString.startsWith(tabName)) {
 | 
			
		||||
            // Tab is fully displayed
 | 
			
		||||
            currentTabEnd = searchPos + tabName.length;
 | 
			
		||||
          } else {
 | 
			
		||||
            // Tab might be truncated, find where it ends
 | 
			
		||||
            const nextSeparatorPos = remainingString.indexOf(" |");
 | 
			
		||||
            const nextIndicatorPos = remainingString.indexOf(" >");
 | 
			
		||||
            const ellipsisPos = remainingString.indexOf("...");
 | 
			
		||||
 | 
			
		||||
            let endPos = remainingString.length;
 | 
			
		||||
            if (nextSeparatorPos >= 0)
 | 
			
		||||
              endPos = Math.min(endPos, nextSeparatorPos);
 | 
			
		||||
            if (nextIndicatorPos >= 0)
 | 
			
		||||
              endPos = Math.min(endPos, nextIndicatorPos);
 | 
			
		||||
            if (ellipsisPos >= 0) endPos = Math.min(endPos, ellipsisPos);
 | 
			
		||||
 | 
			
		||||
            currentTabEnd = searchPos + endPos;
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        searchPos += this.tabs[i].length;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Step 5: Render with highlighting
 | 
			
		||||
    term.setTextColor(colors.white);
 | 
			
		||||
    term.setBackgroundColor(colors.black);
 | 
			
		||||
 | 
			
		||||
    if (currentTabStart >= 0 && currentTabEnd > currentTabStart) {
 | 
			
		||||
      // Render text before current tab
 | 
			
		||||
      if (currentTabStart > 0) {
 | 
			
		||||
        term.write(displayString.substring(0, currentTabStart));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Render current tab with highlighting
 | 
			
		||||
      term.setTextColor(colors.yellow);
 | 
			
		||||
      term.setBackgroundColor(colors.gray);
 | 
			
		||||
      term.write(displayString.substring(currentTabStart, currentTabEnd));
 | 
			
		||||
 | 
			
		||||
      // Reset colors and render remaining text
 | 
			
		||||
      term.setTextColor(colors.white);
 | 
			
		||||
      term.setBackgroundColor(colors.black);
 | 
			
		||||
      term.write(displayString.substring(currentTabEnd));
 | 
			
		||||
    } else {
 | 
			
		||||
      // No highlighting needed, render entire string
 | 
			
		||||
      term.write(displayString);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Restore original cursor position
 | 
			
		||||
    term.setCursorPos(originalX, originalY);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Handles key input events for the tab widget
 | 
			
		||||
   * Supports left/right arrow keys to switch between tabs
 | 
			
		||||
   * @param event The key event to handle
 | 
			
		||||
   */
 | 
			
		||||
  handleKeyInput(event: KeyEvent): void {
 | 
			
		||||
    if (!this.focused) return;
 | 
			
		||||
 | 
			
		||||
    this.onKeyPress.emit(event);
 | 
			
		||||
 | 
			
		||||
    const key = event.key;
 | 
			
		||||
 | 
			
		||||
    // Handle left arrow to move to previous visible tab
 | 
			
		||||
    if (key === keys.left && this.canMoveToPreviousTab()) {
 | 
			
		||||
      this.moveToPreviousTab();
 | 
			
		||||
    }
 | 
			
		||||
    // Handle right arrow to move to next visible tab
 | 
			
		||||
    else if (key === keys.right && this.canMoveToNextTab()) {
 | 
			
		||||
      this.moveToNextTab();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Checks if there is a previous tab available to move to
 | 
			
		||||
   * @returns True if there's a previous tab, false otherwise
 | 
			
		||||
   */
 | 
			
		||||
  private canMoveToPreviousTab(): boolean {
 | 
			
		||||
    return this.currentIndex > 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Moves to the previous tab
 | 
			
		||||
   */
 | 
			
		||||
  private moveToPreviousTab(): void {
 | 
			
		||||
    if (this.currentIndex > 0) {
 | 
			
		||||
      this.currentIndex--;
 | 
			
		||||
      this.updateVisibleRange();
 | 
			
		||||
      this.onTabChanged.emit({
 | 
			
		||||
        index: this.currentIndex,
 | 
			
		||||
        name: this.tabs[this.currentIndex],
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Checks if there is a next tab available to move to
 | 
			
		||||
   * @returns True if there's a next tab, false otherwise
 | 
			
		||||
   */
 | 
			
		||||
  private canMoveToNextTab(): boolean {
 | 
			
		||||
    return this.currentIndex < this.tabs.length - 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Moves to the next tab
 | 
			
		||||
   */
 | 
			
		||||
  private moveToNextTab(): void {
 | 
			
		||||
    if (this.currentIndex < this.tabs.length - 1) {
 | 
			
		||||
      this.currentIndex++;
 | 
			
		||||
      this.updateVisibleRange();
 | 
			
		||||
      this.onTabChanged.emit({
 | 
			
		||||
        index: this.currentIndex,
 | 
			
		||||
        name: this.tabs[this.currentIndex],
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the index of the currently selected tab
 | 
			
		||||
   * @returns The index of the current tab
 | 
			
		||||
   */
 | 
			
		||||
  getCurrentTabIndex(): number {
 | 
			
		||||
    return this.currentIndex;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Gets the name of the currently selected tab
 | 
			
		||||
   * @returns The name of the current tab
 | 
			
		||||
   */
 | 
			
		||||
  getCurrentTabName(): string {
 | 
			
		||||
    if (this.currentIndex >= 0 && this.currentIndex < this.tabs.length) {
 | 
			
		||||
      return this.tabs[this.currentIndex];
 | 
			
		||||
    }
 | 
			
		||||
    return "";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Sets the currently selected tab by index
 | 
			
		||||
   * @param index The index of the tab to select
 | 
			
		||||
   */
 | 
			
		||||
  setCurrentTabIndex(index: number): void {
 | 
			
		||||
    if (index >= 0 && index < this.tabs.length) {
 | 
			
		||||
      this.currentIndex = index;
 | 
			
		||||
      this.updateVisibleRange();
 | 
			
		||||
      this.onTabChanged.emit({
 | 
			
		||||
        index: this.currentIndex,
 | 
			
		||||
        name: this.tabs[this.currentIndex],
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Updates the list of tabs with new tab names
 | 
			
		||||
   * @param tabNames The new list of tab names
 | 
			
		||||
   */
 | 
			
		||||
  setTabNames(tabNames: string[]): void {
 | 
			
		||||
    this.tabs = [...tabNames];
 | 
			
		||||
 | 
			
		||||
    // Ensure current index is within bounds
 | 
			
		||||
    if (this.currentIndex >= this.tabs.length) {
 | 
			
		||||
      this.currentIndex = Math.max(0, this.tabs.length - 1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.updateVisibleRange();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,49 +0,0 @@
 | 
			
		||||
import { UIComponent } from "./UIComponent";
 | 
			
		||||
/**
 | 
			
		||||
 * Text output component
 | 
			
		||||
 */
 | 
			
		||||
export class TextLabel extends UIComponent {
 | 
			
		||||
  private text: string;
 | 
			
		||||
  private textColor: number;
 | 
			
		||||
  private bgColor: number;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    objectName: string,
 | 
			
		||||
    x: number,
 | 
			
		||||
    y: number,
 | 
			
		||||
    text: string,
 | 
			
		||||
    textColor: number = colors.white,
 | 
			
		||||
    bgColor: number = colors.black,
 | 
			
		||||
  ) {
 | 
			
		||||
    super(objectName, 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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setText(newText: string): void {
 | 
			
		||||
    this.text = newText;
 | 
			
		||||
    this.width = newText.length; // Update width based on new text
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getText(): string {
 | 
			
		||||
    return this.text;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,104 +0,0 @@
 | 
			
		||||
import { Signal } from "./Signal";
 | 
			
		||||
import { KeyEvent, CharEvent, TimerEvent } from "../event";
 | 
			
		||||
import { UIObject } from "./UIObject";
 | 
			
		||||
/**
 | 
			
		||||
 * Base class for all UI components
 | 
			
		||||
 */
 | 
			
		||||
export abstract class UIComponent extends UIObject {
 | 
			
		||||
  protected x: number;
 | 
			
		||||
  protected y: number;
 | 
			
		||||
  protected width: number;
 | 
			
		||||
  protected height: number;
 | 
			
		||||
  protected visible = true;
 | 
			
		||||
  protected focused = false;
 | 
			
		||||
  protected timerTasks: Record<number, (event: TimerEvent) => void> = {};
 | 
			
		||||
 | 
			
		||||
  // 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(objectName: string, x: number, y: number, width = 0, height = 0) {
 | 
			
		||||
    super(objectName);
 | 
			
		||||
    this.x = x;
 | 
			
		||||
    this.y = y;
 | 
			
		||||
    this.width = width;
 | 
			
		||||
    this.height = height;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Render the component to the terminal
 | 
			
		||||
  abstract render(): void;
 | 
			
		||||
 | 
			
		||||
  // Handle events
 | 
			
		||||
  // Key
 | 
			
		||||
  handleKeyInput(_event: KeyEvent): void {
 | 
			
		||||
    // Do nothing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Char
 | 
			
		||||
  handleCharInput(_event: CharEvent): void {
 | 
			
		||||
    // Do nothing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleTimerTrigger(event: TimerEvent): void {
 | 
			
		||||
    this.timerTasks[event.id]?.(event);
 | 
			
		||||
    delete this.timerTasks[event.id];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 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
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Start a timer for delay task
 | 
			
		||||
  startTimer(task: (event: TimerEvent) => void, miliseconds: number): void {
 | 
			
		||||
    const id = os.startTimer(miliseconds);
 | 
			
		||||
    this.timerTasks[id] = task;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,32 +1,236 @@
 | 
			
		||||
import { CCLog } from "../ccLog";
 | 
			
		||||
/**
 | 
			
		||||
 * New UIObject system for functional component-based UI
 | 
			
		||||
 * Represents a node in the UI tree
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export abstract class UIObject {
 | 
			
		||||
  readonly objectName: string;
 | 
			
		||||
  private parent?: UIObject;
 | 
			
		||||
  private children: Record<string, UIObject> = {};
 | 
			
		||||
import { Accessor } from "./reactivity";
 | 
			
		||||
 | 
			
		||||
  public log?: CCLog;
 | 
			
		||||
/**
 | 
			
		||||
 * Layout properties for flexbox layout
 | 
			
		||||
 */
 | 
			
		||||
export interface LayoutProps {
 | 
			
		||||
  /** Flexbox direction */
 | 
			
		||||
  flexDirection?: "row" | "column";
 | 
			
		||||
  /** Justify content (main axis alignment) */
 | 
			
		||||
  justifyContent?: "start" | "center" | "end" | "between";
 | 
			
		||||
  /** Align items (cross axis alignment) */
 | 
			
		||||
  alignItems?: "start" | "center" | "end";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  constructor(name: string, parent?: UIObject, log?: CCLog) {
 | 
			
		||||
    this.objectName = name;
 | 
			
		||||
    this.parent = parent;
 | 
			
		||||
    this.log = log;
 | 
			
		||||
/**
 | 
			
		||||
 * Computed layout result after flexbox calculation
 | 
			
		||||
 */
 | 
			
		||||
export interface ComputedLayout {
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  width: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base props that all components can accept
 | 
			
		||||
 */
 | 
			
		||||
export interface BaseProps {
 | 
			
		||||
  /** CSS-like class names for layout (e.g., "flex flex-col") */
 | 
			
		||||
  class?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * UIObject node type
 | 
			
		||||
 */
 | 
			
		||||
export type UIObjectType = 
 | 
			
		||||
  | "div" 
 | 
			
		||||
  | "label" 
 | 
			
		||||
  | "button" 
 | 
			
		||||
  | "input" 
 | 
			
		||||
  | "form"
 | 
			
		||||
  | "h1"
 | 
			
		||||
  | "h2"
 | 
			
		||||
  | "h3"
 | 
			
		||||
  | "for"
 | 
			
		||||
  | "show"
 | 
			
		||||
  | "fragment";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * UIObject represents a node in the UI tree
 | 
			
		||||
 * It can be a component, text, or a control flow element
 | 
			
		||||
 */
 | 
			
		||||
export class UIObject {
 | 
			
		||||
  /** Type of the UI object */
 | 
			
		||||
  type: UIObjectType;
 | 
			
		||||
  
 | 
			
		||||
  /** Props passed to the component */
 | 
			
		||||
  props: Record<string, unknown>;
 | 
			
		||||
  
 | 
			
		||||
  /** Children UI objects */
 | 
			
		||||
  children: UIObject[];
 | 
			
		||||
  
 | 
			
		||||
  /** Parent UI object */
 | 
			
		||||
  parent?: UIObject;
 | 
			
		||||
  
 | 
			
		||||
  /** Computed layout after flexbox calculation */
 | 
			
		||||
  layout?: ComputedLayout;
 | 
			
		||||
  
 | 
			
		||||
  /** Layout properties parsed from class string */
 | 
			
		||||
  layoutProps: LayoutProps;
 | 
			
		||||
  
 | 
			
		||||
  /** Whether this component is currently mounted */
 | 
			
		||||
  mounted: boolean;
 | 
			
		||||
  
 | 
			
		||||
  /** Cleanup functions to call when unmounting */
 | 
			
		||||
  cleanupFns: (() => void)[];
 | 
			
		||||
  
 | 
			
		||||
  /** For text nodes - the text content (can be reactive) */
 | 
			
		||||
  textContent?: string | Accessor<string>;
 | 
			
		||||
  
 | 
			
		||||
  /** Event handlers */
 | 
			
		||||
  handlers: Record<string, ((...args: unknown[]) => void) | undefined>;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    type: UIObjectType,
 | 
			
		||||
    props: Record<string, unknown> = {},
 | 
			
		||||
    children: UIObject[] = []
 | 
			
		||||
  ) {
 | 
			
		||||
    this.type = type;
 | 
			
		||||
    this.props = props;
 | 
			
		||||
    this.children = children;
 | 
			
		||||
    this.layoutProps = {};
 | 
			
		||||
    this.mounted = false;
 | 
			
		||||
    this.cleanupFns = [];
 | 
			
		||||
    this.handlers = {};
 | 
			
		||||
    
 | 
			
		||||
    // Parse layout from class prop
 | 
			
		||||
    this.parseLayout();
 | 
			
		||||
    
 | 
			
		||||
    // Extract event handlers
 | 
			
		||||
    this.extractHandlers();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setParent(parent: UIObject) {
 | 
			
		||||
    this.parent = parent;
 | 
			
		||||
    this.log ??= parent.log;
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * Parse CSS-like class string into layout properties
 | 
			
		||||
   */
 | 
			
		||||
  private parseLayout(): void {
 | 
			
		||||
    const className = this.props.class as string | undefined;
 | 
			
		||||
    if (className === undefined) return;
 | 
			
		||||
 | 
			
		||||
  public addChild(child: UIObject) {
 | 
			
		||||
    this.children[child.objectName] = child;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public removeChild(child: UIObject) {
 | 
			
		||||
    Object.entries(this.children).forEach(([key, value]) => {
 | 
			
		||||
      if (value === child) {
 | 
			
		||||
        delete this.children[key];
 | 
			
		||||
    const classes = className.split(" ").filter(c => c.length > 0);
 | 
			
		||||
    
 | 
			
		||||
    for (const cls of classes) {
 | 
			
		||||
      // Flex direction
 | 
			
		||||
      if (cls === "flex-row") {
 | 
			
		||||
        this.layoutProps.flexDirection = "row";
 | 
			
		||||
      } else if (cls === "flex-col") {
 | 
			
		||||
        this.layoutProps.flexDirection = "column";
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
      
 | 
			
		||||
      // Justify content
 | 
			
		||||
      else if (cls === "justify-start") {
 | 
			
		||||
        this.layoutProps.justifyContent = "start";
 | 
			
		||||
      } else if (cls === "justify-center") {
 | 
			
		||||
        this.layoutProps.justifyContent = "center";
 | 
			
		||||
      } else if (cls === "justify-end") {
 | 
			
		||||
        this.layoutProps.justifyContent = "end";
 | 
			
		||||
      } else if (cls === "justify-between") {
 | 
			
		||||
        this.layoutProps.justifyContent = "between";
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Align items
 | 
			
		||||
      else if (cls === "items-start") {
 | 
			
		||||
        this.layoutProps.alignItems = "start";
 | 
			
		||||
      } else if (cls === "items-center") {
 | 
			
		||||
        this.layoutProps.alignItems = "center";
 | 
			
		||||
      } else if (cls === "items-end") {
 | 
			
		||||
        this.layoutProps.alignItems = "end";
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Set defaults
 | 
			
		||||
    if (this.type === "div") {
 | 
			
		||||
      this.layoutProps.flexDirection ??= "row";
 | 
			
		||||
    }
 | 
			
		||||
    this.layoutProps.justifyContent ??= "start";
 | 
			
		||||
    this.layoutProps.alignItems ??= "start";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Extract event handlers from props
 | 
			
		||||
   */
 | 
			
		||||
  private extractHandlers(): void {
 | 
			
		||||
    for (const [key, value] of pairs(this.props)) {
 | 
			
		||||
      if (typeof key === "string" && key.startsWith("on") && typeof value === "function") {
 | 
			
		||||
        this.handlers[key] = value as (...args: unknown[]) => void;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Add a child to this UI object
 | 
			
		||||
   */
 | 
			
		||||
  appendChild(child: UIObject): void {
 | 
			
		||||
    child.parent = this;
 | 
			
		||||
    this.children.push(child);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Remove a child from this UI object
 | 
			
		||||
   */
 | 
			
		||||
  removeChild(child: UIObject): void {
 | 
			
		||||
    const index = this.children.indexOf(child);
 | 
			
		||||
    if (index !== -1) {
 | 
			
		||||
      this.children.splice(index, 1);
 | 
			
		||||
      child.parent = undefined;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Mount this component and all children
 | 
			
		||||
   */
 | 
			
		||||
  mount(): void {
 | 
			
		||||
    if (this.mounted) return;
 | 
			
		||||
    this.mounted = true;
 | 
			
		||||
    
 | 
			
		||||
    // Mount all children
 | 
			
		||||
    for (const child of this.children) {
 | 
			
		||||
      child.mount();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Unmount this component and run cleanup
 | 
			
		||||
   */
 | 
			
		||||
  unmount(): void {
 | 
			
		||||
    if (!this.mounted) return;
 | 
			
		||||
    this.mounted = false;
 | 
			
		||||
    
 | 
			
		||||
    // Unmount all children first
 | 
			
		||||
    for (const child of this.children) {
 | 
			
		||||
      child.unmount();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Run cleanup functions
 | 
			
		||||
    for (const cleanup of this.cleanupFns) {
 | 
			
		||||
      try {
 | 
			
		||||
        cleanup();
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        printError(e);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.cleanupFns = [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Register a cleanup function to be called on unmount
 | 
			
		||||
   */
 | 
			
		||||
  onCleanup(fn: () => void): void {
 | 
			
		||||
    this.cleanupFns.push(fn);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Create a text node
 | 
			
		||||
 */
 | 
			
		||||
export function createTextNode(text: string | Accessor<string>): UIObject {
 | 
			
		||||
  const node = new UIObject("fragment", {}, []);
 | 
			
		||||
  node.textContent = text;
 | 
			
		||||
  return node;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,107 +0,0 @@
 | 
			
		||||
import { UIComponent } from "./UIComponent";
 | 
			
		||||
import { KeyEvent, CharEvent, TimerEvent } from "../event";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base Window class to manage UI components
 | 
			
		||||
 */
 | 
			
		||||
export class UIWindow {
 | 
			
		||||
  private components: UIComponent[] = [];
 | 
			
		||||
  private focusedComponentIndex = -1;
 | 
			
		||||
 | 
			
		||||
  addComponent(component: UIComponent): 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);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleTimerTrigger(event: TimerEvent) {
 | 
			
		||||
    for (const component of this.components) {
 | 
			
		||||
      component.handleTimerTrigger(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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										338
									
								
								src/lib/ccTUI/application.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										338
									
								
								src/lib/ccTUI/application.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,338 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Application class for managing the UI lifecycle
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { UIObject } from "./UIObject";
 | 
			
		||||
import { calculateLayout } from "./layout";
 | 
			
		||||
import { render as renderTree, clearScreen } from "./renderer";
 | 
			
		||||
import { createEffect } from "./reactivity";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Main application class
 | 
			
		||||
 * Manages the root UI component and handles rendering
 | 
			
		||||
 */
 | 
			
		||||
export class Application {
 | 
			
		||||
  private root?: UIObject;
 | 
			
		||||
  private running = false;
 | 
			
		||||
  private needsRender = true;
 | 
			
		||||
  private focusedNode?: UIObject;
 | 
			
		||||
  private termWidth: number;
 | 
			
		||||
  private termHeight: number;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    const [width, height] = term.getSize();
 | 
			
		||||
    this.termWidth = width;
 | 
			
		||||
    this.termHeight = height;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Set the root component for the application
 | 
			
		||||
   *
 | 
			
		||||
   * @param rootComponent - The root UI component
 | 
			
		||||
   */
 | 
			
		||||
  setRoot(rootComponent: UIObject): void {
 | 
			
		||||
    // Unmount old root if it exists
 | 
			
		||||
    if (this.root !== undefined) {
 | 
			
		||||
      this.root.unmount();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.root = rootComponent;
 | 
			
		||||
    this.root.mount();
 | 
			
		||||
    this.needsRender = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Request a re-render on the next frame
 | 
			
		||||
   */
 | 
			
		||||
  requestRender(): void {
 | 
			
		||||
    this.needsRender = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Run the application event loop
 | 
			
		||||
   */
 | 
			
		||||
  run(): void {
 | 
			
		||||
    if (this.root === undefined) {
 | 
			
		||||
      error(
 | 
			
		||||
        "Cannot run application without a root component. Call setRoot() first.",
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.running = true;
 | 
			
		||||
    term.setCursorBlink(false);
 | 
			
		||||
    clearScreen();
 | 
			
		||||
 | 
			
		||||
    // Initial render
 | 
			
		||||
    this.renderFrame();
 | 
			
		||||
 | 
			
		||||
    // Main event loop
 | 
			
		||||
    parallel.waitForAll(
 | 
			
		||||
      () => this.renderLoop(),
 | 
			
		||||
      () => this.eventLoop(),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Stop the application
 | 
			
		||||
   */
 | 
			
		||||
  stop(): void {
 | 
			
		||||
    this.running = false;
 | 
			
		||||
 | 
			
		||||
    if (this.root !== undefined) {
 | 
			
		||||
      this.root.unmount();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    clearScreen();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Render loop - continuously renders when needed
 | 
			
		||||
   */
 | 
			
		||||
  private renderLoop(): void {
 | 
			
		||||
    // Set up reactive rendering - re-render whenever any signal changes
 | 
			
		||||
    createEffect(() => {
 | 
			
		||||
      this.renderFrame();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    while (this.running) {
 | 
			
		||||
      // Keep the loop alive
 | 
			
		||||
      os.sleep(0.05);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Render a single frame
 | 
			
		||||
   */
 | 
			
		||||
  private renderFrame(): void {
 | 
			
		||||
    if (this.root === undefined) return;
 | 
			
		||||
 | 
			
		||||
    // Calculate layout
 | 
			
		||||
    calculateLayout(this.root, this.termWidth, this.termHeight, 1, 1);
 | 
			
		||||
 | 
			
		||||
    // Clear screen
 | 
			
		||||
    clearScreen();
 | 
			
		||||
 | 
			
		||||
    // Render the tree
 | 
			
		||||
    renderTree(this.root, this.focusedNode);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Event loop - handles user input
 | 
			
		||||
   */
 | 
			
		||||
  private eventLoop(): void {
 | 
			
		||||
    while (this.running) {
 | 
			
		||||
      const [eventType, ...eventData] = os.pullEvent() as LuaMultiReturn<
 | 
			
		||||
        unknown[]
 | 
			
		||||
      >;
 | 
			
		||||
 | 
			
		||||
      if (eventType === "key") {
 | 
			
		||||
        this.handleKeyEvent(eventData[0] as number);
 | 
			
		||||
      } else if (eventType === "char") {
 | 
			
		||||
        this.handleCharEvent(eventData[0] as string);
 | 
			
		||||
      } else if (eventType === "mouse_click") {
 | 
			
		||||
        this.handleMouseClick(
 | 
			
		||||
          eventData[0] as number,
 | 
			
		||||
          eventData[1] as number,
 | 
			
		||||
          eventData[2] as number,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Handle keyboard key events
 | 
			
		||||
   */
 | 
			
		||||
  private handleKeyEvent(key: number): void {
 | 
			
		||||
    if (key === keys.tab) {
 | 
			
		||||
      // Focus next element
 | 
			
		||||
      this.focusNext();
 | 
			
		||||
      this.needsRender = true;
 | 
			
		||||
    } else if (key === keys.enter && this.focusedNode !== undefined) {
 | 
			
		||||
      // Trigger action on focused element
 | 
			
		||||
      if (this.focusedNode.type === "button") {
 | 
			
		||||
        const onClick = this.focusedNode.handlers.onClick;
 | 
			
		||||
        if (onClick) {
 | 
			
		||||
          (onClick as () => void)();
 | 
			
		||||
          this.needsRender = true;
 | 
			
		||||
        }
 | 
			
		||||
      } else if (this.focusedNode.type === "input") {
 | 
			
		||||
        const type = this.focusedNode.props.type as string | undefined;
 | 
			
		||||
        if (type === "checkbox") {
 | 
			
		||||
          // Toggle checkbox
 | 
			
		||||
          const onChangeProp = this.focusedNode.props.onChange;
 | 
			
		||||
          const checkedProp = this.focusedNode.props.checked;
 | 
			
		||||
 | 
			
		||||
          if (
 | 
			
		||||
            typeof onChangeProp === "function" &&
 | 
			
		||||
            typeof checkedProp === "function"
 | 
			
		||||
          ) {
 | 
			
		||||
            const currentValue = (checkedProp as () => boolean)();
 | 
			
		||||
            (onChangeProp as (v: boolean) => void)(!currentValue);
 | 
			
		||||
            this.needsRender = true;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Handle character input events
 | 
			
		||||
   */
 | 
			
		||||
  private handleCharEvent(char: string): void {
 | 
			
		||||
    if (this.focusedNode !== undefined && this.focusedNode.type === "input") {
 | 
			
		||||
      const type = this.focusedNode.props.type as string | undefined;
 | 
			
		||||
      if (type !== "checkbox") {
 | 
			
		||||
        // Add character to text input
 | 
			
		||||
        const onInputProp = this.focusedNode.props.onInput;
 | 
			
		||||
        const valueProp = this.focusedNode.props.value;
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
          typeof onInputProp === "function" &&
 | 
			
		||||
          typeof valueProp === "function"
 | 
			
		||||
        ) {
 | 
			
		||||
          const currentValue = (valueProp as () => string)();
 | 
			
		||||
          (onInputProp as (v: string) => void)(currentValue + char);
 | 
			
		||||
          this.needsRender = true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Handle mouse click events
 | 
			
		||||
   */
 | 
			
		||||
  private handleMouseClick(button: number, x: number, y: number): void {
 | 
			
		||||
    if (button !== 1 || this.root === undefined) return;
 | 
			
		||||
 | 
			
		||||
    // Find which element was clicked
 | 
			
		||||
    const clicked = this.findNodeAt(this.root, x, y);
 | 
			
		||||
 | 
			
		||||
    if (clicked !== undefined) {
 | 
			
		||||
      // Set focus
 | 
			
		||||
      this.focusedNode = clicked;
 | 
			
		||||
 | 
			
		||||
      // Trigger click handler
 | 
			
		||||
      if (clicked.type === "button") {
 | 
			
		||||
        const onClick = clicked.handlers.onClick;
 | 
			
		||||
        if (onClick) {
 | 
			
		||||
          (onClick as () => void)();
 | 
			
		||||
        }
 | 
			
		||||
      } else if (clicked.type === "input") {
 | 
			
		||||
        const type = clicked.props.type as string | undefined;
 | 
			
		||||
        if (type === "checkbox") {
 | 
			
		||||
          const onChangeProp = clicked.props.onChange;
 | 
			
		||||
          const checkedProp = clicked.props.checked;
 | 
			
		||||
 | 
			
		||||
          if (
 | 
			
		||||
            typeof onChangeProp === "function" &&
 | 
			
		||||
            typeof checkedProp === "function"
 | 
			
		||||
          ) {
 | 
			
		||||
            const currentValue = (checkedProp as () => boolean)();
 | 
			
		||||
            (onChangeProp as (v: boolean) => void)(!currentValue);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.needsRender = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Find the UI node at a specific screen position
 | 
			
		||||
   */
 | 
			
		||||
  private findNodeAt(
 | 
			
		||||
    node: UIObject,
 | 
			
		||||
    x: number,
 | 
			
		||||
    y: number,
 | 
			
		||||
  ): UIObject | undefined {
 | 
			
		||||
    // Check children first (depth-first)
 | 
			
		||||
    for (const child of node.children) {
 | 
			
		||||
      const found = this.findNodeAt(child, x, y);
 | 
			
		||||
      if (found !== undefined) {
 | 
			
		||||
        return found;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check this node
 | 
			
		||||
    if (node.layout !== undefined) {
 | 
			
		||||
      const { x: nx, y: ny, width, height } = node.layout;
 | 
			
		||||
      if (x >= nx && x < nx + width && y >= ny && y < ny + height) {
 | 
			
		||||
        // Only return interactive elements
 | 
			
		||||
        if (node.type === "button" || node.type === "input") {
 | 
			
		||||
          return node;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return undefined;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Focus the next interactive element
 | 
			
		||||
   */
 | 
			
		||||
  private focusNext(): void {
 | 
			
		||||
    if (this.root === undefined) return;
 | 
			
		||||
 | 
			
		||||
    const interactive = this.collectInteractive(this.root);
 | 
			
		||||
 | 
			
		||||
    if (interactive.length === 0) {
 | 
			
		||||
      this.focusedNode = undefined;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.focusedNode === undefined) {
 | 
			
		||||
      this.focusedNode = interactive[0];
 | 
			
		||||
    } else {
 | 
			
		||||
      const currentIndex = interactive.indexOf(this.focusedNode);
 | 
			
		||||
      const nextIndex = (currentIndex + 1) % interactive.length;
 | 
			
		||||
      this.focusedNode = interactive[nextIndex];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Collect all interactive elements in the tree
 | 
			
		||||
   */
 | 
			
		||||
  private collectInteractive(node: UIObject): UIObject[] {
 | 
			
		||||
    const result: UIObject[] = [];
 | 
			
		||||
 | 
			
		||||
    if (node.type === "button" || node.type === "input") {
 | 
			
		||||
      result.push(node);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const child of node.children) {
 | 
			
		||||
      result.push(...this.collectInteractive(child));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Convenience function to create and run an application
 | 
			
		||||
 *
 | 
			
		||||
 * @param rootFn - Function that returns the root component
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```typescript
 | 
			
		||||
 * render(() => {
 | 
			
		||||
 *   const [count, setCount] = createSignal(0);
 | 
			
		||||
 *   return div({ class: "flex flex-col" },
 | 
			
		||||
 *     label({}, () => `Count: ${count()}`),
 | 
			
		||||
 *     button({ onClick: () => setCount(count() + 1) }, "Increment")
 | 
			
		||||
 *   );
 | 
			
		||||
 * });
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function render(rootFn: () => UIObject): void {
 | 
			
		||||
  const app = new Application();
 | 
			
		||||
 | 
			
		||||
  // Create the root component
 | 
			
		||||
  const root = rootFn();
 | 
			
		||||
  app.setRoot(root);
 | 
			
		||||
 | 
			
		||||
  try {
 | 
			
		||||
    app.run();
 | 
			
		||||
  } finally {
 | 
			
		||||
    app.stop();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										219
									
								
								src/lib/ccTUI/components.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								src/lib/ccTUI/components.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,219 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Functional component API
 | 
			
		||||
 * Provides declarative UI building blocks
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { UIObject, BaseProps, createTextNode } from "./UIObject";
 | 
			
		||||
import { Accessor, Setter, Signal } from "./reactivity";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Props for div component
 | 
			
		||||
 */
 | 
			
		||||
export type DivProps = BaseProps & Record<string, unknown>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Props for label component
 | 
			
		||||
 */
 | 
			
		||||
export type LabelProps = BaseProps & Record<string, unknown>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Props for button component
 | 
			
		||||
 */
 | 
			
		||||
export type ButtonProps = BaseProps & {
 | 
			
		||||
  /** Click handler */
 | 
			
		||||
  onClick?: () => void;
 | 
			
		||||
} & Record<string, unknown>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Props for input component
 | 
			
		||||
 */
 | 
			
		||||
export type InputProps = BaseProps & {
 | 
			
		||||
  /** Input type */
 | 
			
		||||
  type?: "text" | "checkbox";
 | 
			
		||||
  /** Value signal for text input */
 | 
			
		||||
  value?: Accessor<string> | Signal<string>;
 | 
			
		||||
  /** Input handler for text input */
 | 
			
		||||
  onInput?: Setter<string> | ((value: string) => void);
 | 
			
		||||
  /** Checked signal for checkbox */
 | 
			
		||||
  checked?: Accessor<boolean> | Signal<boolean>;
 | 
			
		||||
  /** Change handler for checkbox */
 | 
			
		||||
  onChange?: Setter<boolean> | ((checked: boolean) => void);
 | 
			
		||||
  /** Placeholder text */
 | 
			
		||||
  placeholder?: string;
 | 
			
		||||
} & Record<string, unknown>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Props for form component
 | 
			
		||||
 */
 | 
			
		||||
export type FormProps = BaseProps & {
 | 
			
		||||
  /** Submit handler */
 | 
			
		||||
  onSubmit?: () => void;
 | 
			
		||||
} & Record<string, unknown>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Generic container component for layout
 | 
			
		||||
 *
 | 
			
		||||
 * @param props - Component props including layout classes
 | 
			
		||||
 * @param children - Child components or text
 | 
			
		||||
 * @returns UIObject representing the div
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```typescript
 | 
			
		||||
 * div({ class: "flex flex-col justify-center" },
 | 
			
		||||
 *   label({}, "Hello"),
 | 
			
		||||
 *   button({ onClick: () => print("clicked") }, "Click me")
 | 
			
		||||
 * )
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function div(
 | 
			
		||||
  props: DivProps,
 | 
			
		||||
  ...children: (UIObject | string | Accessor<string>)[]
 | 
			
		||||
): UIObject {
 | 
			
		||||
  // Convert string children to text nodes
 | 
			
		||||
  const uiChildren = children.map((child) => {
 | 
			
		||||
    if (typeof child === "string" || typeof child === "function") {
 | 
			
		||||
      return createTextNode(child);
 | 
			
		||||
    }
 | 
			
		||||
    return child;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const node = new UIObject("div", props, uiChildren);
 | 
			
		||||
  uiChildren.forEach((child) => (child.parent = node));
 | 
			
		||||
  return node;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Text label component
 | 
			
		||||
 *
 | 
			
		||||
 * @param props - Component props
 | 
			
		||||
 * @param text - Text content (can be a string or signal)
 | 
			
		||||
 * @returns UIObject representing the label
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```typescript
 | 
			
		||||
 * const [name, setName] = createSignal("World");
 | 
			
		||||
 * label({}, () => `Hello, ${name()}!`)
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function label(
 | 
			
		||||
  props: LabelProps,
 | 
			
		||||
  text: string | Accessor<string>,
 | 
			
		||||
): UIObject {
 | 
			
		||||
  const textNode = createTextNode(text);
 | 
			
		||||
  const node = new UIObject("label", props, [textNode]);
 | 
			
		||||
  textNode.parent = node;
 | 
			
		||||
  return node;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Heading level 1 component
 | 
			
		||||
 *
 | 
			
		||||
 * @param text - Heading text
 | 
			
		||||
 * @returns UIObject representing h1
 | 
			
		||||
 */
 | 
			
		||||
export function h1(text: string | Accessor<string>): UIObject {
 | 
			
		||||
  return label({ class: "heading-1" }, text);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Heading level 2 component
 | 
			
		||||
 *
 | 
			
		||||
 * @param text - Heading text
 | 
			
		||||
 * @returns UIObject representing h2
 | 
			
		||||
 */
 | 
			
		||||
export function h2(text: string | Accessor<string>): UIObject {
 | 
			
		||||
  return label({ class: "heading-2" }, text);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Heading level 3 component
 | 
			
		||||
 *
 | 
			
		||||
 * @param text - Heading text
 | 
			
		||||
 * @returns UIObject representing h3
 | 
			
		||||
 */
 | 
			
		||||
export function h3(text: string | Accessor<string>): UIObject {
 | 
			
		||||
  return label({ class: "heading-3" }, text);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Button component
 | 
			
		||||
 *
 | 
			
		||||
 * @param props - Component props including onClick handler
 | 
			
		||||
 * @param text - Button text
 | 
			
		||||
 * @returns UIObject representing the button
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```typescript
 | 
			
		||||
 * button({ onClick: () => print("Clicked!") }, "Click me")
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function button(props: ButtonProps, text: string): UIObject {
 | 
			
		||||
  const textNode = createTextNode(text);
 | 
			
		||||
  const node = new UIObject("button", props, [textNode]);
 | 
			
		||||
  textNode.parent = node;
 | 
			
		||||
  return node;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Input component (text input or checkbox)
 | 
			
		||||
 *
 | 
			
		||||
 * @param props - Component props
 | 
			
		||||
 * @returns UIObject representing the input
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```typescript
 | 
			
		||||
 * // Text input
 | 
			
		||||
 * const [text, setText] = createSignal("");
 | 
			
		||||
 * input({ type: "text", value: text, onInput: setText, placeholder: "Enter text" })
 | 
			
		||||
 *
 | 
			
		||||
 * // Checkbox
 | 
			
		||||
 * const [checked, setChecked] = createSignal(false);
 | 
			
		||||
 * input({ type: "checkbox", checked: checked, onChange: setChecked })
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function input(props: InputProps): UIObject {
 | 
			
		||||
  // Normalize signal tuples to just the accessor
 | 
			
		||||
  const normalizedProps = { ...props };
 | 
			
		||||
 | 
			
		||||
  if (Array.isArray(normalizedProps.value)) {
 | 
			
		||||
    normalizedProps.value = (normalizedProps.value)[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (Array.isArray(normalizedProps.checked)) {
 | 
			
		||||
    normalizedProps.checked = (normalizedProps.checked)[0];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return new UIObject("input", normalizedProps, []);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Form component for grouping inputs
 | 
			
		||||
 *
 | 
			
		||||
 * @param props - Component props including onSubmit handler
 | 
			
		||||
 * @param children - Child components
 | 
			
		||||
 * @returns UIObject representing the form
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```typescript
 | 
			
		||||
 * form({ onSubmit: () => print("Submitted!"), class: "flex flex-row" },
 | 
			
		||||
 *   input({ placeholder: "Enter text" }),
 | 
			
		||||
 *   button({}, "Submit")
 | 
			
		||||
 * )
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function form(
 | 
			
		||||
  props: FormProps,
 | 
			
		||||
  ...children: (UIObject | string | Accessor<string>)[]
 | 
			
		||||
): UIObject {
 | 
			
		||||
  // Convert string children to text nodes
 | 
			
		||||
  const uiChildren = children.map((child) => {
 | 
			
		||||
    if (typeof child === "string" || typeof child === "function") {
 | 
			
		||||
      return createTextNode(child);
 | 
			
		||||
    }
 | 
			
		||||
    return child;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const node = new UIObject("form", props, uiChildren);
 | 
			
		||||
  uiChildren.forEach((child) => (child.parent = node));
 | 
			
		||||
  return node;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										145
									
								
								src/lib/ccTUI/controlFlow.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/lib/ccTUI/controlFlow.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,145 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Control flow components for conditional and list rendering
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { UIObject } from "./UIObject";
 | 
			
		||||
import { Accessor, createEffect } from "./reactivity";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Props for For component
 | 
			
		||||
 */
 | 
			
		||||
export type ForProps<T> = {
 | 
			
		||||
  /** Signal or accessor containing the array to iterate over */
 | 
			
		||||
  each: Accessor<T[]>;
 | 
			
		||||
} & Record<string, unknown>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Props for Show component
 | 
			
		||||
 */
 | 
			
		||||
export type ShowProps = {
 | 
			
		||||
  /** Condition accessor - when true, shows the child */
 | 
			
		||||
  when: Accessor<boolean>;
 | 
			
		||||
  /** Optional fallback to show when condition is false */
 | 
			
		||||
  fallback?: UIObject;
 | 
			
		||||
} & Record<string, unknown>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * For component - renders a list of items
 | 
			
		||||
 * Efficiently updates when the array changes
 | 
			
		||||
 * 
 | 
			
		||||
 * @template T - The type of items in the array
 | 
			
		||||
 * @param props - Props containing the array accessor
 | 
			
		||||
 * @param renderFn - Function to render each item
 | 
			
		||||
 * @returns UIObject representing the list
 | 
			
		||||
 * 
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```typescript
 | 
			
		||||
 * const [todos, setTodos] = createStore<Todo[]>([]);
 | 
			
		||||
 * 
 | 
			
		||||
 * For({ each: () => todos },
 | 
			
		||||
 *   (todo, i) => div({ class: "flex flex-row" },
 | 
			
		||||
 *     label({}, () => todo.title),
 | 
			
		||||
 *     button({ onClick: () => setTodos(arr => removeIndex(arr, i())) }, "X")
 | 
			
		||||
 *   )
 | 
			
		||||
 * )
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function For<T>(
 | 
			
		||||
  props: ForProps<T>,
 | 
			
		||||
  renderFn: (item: T, index: Accessor<number>) => UIObject
 | 
			
		||||
): UIObject {
 | 
			
		||||
  const container = new UIObject("for", props, []);
 | 
			
		||||
  
 | 
			
		||||
  // Track rendered items
 | 
			
		||||
  let renderedItems: UIObject[] = [];
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
   * Update the list when the array changes
 | 
			
		||||
   */
 | 
			
		||||
  const updateList = () => {
 | 
			
		||||
    const items = props.each();
 | 
			
		||||
    
 | 
			
		||||
    // Clear old items
 | 
			
		||||
    renderedItems.forEach(item => item.unmount());
 | 
			
		||||
    container.children = [];
 | 
			
		||||
    renderedItems = [];
 | 
			
		||||
    
 | 
			
		||||
    // Render new items
 | 
			
		||||
    items.forEach((item, index) => {
 | 
			
		||||
      const indexAccessor = () => index;
 | 
			
		||||
      const rendered = renderFn(item, indexAccessor);
 | 
			
		||||
      rendered.parent = container;
 | 
			
		||||
      container.children.push(rendered);
 | 
			
		||||
      renderedItems.push(rendered);
 | 
			
		||||
      rendered.mount();
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  // Create effect to watch for changes
 | 
			
		||||
  createEffect(() => {
 | 
			
		||||
    updateList();
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  return container;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Show component - conditionally renders content
 | 
			
		||||
 * 
 | 
			
		||||
 * @param props - Props containing condition and optional fallback
 | 
			
		||||
 * @param child - Content to show when condition is true
 | 
			
		||||
 * @returns UIObject representing the conditional content
 | 
			
		||||
 * 
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```typescript
 | 
			
		||||
 * const [loggedIn, setLoggedIn] = createSignal(false);
 | 
			
		||||
 * 
 | 
			
		||||
 * Show(
 | 
			
		||||
 *   {
 | 
			
		||||
 *     when: loggedIn,
 | 
			
		||||
 *     fallback: button({ onClick: () => setLoggedIn(true) }, "Log In")
 | 
			
		||||
 *   },
 | 
			
		||||
 *   button({ onClick: () => setLoggedIn(false) }, "Log Out")
 | 
			
		||||
 * )
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function Show(props: ShowProps, child: UIObject): UIObject {
 | 
			
		||||
  const container = new UIObject("show", props, []);
 | 
			
		||||
  
 | 
			
		||||
  let currentChild: UIObject | undefined = undefined;
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
   * Update which child is shown based on condition
 | 
			
		||||
   */
 | 
			
		||||
  const updateChild = () => {
 | 
			
		||||
    const condition = props.when();
 | 
			
		||||
    
 | 
			
		||||
    // Unmount current child
 | 
			
		||||
    if (currentChild !== undefined) {
 | 
			
		||||
      currentChild.unmount();
 | 
			
		||||
      container.removeChild(currentChild);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Mount appropriate child
 | 
			
		||||
    if (condition) {
 | 
			
		||||
      currentChild = child;
 | 
			
		||||
    } else if (props.fallback !== undefined) {
 | 
			
		||||
      currentChild = props.fallback;
 | 
			
		||||
    } else {
 | 
			
		||||
      currentChild = undefined;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    if (currentChild !== undefined) {
 | 
			
		||||
      container.appendChild(currentChild);
 | 
			
		||||
      currentChild.mount();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  // Create effect to watch for condition changes
 | 
			
		||||
  createEffect(() => {
 | 
			
		||||
    updateChild();
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  return container;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										438
									
								
								src/lib/ccTUI/framework.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										438
									
								
								src/lib/ccTUI/framework.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,438 @@
 | 
			
		||||
# ccTUI 框架重构指南
 | 
			
		||||
 | 
			
		||||
本文档旨在指导 ccTUI 框架的重构工作。框架将用于 Minecraft ComputerCraft (CC: Tweaked) 环境,其设计灵感来源于现代前端框架 SolidJS,并采用声明式、组件化的编程模型。
 | 
			
		||||
 | 
			
		||||
## 核心理念
 | 
			
		||||
 | 
			
		||||
- **声明式 UI**: 像 SolidJS 或 React 一样,通过编写函数式组件来描述 UI 状态,而不是手动操作界面。
 | 
			
		||||
- **响应式状态管理**: UI 会根据状态(State/Signal)的变化自动更新,开发者无需手动重绘。
 | 
			
		||||
- **组件化**: 将 UI 拆分为可复用的组件(函数),每个组件负责自身的状态和渲染。
 | 
			
		||||
- **Flexbox 布局**: 借鉴 Web 上的 Flexbox 模型,提供一套声明式的、强大的布局工具,以替代传统的绝对坐标布局。
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 目标 API 预览
 | 
			
		||||
 | 
			
		||||
为了直观地展示目标 API,这里将 SolidJS 的 "Simple Todos" 示例与我们期望的 ccTUI 实现进行对比。
 | 
			
		||||
 | 
			
		||||
**SolidJS (Web) 版本:**
 | 
			
		||||
```typescript
 | 
			
		||||
// ... imports
 | 
			
		||||
const App = () => {
 | 
			
		||||
  const [newTitle, setTitle] = createSignal("");
 | 
			
		||||
  const [todos, setTodos] = createLocalStore<TodoItem[]>("todo list", []);
 | 
			
		||||
  // ... logic
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <h3>Simple Todos Example</h3>
 | 
			
		||||
      <form onSubmit={addTodo}>
 | 
			
		||||
        <input /* ...props */ />
 | 
			
		||||
        <button>+</button>
 | 
			
		||||
      </form>
 | 
			
		||||
      <For each={todos}>
 | 
			
		||||
        {(todo, i) => (
 | 
			
		||||
          <div>
 | 
			
		||||
            <input type="checkbox" /* ...props */ />
 | 
			
		||||
            <input type="text" /* ...props */ />
 | 
			
		||||
            <button /* ...props */>x</button>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </For>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
render(App, document.getElementById("app")!);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**ccTUI (ComputerCraft) 目标版本:**
 | 
			
		||||
```typescript
 | 
			
		||||
// ... imports
 | 
			
		||||
type TodoItem = { title: string; done: boolean };
 | 
			
		||||
 | 
			
		||||
const App = () => {
 | 
			
		||||
  const [newTitle, setTitle] = createSignal("");
 | 
			
		||||
  const [todos, setTodos] = createStore<TodoItem[]>([]); // 使用简化版的 Store
 | 
			
		||||
 | 
			
		||||
  const addTodo = () => {
 | 
			
		||||
    batch(() => {
 | 
			
		||||
      setTodos(todos.length, { title: newTitle(), done: false });
 | 
			
		||||
      setTitle("");
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return div({ class: "flex flex-col" }, // 使用类似 TailwindCSS 的类名进行布局
 | 
			
		||||
    h3("Simple Todos Example"),
 | 
			
		||||
    form({ onSubmit: addTodo, class: "flex flex-row" },
 | 
			
		||||
      input({
 | 
			
		||||
        placeholder: "enter todo and click +",
 | 
			
		||||
        value: newTitle, // 直接传递 Signal
 | 
			
		||||
        onInput: setTitle, // 直接传递 Setter
 | 
			
		||||
      }),
 | 
			
		||||
      button("+")
 | 
			
		||||
    ),
 | 
			
		||||
    For({ each: todos },
 | 
			
		||||
      (todo, i) => div({ class: "flex flex-row items-center" },
 | 
			
		||||
        input({
 | 
			
		||||
          type: "checkbox",
 | 
			
		||||
          checked: () => todo.done, // 通过 accessor 获取
 | 
			
		||||
          onChange: (checked) => setTodos(i(), "done", checked),
 | 
			
		||||
        }),
 | 
			
		||||
        input({
 | 
			
		||||
          type: "text",
 | 
			
		||||
          value: () => todo.title,
 | 
			
		||||
          onChange: (newTitle) => setTodos(i(), "title", newTitle),
 | 
			
		||||
        }),
 | 
			
		||||
        button({ onClick: () => setTodos((t) => removeIndex(t, i())) }, "x")
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
render(App);
 | 
			
		||||
```
 | 
			
		||||
*注意:上述 ccTUI 代码是设计目标,具体实现(如 `createStore`, `removeIndex`)需要被创建。*
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 1. 基础组件 API
 | 
			
		||||
 | 
			
		||||
组件是返回 `UIObject` 的函数。第一个参数是 `props` 对象,后续参数是子组件。
 | 
			
		||||
 | 
			
		||||
### 容器与文本
 | 
			
		||||
- **`div(props: DivProps, ...children: UIObject[]): UIObject`**
 | 
			
		||||
  - 通用容器组件,用于包裹其他组件并应用布局样式。
 | 
			
		||||
  - `DivProps`: `{ class?: string }` - `class` 属性用于指定布局,详见“布局系统”。
 | 
			
		||||
 | 
			
		||||
- **`label(props: LabelProps, text: string | Signal<string>): UIObject`**
 | 
			
		||||
  - 静态或动态文本标签。
 | 
			
		||||
  - `LabelProps`: `{ class?: string }`
 | 
			
		||||
 | 
			
		||||
- **`h1`, `h2`, `h3`(text): UIObject**
 | 
			
		||||
  - 预设样式的标题标签,本质是 `label` 的封装。
 | 
			
		||||
 | 
			
		||||
### 交互组件
 | 
			
		||||
- **`button(props: ButtonProps, text: string): UIObject`**
 | 
			
		||||
  - 可点击的按钮。
 | 
			
		||||
  - `ButtonProps`: `{ onClick?: () => void, class?: string }`
 | 
			
		||||
  - 按钮会在被点击时调用 `onClick` 回调。
 | 
			
		||||
 | 
			
		||||
- **`input(props: InputProps): UIObject`**
 | 
			
		||||
  - 文本或复选框输入。
 | 
			
		||||
  - `InputProps`:
 | 
			
		||||
    - `type?: "text" | "checkbox"` (默认为 "text")
 | 
			
		||||
    - `value?: Signal<string>`: (用于 text) 文本内容的 Signal。
 | 
			
		||||
    - `onInput?: (value: string) => void`: (用于 text) 内容变化时的回调。
 | 
			
		||||
    - `checked?: Signal<boolean>`: (用于 checkbox) 选中状态的 Signal。
 | 
			
		||||
    - `onChange?: (checked: boolean) => void`: (用于 checkbox) 状态变化时的回调。
 | 
			
		||||
    - `placeholder?: string`
 | 
			
		||||
    - `class?: string`
 | 
			
		||||
 | 
			
		||||
- **`form(props: FormProps, ...children: UIObject[]): UIObject`**
 | 
			
		||||
  - 表单容器,主要用于组织输入组件。
 | 
			
		||||
  - `FormProps`: `{ onSubmit?: () => void, class?: string }`
 | 
			
		||||
  - 在表单内按回车键(或点击提交按钮,如果未来实现)会触发 `onSubmit`。
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 2. 控制流
 | 
			
		||||
 | 
			
		||||
- **`For<T>(props: ForProps<T>, renderFn: (item: T, index: number) => UIObject): UIObject`**
 | 
			
		||||
  - 用于渲染列表。它会根据 `each` 数组的变化,高效地创建、销毁或更新子组件。
 | 
			
		||||
  - `ForProps`: `{ each: Signal<T[]> }`
 | 
			
		||||
  - `renderFn`: 一个函数,接收当前项和索引,返回用于渲染该项的 `UIObject`。
 | 
			
		||||
 | 
			
		||||
- **`Show(props: ShowProps, child: UIObject): UIObject`**
 | 
			
		||||
  - 用于条件渲染。当 `when` 条件为 `true` 时渲染 `child`,否则渲染 `fallback`。
 | 
			
		||||
  - `ShowProps`: 
 | 
			
		||||
    - `when: () => boolean`: 一个返回布尔值的访问器函数 (accessor)。
 | 
			
		||||
    - `fallback?: UIObject`: 当 `when` 返回 `false` 时要渲染的组件。
 | 
			
		||||
  - `child`: 当 `when` 返回 `true` 时要渲染的组件。
 | 
			
		||||
 | 
			
		||||
  **SolidJS 示例:**
 | 
			
		||||
  ```typescript
 | 
			
		||||
  import { createSignal, Show } from "solid-js";
 | 
			
		||||
 | 
			
		||||
  function App() {
 | 
			
		||||
    const [loggedIn, setLoggedIn] = createSignal(false);
 | 
			
		||||
    const toggle = () => setLoggedIn(!loggedIn());
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Show
 | 
			
		||||
        when={loggedIn()}
 | 
			
		||||
        fallback={<button onClick={toggle}>Log In</button>}
 | 
			
		||||
      >
 | 
			
		||||
        <button onClick={toggle}>Log Out</button>
 | 
			
		||||
      </Show>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  ```
 | 
			
		||||
 | 
			
		||||
  **ccTUI 目标版本:**
 | 
			
		||||
  ```typescript
 | 
			
		||||
  const App = () => {
 | 
			
		||||
    const [loggedIn, setLoggedIn] = createSignal(false);
 | 
			
		||||
    const toggle = () => setLoggedIn(!loggedIn());
 | 
			
		||||
 | 
			
		||||
    return Show(
 | 
			
		||||
      {
 | 
			
		||||
        when: loggedIn, // 直接传递 Signal 的 getter
 | 
			
		||||
        fallback: button({ onClick: toggle }, "Log In"),
 | 
			
		||||
      },
 | 
			
		||||
      button({ onClick: toggle }, "Log Out")
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
  ```
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 3. 布局系统 (Flexbox)
 | 
			
		||||
 | 
			
		||||
借鉴 TailwindCSS 的类名系统,通过 `class` 属性为 `div` 等容器组件提供布局指令。渲染引擎需要解析这些类名并应用 Flexbox 算法。
 | 
			
		||||
 | 
			
		||||
### 核心类名
 | 
			
		||||
 | 
			
		||||
- **`flex`**: 必须。将容器声明为 Flex 容器。
 | 
			
		||||
- **`flex-row`**: (默认) 主轴方向为水平。
 | 
			
		||||
- **`flex-col`**: 主轴方向为垂直。
 | 
			
		||||
 | 
			
		||||
### 对齐与分布 (Justify & Align)
 | 
			
		||||
 | 
			
		||||
- **`justify-start`**: (默认) 从主轴起点开始排列。
 | 
			
		||||
- **`justify-center`**: 主轴居中。
 | 
			
		||||
- **`justify-end`**: 从主轴终点开始排列。
 | 
			
		||||
- **`justify-between`**: 两端对齐,项目之间的间隔都相等。
 | 
			
		||||
 | 
			
		||||
- **`items-start`**: 交叉轴的起点对齐。
 | 
			
		||||
- **`items-center`**: 交叉轴的中点对齐。
 | 
			
		||||
- **`items-end`**: 交叉轴的终点对齐。
 | 
			
		||||
 | 
			
		||||
### 示例
 | 
			
		||||
```typescript
 | 
			
		||||
// 一个垂直居中的登录框
 | 
			
		||||
div({ class: "flex flex-col justify-center items-center" },
 | 
			
		||||
  label("Username"),
 | 
			
		||||
  input({}),
 | 
			
		||||
  label("Password"),
 | 
			
		||||
  input({}),
 | 
			
		||||
  button("Login")
 | 
			
		||||
)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 实现要点
 | 
			
		||||
渲染引擎在计算布局时:
 | 
			
		||||
1. 解析 `class` 字符串,转换为布局属性(如 `flexDirection`, `justifyContent`)。
 | 
			
		||||
2. 实现一个简化的 Flexbox 算法,该算法能根据容器尺寸、子元素尺寸和布局属性,为每个子元素计算出正确的 `(x, y)` 坐标和 `(width, height)`。
 | 
			
		||||
3. 在 `draw` 阶段,将计算出的区域传递给子组件进行绘制。
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 4. 响应式系统 (Reactivity System)
 | 
			
		||||
 | 
			
		||||
框架的核心是其细粒度的响应式系统。该系统由 Signal 和 Effect 组成,其设计深受 SolidJS 启发。理解这两者是构建动态UI的关键。
 | 
			
		||||
 | 
			
		||||
### `createSignal`: 响应式的基本单元
 | 
			
		||||
 | 
			
		||||
Signal 是一个包含值的“盒子”,当它的值发生变化时,它可以通知所有正在“监听”它的代码。
 | 
			
		||||
 | 
			
		||||
- **`createSignal<T>(initialValue: T): [() => T, (newValue: T) => void]`**
 | 
			
		||||
  - 它接收一个初始值,并返回一个包含两个函数的数组:一个 `getter` 和一个 `setter`。
 | 
			
		||||
  - **Getter** (`() => T`): 一个无参数的函数,调用它会返回 Signal 的当前值。**重要的是,在特定上下文(如组件渲染或 Effect 中)调用 getter 会自动将该上下文注册为监听者。**
 | 
			
		||||
  - **Setter** (`(newValue: T) => void`): 一个函数,用于更新 Signal 的值。调用它会触发所有监听该 Signal 的上下文重新执行。
 | 
			
		||||
 | 
			
		||||
  **示例:**
 | 
			
		||||
  ```typescript
 | 
			
		||||
  // 1. 创建一个 signal
 | 
			
		||||
  const [count, setCount] = createSignal(0);
 | 
			
		||||
 | 
			
		||||
  // 2. 读取值 (这是一个函数调用)
 | 
			
		||||
  print(count()); // 输出: 0
 | 
			
		||||
 | 
			
		||||
  // 3. 更新值
 | 
			
		||||
  setCount(1);
 | 
			
		||||
  print(count()); // 输出: 1
 | 
			
		||||
 | 
			
		||||
  // 4. 在组件中使用 (当 count 变化时,label 会自动更新)
 | 
			
		||||
  label({}, () => `Count: ${count()}`);
 | 
			
		||||
  ```
 | 
			
		||||
 | 
			
		||||
### `createEffect`: 响应 Signal 的变化
 | 
			
		||||
 | 
			
		||||
Effect 用于将响应式系统与外部世界(如日志、计时器、手动API调用)连接起来。它是一个自动跟踪其依赖(即它内部读取的 Signal)并重新执行的函数。
 | 
			
		||||
 | 
			
		||||
- **`createEffect(fn: () => void): void`**
 | 
			
		||||
  - 它接收一个函数 `fn` 并立即执行一次。
 | 
			
		||||
  - 框架会监视 `fn` 在执行期间读取了哪些 Signal (调用了哪些 getter)。
 | 
			
		||||
  - 当任何一个被依赖的 Signal 更新时,`fn` 会被自动重新执行。
 | 
			
		||||
 | 
			
		||||
  **示例:**
 | 
			
		||||
  ```typescript
 | 
			
		||||
  const [count, setCount] = createSignal(0);
 | 
			
		||||
 | 
			
		||||
  // 创建一个 effect 来响应 count 的变化
 | 
			
		||||
  createEffect(() => {
 | 
			
		||||
    // 这个 effect 读取了 count(),因此它依赖于 count Signal
 | 
			
		||||
    print(`The current count is: ${count()}`);
 | 
			
		||||
  });
 | 
			
		||||
  // 控制台立即输出: "The current count is: 0"
 | 
			
		||||
 | 
			
		||||
  // 稍后在代码的其他地方更新 signal
 | 
			
		||||
  setCount(5);
 | 
			
		||||
  // effect 会自动重新运行,控制台输出: "The current count is: 5"
 | 
			
		||||
  ```
 | 
			
		||||
 | 
			
		||||
### 更新与批处理
 | 
			
		||||
 | 
			
		||||
- **`batch(fn: () => void)`**
 | 
			
		||||
  - 将多次状态更新合并为一次,以进行单次、高效的 UI 重绘。如果你需要在一个操作中连续多次调用 `setter`,应该将它们包裹在 `batch` 中以获得最佳性能。
 | 
			
		||||
 | 
			
		||||
  ```typescript
 | 
			
		||||
  batch(() => {
 | 
			
		||||
    setFirstName("John");
 | 
			
		||||
    setLastName("Smith");
 | 
			
		||||
  }); // UI 只会更新一次
 | 
			
		||||
  ```
 | 
			
		||||
 | 
			
		||||
### 复杂状态管理
 | 
			
		||||
 | 
			
		||||
- **`createStore<T extends object>(initialValue: T): [T, (updater: ...) => void]`**
 | 
			
		||||
  - 用于响应式地管理对象和数组。与 `createSignal` 管理单个值不同,`createStore` 允许你独立地更新对象或数组的特定部分,并只触发关心这些部分的更新。其 API 应参考 SolidJS 的 `createStore`。
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 5. 代码规范与构建
 | 
			
		||||
 | 
			
		||||
- **代码规范**:
 | 
			
		||||
  - 使用 `unknown` 代替 `any`。
 | 
			
		||||
  - 使用 `undefined` 代替 `null`。
 | 
			
		||||
  - 遵循 TSDoc 规范为所有函数、参数、返回值、分支和循环添加注释。
 | 
			
		||||
- **构建与验证**:
 | 
			
		||||
  - 使用 `just build-example sync` 命令构建示例代码并检查编译时错误。
 | 
			
		||||
  - 使用 `pnpm dlx eslint [file]` 命令对修改后的文件进行代码风格检查。
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 6. 文件结构说明
 | 
			
		||||
 | 
			
		||||
本节旨在说明 `ccTUI` 框架核心目录下的主要文件及其职责。
 | 
			
		||||
 | 
			
		||||
### `src/lib/ccTUI/`
 | 
			
		||||
 | 
			
		||||
- **`index.ts`**: 框架的公共 API 入口。所有可供外部使用的组件(如 `div`, `button`)和函数(如 `createSignal`)都应由此文件导出。
 | 
			
		||||
- **`Signal.ts`**: 包含框架的响应式系统核心,即 `createSignal`, `createEffect`, `batch` 等的实现。
 | 
			
		||||
- **`UIObject.ts`**: 所有 UI 元素的基类或基础类型。定义了如位置、尺寸、父子关系、绘制(draw)和更新(update)等通用接口。
 | 
			
		||||
- **`TUIApplication.ts`**: 应用程序的根实例。负责管理主窗口、事件循环(event loop)、焦点管理和全局重绘。
 | 
			
		||||
- **`UIWindow.ts`**: 代表一个独立的窗口(通常是整个终端屏幕),作为所有 UI 元素的根容器和绘制表面。
 | 
			
		||||
- **`TextLabel.ts`, `Button.ts`, `InputField.ts`**: 具体的基础组件实现。
 | 
			
		||||
- **`framework.md`**: (本文档) 框架的设计指南、API 参考和代码规范。
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 7. 框架示例
 | 
			
		||||
 | 
			
		||||
- **`src/tuiExample/main.ts`**
 | 
			
		||||
  - 此文件是 `ccTUI` 框架的功能示例和测试场(使用旧的 API)。
 | 
			
		||||
- **`src/tuiExample/main.new.ts`**
 | 
			
		||||
  - 新的响应式框架示例,展示 SolidJS 风格的 API。
 | 
			
		||||
  - 在对框架进行任何修改或添加新功能后,都应在此文件中创建相应的示例来验证其正确性并进行展示。
 | 
			
		||||
  - 使用 `just build-example sync` 命令可以编译此示例并将其同步到游戏内的 `computer` 目录中,以便在 Minecraft 环境中实际运行和查看效果。
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 8. 实现状态
 | 
			
		||||
 | 
			
		||||
### ✅ 已实现的功能
 | 
			
		||||
 | 
			
		||||
#### 响应式系统 (reactivity.ts)
 | 
			
		||||
- ✅ `createSignal<T>(initialValue: T)` - 创建响应式信号
 | 
			
		||||
- ✅ `createEffect(fn: () => void)` - 创建自动跟踪依赖的副作用
 | 
			
		||||
- ✅ `batch(fn: () => void)` - 批量更新多个信号
 | 
			
		||||
- ✅ `createMemo<T>(fn: () => T)` - 创建派生信号(计算属性)
 | 
			
		||||
 | 
			
		||||
#### Store (store.ts)
 | 
			
		||||
- ✅ `createStore<T>(initialValue: T)` - 创建响应式存储,用于管理对象和数组
 | 
			
		||||
- ✅ `removeIndex<T>(array: T[], index: number)` - 辅助函数:从数组中移除元素
 | 
			
		||||
- ✅ `insertAt<T>(array: T[], index: number, item: T)` - 辅助函数:插入元素到数组
 | 
			
		||||
 | 
			
		||||
#### 基础组件 (components.ts)
 | 
			
		||||
- ✅ `div(props, ...children)` - 通用容器组件
 | 
			
		||||
- ✅ `label(props, text)` - 文本标签组件
 | 
			
		||||
- ✅ `h1(text)`, `h2(text)`, `h3(text)` - 标题组件
 | 
			
		||||
- ✅ `button(props, text)` - 按钮组件
 | 
			
		||||
- ✅ `input(props)` - 输入组件(支持 text 和 checkbox 类型)
 | 
			
		||||
- ✅ `form(props, ...children)` - 表单容器组件
 | 
			
		||||
 | 
			
		||||
#### 控制流 (controlFlow.ts)
 | 
			
		||||
- ✅ `For<T>(props, renderFn)` - 列表渲染组件
 | 
			
		||||
- ✅ `Show(props, child)` - 条件渲染组件
 | 
			
		||||
 | 
			
		||||
#### 布局系统 (layout.ts)
 | 
			
		||||
- ✅ Flexbox 布局引擎实现
 | 
			
		||||
- ✅ 支持的类名:
 | 
			
		||||
  - `flex-row`, `flex-col` - 设置 flex 方向
 | 
			
		||||
  - `justify-start`, `justify-center`, `justify-end`, `justify-between` - 主轴对齐
 | 
			
		||||
  - `items-start`, `items-center`, `items-end` - 交叉轴对齐
 | 
			
		||||
 | 
			
		||||
#### 渲染器 (renderer.ts)
 | 
			
		||||
- ✅ 将 UI 树渲染到 ComputerCraft 终端
 | 
			
		||||
- ✅ 支持响应式文本内容
 | 
			
		||||
- ✅ 处理焦点状态的视觉反馈
 | 
			
		||||
 | 
			
		||||
#### 应用程序 (application.ts)
 | 
			
		||||
- ✅ `Application` 类 - 管理应用生命周期
 | 
			
		||||
- ✅ `render(rootFn)` - 便捷的渲染函数
 | 
			
		||||
- ✅ 事件循环(键盘、鼠标)
 | 
			
		||||
- ✅ 自动焦点管理
 | 
			
		||||
- ✅ 响应式重渲染
 | 
			
		||||
 | 
			
		||||
### 📋 API 导出 (index.ts)
 | 
			
		||||
- ✅ 所有新 API 已正确导出
 | 
			
		||||
- ✅ 保留旧 API 以实现向后兼容
 | 
			
		||||
 | 
			
		||||
### 🎯 示例代码
 | 
			
		||||
- ✅ `main.new.ts` - 简单的计数器示例,演示响应式系统的基本用法
 | 
			
		||||
 | 
			
		||||
### 🔄 向后兼容性
 | 
			
		||||
- ✅ 旧的类组件系统(Signal, UIComponent, Button 等)仍然可用
 | 
			
		||||
- ✅ 旧的示例代码 `main.ts` 不受影响
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
## 9. 使用指南
 | 
			
		||||
 | 
			
		||||
### 基本示例
 | 
			
		||||
 | 
			
		||||
```typescript
 | 
			
		||||
import { createSignal, div, label, button, render } from "../lib/ccTUI";
 | 
			
		||||
 | 
			
		||||
const App = () => {
 | 
			
		||||
  const [count, setCount] = createSignal(0);
 | 
			
		||||
  
 | 
			
		||||
  return div({ class: "flex flex-col" },
 | 
			
		||||
    label({}, () => `Count: ${count()}`),
 | 
			
		||||
    button({ onClick: () => setCount(count() + 1) }, "Increment")
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
render(App);
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 构建与运行
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# 构建示例
 | 
			
		||||
just build-example
 | 
			
		||||
 | 
			
		||||
# 构建并同步到游戏
 | 
			
		||||
just build-example sync
 | 
			
		||||
 | 
			
		||||
# 或使用 pnpm 直接构建
 | 
			
		||||
pnpm tstl -p ./tsconfig.tuiExample.json
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 代码检查
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# 运行 ESLint 检查
 | 
			
		||||
pnpm dlx eslint src/lib/ccTUI/reactivity.ts
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@@ -1,30 +1,55 @@
 | 
			
		||||
/**
 | 
			
		||||
 * ComputerCraft TUI (Terminal User Interface) Framework
 | 
			
		||||
 * Based on Qt signal/slot principles for event handling
 | 
			
		||||
 * Provides input/output, option selection and keyboard event handling
 | 
			
		||||
 * A declarative, reactive UI framework inspired by SolidJS
 | 
			
		||||
 * Provides components, reactivity, and flexbox layout for ComputerCraft
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { Signal } from "./Signal";
 | 
			
		||||
import { UIObject } from "./UIObject";
 | 
			
		||||
import { UIComponent } from "./UIComponent";
 | 
			
		||||
import { TextLabel } from "./TextLabel";
 | 
			
		||||
import { InputField } from "./InputField";
 | 
			
		||||
import { OptionSelector } from "./OptionSelector";
 | 
			
		||||
import { TabBar } from "./TabBar";
 | 
			
		||||
import { UIWindow } from "./UIWindow";
 | 
			
		||||
import { TUIApplication } from "./TUIApplication";
 | 
			
		||||
import { Button } from "./Button";
 | 
			
		||||
 | 
			
		||||
// Export the main classes for use in other modules
 | 
			
		||||
// Reactivity system
 | 
			
		||||
export {
 | 
			
		||||
  createSignal,
 | 
			
		||||
  createEffect,
 | 
			
		||||
  createMemo,
 | 
			
		||||
  batch,
 | 
			
		||||
  type Accessor,
 | 
			
		||||
  type Setter,
 | 
			
		||||
  type Signal,
 | 
			
		||||
} from "./reactivity";
 | 
			
		||||
 | 
			
		||||
// Store for complex state
 | 
			
		||||
export {
 | 
			
		||||
  createStore,
 | 
			
		||||
  removeIndex,
 | 
			
		||||
  insertAt,
 | 
			
		||||
  type SetStoreFunction,
 | 
			
		||||
} from "./store";
 | 
			
		||||
 | 
			
		||||
// Components
 | 
			
		||||
export {
 | 
			
		||||
  div,
 | 
			
		||||
  label,
 | 
			
		||||
  h1,
 | 
			
		||||
  h2,
 | 
			
		||||
  h3,
 | 
			
		||||
  button,
 | 
			
		||||
  input,
 | 
			
		||||
  form,
 | 
			
		||||
  type DivProps,
 | 
			
		||||
  type LabelProps,
 | 
			
		||||
  type ButtonProps,
 | 
			
		||||
  type InputProps,
 | 
			
		||||
  type FormProps,
 | 
			
		||||
} from "./components";
 | 
			
		||||
 | 
			
		||||
// Control flow
 | 
			
		||||
export { For, Show, type ForProps, type ShowProps } from "./controlFlow";
 | 
			
		||||
 | 
			
		||||
// Application
 | 
			
		||||
export { Application, render } from "./application";
 | 
			
		||||
 | 
			
		||||
// Core types
 | 
			
		||||
export {
 | 
			
		||||
  Signal,
 | 
			
		||||
  UIObject,
 | 
			
		||||
  UIComponent,
 | 
			
		||||
  TextLabel,
 | 
			
		||||
  InputField,
 | 
			
		||||
  OptionSelector,
 | 
			
		||||
  TabBar,
 | 
			
		||||
  UIWindow,
 | 
			
		||||
  TUIApplication,
 | 
			
		||||
  Button,
 | 
			
		||||
};
 | 
			
		||||
  type LayoutProps,
 | 
			
		||||
  type ComputedLayout,
 | 
			
		||||
  type BaseProps,
 | 
			
		||||
} from "./UIObject";
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										209
									
								
								src/lib/ccTUI/layout.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								src/lib/ccTUI/layout.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,209 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Flexbox layout engine
 | 
			
		||||
 * Calculates positions and sizes for UI elements based on flexbox rules
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { UIObject } from "./UIObject";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Measure the natural size of a UI element
 | 
			
		||||
 * This determines how much space an element wants to take up
 | 
			
		||||
 * 
 | 
			
		||||
 * @param node - The UI node to measure
 | 
			
		||||
 * @returns Width and height of the element
 | 
			
		||||
 */
 | 
			
		||||
function measureNode(node: UIObject): { width: number; height: number } {
 | 
			
		||||
  // Get text content if it exists
 | 
			
		||||
  const getTextContent = (): string => {
 | 
			
		||||
    if (node.textContent !== undefined) {
 | 
			
		||||
      if (typeof node.textContent === "function") {
 | 
			
		||||
        return node.textContent();
 | 
			
		||||
      }
 | 
			
		||||
      return node.textContent;
 | 
			
		||||
    }
 | 
			
		||||
    return "";
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  switch (node.type) {
 | 
			
		||||
    case "label":
 | 
			
		||||
    case "h1":
 | 
			
		||||
    case "h2":
 | 
			
		||||
    case "h3": {
 | 
			
		||||
      const text = getTextContent();
 | 
			
		||||
      return { width: text.length, height: 1 };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    case "button": {
 | 
			
		||||
      const text = getTextContent();
 | 
			
		||||
      // Buttons have brackets around them: [text]
 | 
			
		||||
      return { width: text.length + 2, height: 1 };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    case "input": {
 | 
			
		||||
      const type = node.props.type as string | undefined;
 | 
			
		||||
      if (type === "checkbox") {
 | 
			
		||||
        return { width: 3, height: 1 }; // [X] or [ ]
 | 
			
		||||
      }
 | 
			
		||||
      // Text input - use a default width or from props
 | 
			
		||||
      const width = (node.props.width as number | undefined) ?? 20;
 | 
			
		||||
      return { width, height: 1 };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    case "div":
 | 
			
		||||
    case "form":
 | 
			
		||||
    case "for":
 | 
			
		||||
    case "show":
 | 
			
		||||
    case "fragment": {
 | 
			
		||||
      // Container elements size based on their children
 | 
			
		||||
      let totalWidth = 0;
 | 
			
		||||
      let totalHeight = 0;
 | 
			
		||||
      
 | 
			
		||||
      if (node.children.length === 0) {
 | 
			
		||||
        return { width: 0, height: 0 };
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      const direction = node.layoutProps.flexDirection ?? "row";
 | 
			
		||||
      
 | 
			
		||||
      if (direction === "row") {
 | 
			
		||||
        // In row direction, width is sum of children, height is max
 | 
			
		||||
        for (const child of node.children) {
 | 
			
		||||
          const childSize = measureNode(child);
 | 
			
		||||
          totalWidth += childSize.width;
 | 
			
		||||
          totalHeight = math.max(totalHeight, childSize.height);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        // In column direction, height is sum of children, width is max
 | 
			
		||||
        for (const child of node.children) {
 | 
			
		||||
          const childSize = measureNode(child);
 | 
			
		||||
          totalWidth = math.max(totalWidth, childSize.width);
 | 
			
		||||
          totalHeight += childSize.height;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      return { width: totalWidth, height: totalHeight };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    default:
 | 
			
		||||
      return { width: 0, height: 0 };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Apply flexbox layout algorithm to a container and its children
 | 
			
		||||
 * 
 | 
			
		||||
 * @param node - The container node
 | 
			
		||||
 * @param availableWidth - Available width for layout
 | 
			
		||||
 * @param availableHeight - Available height for layout
 | 
			
		||||
 * @param startX - Starting X position
 | 
			
		||||
 * @param startY - Starting Y position
 | 
			
		||||
 */
 | 
			
		||||
export function calculateLayout(
 | 
			
		||||
  node: UIObject,
 | 
			
		||||
  availableWidth: number,
 | 
			
		||||
  availableHeight: number,
 | 
			
		||||
  startX = 1,
 | 
			
		||||
  startY = 1
 | 
			
		||||
): void {
 | 
			
		||||
  // Set this node's layout
 | 
			
		||||
  node.layout = {
 | 
			
		||||
    x: startX,
 | 
			
		||||
    y: startY,
 | 
			
		||||
    width: availableWidth,
 | 
			
		||||
    height: availableHeight,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (node.children.length === 0) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const direction = node.layoutProps.flexDirection ?? "row";
 | 
			
		||||
  const justify = node.layoutProps.justifyContent ?? "start";
 | 
			
		||||
  const align = node.layoutProps.alignItems ?? "start";
 | 
			
		||||
 | 
			
		||||
  // Measure all children
 | 
			
		||||
  const childMeasurements = node.children.map((child: UIObject) => measureNode(child));
 | 
			
		||||
  
 | 
			
		||||
  // Calculate total size needed
 | 
			
		||||
  let totalMainAxisSize = 0;
 | 
			
		||||
  let maxCrossAxisSize = 0;
 | 
			
		||||
  
 | 
			
		||||
  if (direction === "row") {
 | 
			
		||||
    for (const measure of childMeasurements) {
 | 
			
		||||
      totalMainAxisSize += measure.width;
 | 
			
		||||
      maxCrossAxisSize = math.max(maxCrossAxisSize, measure.height);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    for (const measure of childMeasurements) {
 | 
			
		||||
      totalMainAxisSize += measure.height;
 | 
			
		||||
      maxCrossAxisSize = math.max(maxCrossAxisSize, measure.width);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Calculate starting position based on justify-content
 | 
			
		||||
  let mainAxisPos = 0;
 | 
			
		||||
  let spacing = 0;
 | 
			
		||||
  
 | 
			
		||||
  if (direction === "row") {
 | 
			
		||||
    const remainingSpace = availableWidth - totalMainAxisSize;
 | 
			
		||||
    
 | 
			
		||||
    if (justify === "center") {
 | 
			
		||||
      mainAxisPos = remainingSpace / 2;
 | 
			
		||||
    } else if (justify === "end") {
 | 
			
		||||
      mainAxisPos = remainingSpace;
 | 
			
		||||
    } else if (justify === "between" && node.children.length > 1) {
 | 
			
		||||
      spacing = remainingSpace / (node.children.length - 1);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    const remainingSpace = availableHeight - totalMainAxisSize;
 | 
			
		||||
    
 | 
			
		||||
    if (justify === "center") {
 | 
			
		||||
      mainAxisPos = remainingSpace / 2;
 | 
			
		||||
    } else if (justify === "end") {
 | 
			
		||||
      mainAxisPos = remainingSpace;
 | 
			
		||||
    } else if (justify === "between" && node.children.length > 1) {
 | 
			
		||||
      spacing = remainingSpace / (node.children.length - 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Position each child
 | 
			
		||||
  for (let i = 0; i < node.children.length; i++) {
 | 
			
		||||
    const child = node.children[i];
 | 
			
		||||
    const measure = childMeasurements[i];
 | 
			
		||||
    
 | 
			
		||||
    let childX = startX;
 | 
			
		||||
    let childY = startY;
 | 
			
		||||
    
 | 
			
		||||
    if (direction === "row") {
 | 
			
		||||
      // Main axis is horizontal
 | 
			
		||||
      childX = startX + math.floor(mainAxisPos);
 | 
			
		||||
      
 | 
			
		||||
      // Cross axis (vertical) alignment
 | 
			
		||||
      if (align === "center") {
 | 
			
		||||
        childY = startY + math.floor((availableHeight - measure.height) / 2);
 | 
			
		||||
      } else if (align === "end") {
 | 
			
		||||
        childY = startY + (availableHeight - measure.height);
 | 
			
		||||
      } else {
 | 
			
		||||
        childY = startY; // start
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      mainAxisPos += measure.width + spacing;
 | 
			
		||||
    } else {
 | 
			
		||||
      // Main axis is vertical
 | 
			
		||||
      childY = startY + math.floor(mainAxisPos);
 | 
			
		||||
      
 | 
			
		||||
      // Cross axis (horizontal) alignment
 | 
			
		||||
      if (align === "center") {
 | 
			
		||||
        childX = startX + math.floor((availableWidth - measure.width) / 2);
 | 
			
		||||
      } else if (align === "end") {
 | 
			
		||||
        childX = startX + (availableWidth - measure.width);
 | 
			
		||||
      } else {
 | 
			
		||||
        childX = startX; // start
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      mainAxisPos += measure.height + spacing;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Recursively calculate layout for child
 | 
			
		||||
    calculateLayout(child, measure.width, measure.height, childX, childY);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										190
									
								
								src/lib/ccTUI/reactivity.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								src/lib/ccTUI/reactivity.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,190 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Reactive system inspired by SolidJS
 | 
			
		||||
 * Provides fine-grained reactivity with Signals and Effects
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Type for a Signal getter function
 | 
			
		||||
 */
 | 
			
		||||
export type Accessor<T> = () => T;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Type for a Signal setter function
 | 
			
		||||
 */
 | 
			
		||||
export type Setter<T> = (value: T) => void;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Type for a Signal tuple [getter, setter]
 | 
			
		||||
 */
 | 
			
		||||
export type Signal<T> = [Accessor<T>, Setter<T>];
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Listener function type that gets notified when a signal changes
 | 
			
		||||
 */
 | 
			
		||||
type Listener = () => void;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Context stack for tracking which effect is currently running
 | 
			
		||||
 */
 | 
			
		||||
let currentListener: Listener | undefined = undefined;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Batch update context - when true, effects won't run until batch completes
 | 
			
		||||
 */
 | 
			
		||||
let batchDepth = 0;
 | 
			
		||||
const pendingEffects = new Set<Listener>();
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates a reactive signal with a getter and setter
 | 
			
		||||
 * 
 | 
			
		||||
 * @template T - The type of the signal value
 | 
			
		||||
 * @param initialValue - The initial value of the signal
 | 
			
		||||
 * @returns A tuple containing [getter, setter]
 | 
			
		||||
 * 
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```typescript
 | 
			
		||||
 * const [count, setCount] = createSignal(0);
 | 
			
		||||
 * print(count()); // 0
 | 
			
		||||
 * setCount(5);
 | 
			
		||||
 * print(count()); // 5
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function createSignal<T>(initialValue: T): Signal<T> {
 | 
			
		||||
  let value = initialValue;
 | 
			
		||||
  const listeners = new Set<Listener>();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Getter function - reads the current value and subscribes the current listener
 | 
			
		||||
   */
 | 
			
		||||
  const getter: Accessor<T> = () => {
 | 
			
		||||
    // Subscribe the current running effect/computation
 | 
			
		||||
    if (currentListener !== undefined) {
 | 
			
		||||
      listeners.add(currentListener);
 | 
			
		||||
    }
 | 
			
		||||
    return value;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Setter function - updates the value and notifies all listeners
 | 
			
		||||
   */
 | 
			
		||||
  const setter: Setter<T> = (newValue: T) => {
 | 
			
		||||
    // Only update if value actually changed
 | 
			
		||||
    if (value !== newValue) {
 | 
			
		||||
      value = newValue;
 | 
			
		||||
      
 | 
			
		||||
      // Notify all subscribed listeners
 | 
			
		||||
      if (batchDepth > 0) {
 | 
			
		||||
        // In batch mode, collect effects to run later
 | 
			
		||||
        listeners.forEach(listener => pendingEffects.add(listener));
 | 
			
		||||
      } else {
 | 
			
		||||
        // Run effects immediately
 | 
			
		||||
        listeners.forEach(listener => {
 | 
			
		||||
          try {
 | 
			
		||||
            listener();
 | 
			
		||||
          } catch (e) {
 | 
			
		||||
            printError(e);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return [getter, setter];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates an effect that automatically tracks its dependencies and reruns when they change
 | 
			
		||||
 * 
 | 
			
		||||
 * @param fn - The effect function to run
 | 
			
		||||
 * 
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```typescript
 | 
			
		||||
 * const [count, setCount] = createSignal(0);
 | 
			
		||||
 * createEffect(() => {
 | 
			
		||||
 *   print(`Count is: ${count()}`);
 | 
			
		||||
 * });
 | 
			
		||||
 * setCount(1); // Effect automatically reruns and prints "Count is: 1"
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function createEffect(fn: () => void): void {
 | 
			
		||||
  const effect = () => {
 | 
			
		||||
    // Set this effect as the current listener
 | 
			
		||||
    const prevListener = currentListener;
 | 
			
		||||
    currentListener = effect;
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
      // Run the effect function - it will subscribe to any signals it reads
 | 
			
		||||
      fn();
 | 
			
		||||
    } finally {
 | 
			
		||||
      // Restore previous listener
 | 
			
		||||
      currentListener = prevListener;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Run the effect immediately for the first time
 | 
			
		||||
  effect();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Batches multiple signal updates to prevent excessive re-renders
 | 
			
		||||
 * All signal updates within the batch function will only trigger effects once
 | 
			
		||||
 * 
 | 
			
		||||
 * @param fn - Function containing multiple signal updates
 | 
			
		||||
 * 
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```typescript
 | 
			
		||||
 * batch(() => {
 | 
			
		||||
 *   setFirstName("John");
 | 
			
		||||
 *   setLastName("Doe");
 | 
			
		||||
 * }); // Effects only run once after both updates
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function batch(fn: () => void): void {
 | 
			
		||||
  batchDepth++;
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    fn();
 | 
			
		||||
  } finally {
 | 
			
		||||
    batchDepth--;
 | 
			
		||||
    
 | 
			
		||||
    // If we're done with all batches, run pending effects
 | 
			
		||||
    if (batchDepth === 0) {
 | 
			
		||||
      const effects = Array.from(pendingEffects);
 | 
			
		||||
      pendingEffects.clear();
 | 
			
		||||
      
 | 
			
		||||
      effects.forEach(effect => {
 | 
			
		||||
        try {
 | 
			
		||||
          effect();
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          printError(e);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates a derived signal (memo) that computes a value based on other signals
 | 
			
		||||
 * The computation is cached and only recomputed when dependencies change
 | 
			
		||||
 * 
 | 
			
		||||
 * @template T - The type of the computed value
 | 
			
		||||
 * @param fn - Function that computes the value
 | 
			
		||||
 * @returns An accessor function for the computed value
 | 
			
		||||
 * 
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```typescript
 | 
			
		||||
 * const [firstName, setFirstName] = createSignal("John");
 | 
			
		||||
 * const [lastName, setLastName] = createSignal("Doe");
 | 
			
		||||
 * const fullName = createMemo(() => `${firstName()} ${lastName()}`);
 | 
			
		||||
 * print(fullName()); // "John Doe"
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function createMemo<T>(fn: () => T): Accessor<T> {
 | 
			
		||||
  const [value, setValue] = createSignal<T>(undefined as unknown as T);
 | 
			
		||||
  
 | 
			
		||||
  createEffect(() => {
 | 
			
		||||
    setValue(fn());
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  return value;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										199
									
								
								src/lib/ccTUI/renderer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								src/lib/ccTUI/renderer.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,199 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Renderer for drawing UI to the ComputerCraft terminal
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { UIObject } from "./UIObject";
 | 
			
		||||
import { Accessor } from "./reactivity";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get text content from a node (resolving signals if needed)
 | 
			
		||||
 */
 | 
			
		||||
function getTextContent(node: UIObject): string {
 | 
			
		||||
  if (node.textContent !== undefined) {
 | 
			
		||||
    if (typeof node.textContent === "function") {
 | 
			
		||||
      return (node.textContent)();
 | 
			
		||||
    }
 | 
			
		||||
    return node.textContent;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // For nodes with text children, get their content
 | 
			
		||||
  if (node.children.length > 0 && node.children[0].textContent !== undefined) {
 | 
			
		||||
    const child = node.children[0];
 | 
			
		||||
    if (typeof child.textContent === "function") {
 | 
			
		||||
      return (child.textContent)();
 | 
			
		||||
    }
 | 
			
		||||
    return child.textContent!;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return "";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Draw a single UI node to the terminal
 | 
			
		||||
 * 
 | 
			
		||||
 * @param node - The node to draw
 | 
			
		||||
 * @param focused - Whether this node has focus
 | 
			
		||||
 */
 | 
			
		||||
function drawNode(node: UIObject, focused: boolean): void {
 | 
			
		||||
  if (!node.layout) return;
 | 
			
		||||
  
 | 
			
		||||
  const { x, y, width } = node.layout;
 | 
			
		||||
  
 | 
			
		||||
  // Save cursor position
 | 
			
		||||
  const [origX, origY] = term.getCursorPos();
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    switch (node.type) {
 | 
			
		||||
      case "label":
 | 
			
		||||
      case "h1":
 | 
			
		||||
      case "h2":
 | 
			
		||||
      case "h3": {
 | 
			
		||||
        const text = getTextContent(node);
 | 
			
		||||
        
 | 
			
		||||
        // Set colors based on heading level
 | 
			
		||||
        if (node.type === "h1") {
 | 
			
		||||
          term.setTextColor(colors.yellow);
 | 
			
		||||
        } else if (node.type === "h2") {
 | 
			
		||||
          term.setTextColor(colors.orange);
 | 
			
		||||
        } else if (node.type === "h3") {
 | 
			
		||||
          term.setTextColor(colors.lightGray);
 | 
			
		||||
        } else {
 | 
			
		||||
          term.setTextColor(colors.white);
 | 
			
		||||
        }
 | 
			
		||||
        term.setBackgroundColor(colors.black);
 | 
			
		||||
        
 | 
			
		||||
        term.setCursorPos(x, y);
 | 
			
		||||
        term.write(text.substring(0, width));
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      case "button": {
 | 
			
		||||
        const text = getTextContent(node);
 | 
			
		||||
        
 | 
			
		||||
        // Set colors based on focus
 | 
			
		||||
        if (focused) {
 | 
			
		||||
          term.setTextColor(colors.black);
 | 
			
		||||
          term.setBackgroundColor(colors.yellow);
 | 
			
		||||
        } else {
 | 
			
		||||
          term.setTextColor(colors.white);
 | 
			
		||||
          term.setBackgroundColor(colors.gray);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        term.setCursorPos(x, y);
 | 
			
		||||
        term.write(`[${text}]`);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      case "input": {
 | 
			
		||||
        const type = node.props.type as string | undefined;
 | 
			
		||||
        
 | 
			
		||||
        if (type === "checkbox") {
 | 
			
		||||
          // Draw checkbox
 | 
			
		||||
          let isChecked = false;
 | 
			
		||||
          const checkedProp = node.props.checked;
 | 
			
		||||
          if (typeof checkedProp === "function") {
 | 
			
		||||
            isChecked = (checkedProp as Accessor<boolean>)();
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          if (focused) {
 | 
			
		||||
            term.setTextColor(colors.black);
 | 
			
		||||
            term.setBackgroundColor(colors.white);
 | 
			
		||||
          } else {
 | 
			
		||||
            term.setTextColor(colors.white);
 | 
			
		||||
            term.setBackgroundColor(colors.black);
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          term.setCursorPos(x, y);
 | 
			
		||||
          term.write(isChecked ? "[X]" : "[ ]");
 | 
			
		||||
        } else {
 | 
			
		||||
          // Draw text input
 | 
			
		||||
          let value = "";
 | 
			
		||||
          const valueProp = node.props.value;
 | 
			
		||||
          if (typeof valueProp === "function") {
 | 
			
		||||
            value = (valueProp as Accessor<string>)();
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          const placeholder = node.props.placeholder as string | undefined;
 | 
			
		||||
          let displayText = value;
 | 
			
		||||
          
 | 
			
		||||
          if (value === "" && placeholder !== undefined) {
 | 
			
		||||
            displayText = placeholder;
 | 
			
		||||
            term.setTextColor(colors.gray);
 | 
			
		||||
          } else if (focused) {
 | 
			
		||||
            term.setTextColor(colors.black);
 | 
			
		||||
          } else {
 | 
			
		||||
            term.setTextColor(colors.white);
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          if (focused) {
 | 
			
		||||
            term.setBackgroundColor(colors.white);
 | 
			
		||||
          } else {
 | 
			
		||||
            term.setBackgroundColor(colors.black);
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          term.setCursorPos(x, y);
 | 
			
		||||
          // Pad or truncate to fit width
 | 
			
		||||
          if (displayText.length > width) {
 | 
			
		||||
            displayText = displayText.substring(0, width);
 | 
			
		||||
          } else {
 | 
			
		||||
            displayText = displayText.padEnd(width, " ");
 | 
			
		||||
          }
 | 
			
		||||
          term.write(displayText);
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      case "div":
 | 
			
		||||
      case "form":
 | 
			
		||||
      case "for":
 | 
			
		||||
      case "show": {
 | 
			
		||||
        // Container elements don't draw themselves, just their children
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      case "fragment": {
 | 
			
		||||
        // Fragment with text content
 | 
			
		||||
        if (node.textContent !== undefined) {
 | 
			
		||||
          const text = typeof node.textContent === "function" 
 | 
			
		||||
            ? (node.textContent)() 
 | 
			
		||||
            : node.textContent;
 | 
			
		||||
          
 | 
			
		||||
          term.setTextColor(colors.white);
 | 
			
		||||
          term.setBackgroundColor(colors.black);
 | 
			
		||||
          term.setCursorPos(x, y);
 | 
			
		||||
          term.write(text.substring(0, width));
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  } finally {
 | 
			
		||||
    // Restore cursor
 | 
			
		||||
    term.setCursorPos(origX, origY);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Recursively render a UI tree
 | 
			
		||||
 * 
 | 
			
		||||
 * @param node - The root node to render
 | 
			
		||||
 * @param focusedNode - The currently focused node (if any)
 | 
			
		||||
 */
 | 
			
		||||
export function render(node: UIObject, focusedNode?: UIObject): void {
 | 
			
		||||
  // Draw this node
 | 
			
		||||
  const isFocused = node === focusedNode;
 | 
			
		||||
  drawNode(node, isFocused);
 | 
			
		||||
  
 | 
			
		||||
  // Recursively draw children
 | 
			
		||||
  for (const child of node.children) {
 | 
			
		||||
    render(child, focusedNode);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Clear the entire terminal screen
 | 
			
		||||
 */
 | 
			
		||||
export function clearScreen(): void {
 | 
			
		||||
  term.setBackgroundColor(colors.black);
 | 
			
		||||
  term.clear();
 | 
			
		||||
  term.setCursorPos(1, 1);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										131
									
								
								src/lib/ccTUI/store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								src/lib/ccTUI/store.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,131 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Store for managing complex reactive state (objects and arrays)
 | 
			
		||||
 * Inspired by SolidJS's createStore
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { createSignal, Accessor } from "./reactivity";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Store setter function type
 | 
			
		||||
 */
 | 
			
		||||
export interface SetStoreFunction<T> {
 | 
			
		||||
  /**
 | 
			
		||||
   * Set a specific property or array index
 | 
			
		||||
   */
 | 
			
		||||
  <K extends keyof T>(key: K, value: T[K]): void;
 | 
			
		||||
  /**
 | 
			
		||||
   * Set array index and property
 | 
			
		||||
   */
 | 
			
		||||
  (index: number, key: string, value: unknown): void;
 | 
			
		||||
  /**
 | 
			
		||||
   * Set using an updater function
 | 
			
		||||
   */
 | 
			
		||||
  (updater: (prev: T) => T): void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates a reactive store for managing objects and arrays
 | 
			
		||||
 * Returns an accessor for the store and a setter function
 | 
			
		||||
 * 
 | 
			
		||||
 * @template T - The type of the store (must be an object)
 | 
			
		||||
 * @param initialValue - The initial value of the store
 | 
			
		||||
 * @returns A tuple of [accessor, setStore]
 | 
			
		||||
 * 
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```typescript
 | 
			
		||||
 * const [todos, setTodos] = createStore<Todo[]>([]);
 | 
			
		||||
 * 
 | 
			
		||||
 * // Add a new todo
 | 
			
		||||
 * setTodos(todos().length, { title: "New todo", done: false });
 | 
			
		||||
 * 
 | 
			
		||||
 * // Update a specific todo
 | 
			
		||||
 * setTodos(0, "done", true);
 | 
			
		||||
 * 
 | 
			
		||||
 * // Replace entire store
 | 
			
		||||
 * setTodos([{ title: "First", done: false }]);
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function createStore<T extends object>(initialValue: T): [Accessor<T>, SetStoreFunction<T>] {
 | 
			
		||||
  // Use a signal to track the entire state
 | 
			
		||||
  const [get, set] = createSignal(initialValue);
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
   * Setter function with multiple overloads
 | 
			
		||||
   */
 | 
			
		||||
  const setStore: SetStoreFunction<T> = ((...args: unknown[]) => {
 | 
			
		||||
    if (args.length === 1) {
 | 
			
		||||
      // Single argument - either a value or an updater function
 | 
			
		||||
      const arg = args[0];
 | 
			
		||||
      if (typeof arg === "function") {
 | 
			
		||||
        // Updater function
 | 
			
		||||
        const updater = arg as (prev: T) => T;
 | 
			
		||||
        set(updater(get()));
 | 
			
		||||
      } else {
 | 
			
		||||
        // Direct value
 | 
			
		||||
        set(arg as T);
 | 
			
		||||
      }
 | 
			
		||||
    } else if (args.length === 2) {
 | 
			
		||||
      // Two arguments - key and value for object property or array index
 | 
			
		||||
      const key = args[0] as keyof T;
 | 
			
		||||
      const value = args[1] as T[keyof T];
 | 
			
		||||
      const current = get();
 | 
			
		||||
      
 | 
			
		||||
      if (Array.isArray(current)) {
 | 
			
		||||
        // For arrays, create a new array with the updated element
 | 
			
		||||
        const newArray = [...current] as T;
 | 
			
		||||
        (newArray as unknown[])[key as unknown as number] = value;
 | 
			
		||||
        set(newArray);
 | 
			
		||||
      } else {
 | 
			
		||||
        // For objects, create a new object with the updated property
 | 
			
		||||
        set({ ...current, [key]: value });
 | 
			
		||||
      }
 | 
			
		||||
    } else if (args.length === 3) {
 | 
			
		||||
      // Three arguments - array index, property key, and value
 | 
			
		||||
      const index = args[0] as number;
 | 
			
		||||
      const key = args[1] as string;
 | 
			
		||||
      const value = args[2];
 | 
			
		||||
      const current = get();
 | 
			
		||||
      
 | 
			
		||||
      if (Array.isArray(current)) {
 | 
			
		||||
        const newArray = [...current] as unknown[];
 | 
			
		||||
        if (typeof newArray[index] === "object" && newArray[index] !== undefined) {
 | 
			
		||||
          newArray[index] = { ...(newArray[index]!), [key]: value };
 | 
			
		||||
        }
 | 
			
		||||
        set(newArray as T);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }) as SetStoreFunction<T>;
 | 
			
		||||
  
 | 
			
		||||
  return [get, setStore];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper function to remove an item from an array at a specific index
 | 
			
		||||
 * 
 | 
			
		||||
 * @template T - The type of array elements
 | 
			
		||||
 * @param array - The array to remove from
 | 
			
		||||
 * @param index - The index to remove
 | 
			
		||||
 * @returns A new array with the item removed
 | 
			
		||||
 * 
 | 
			
		||||
 * @example
 | 
			
		||||
 * ```typescript
 | 
			
		||||
 * const [todos, setTodos] = createStore([1, 2, 3, 4]);
 | 
			
		||||
 * setTodos(arr => removeIndex(arr, 1)); // Results in [1, 3, 4]
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function removeIndex<T>(array: T[], index: number): T[] {
 | 
			
		||||
  return [...array.slice(0, index), ...array.slice(index + 1)];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper function to insert an item into an array at a specific index
 | 
			
		||||
 * 
 | 
			
		||||
 * @template T - The type of array elements
 | 
			
		||||
 * @param array - The array to insert into
 | 
			
		||||
 * @param index - The index to insert at
 | 
			
		||||
 * @param item - The item to insert
 | 
			
		||||
 * @returns A new array with the item inserted
 | 
			
		||||
 */
 | 
			
		||||
export function insertAt<T>(array: T[], index: number, item: T): T[] {
 | 
			
		||||
  return [...array.slice(0, index), item, ...array.slice(index)];
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										174
									
								
								src/lib/event.ts
									
									
									
									
									
								
							
							
						
						
									
										174
									
								
								src/lib/event.ts
									
									
									
									
									
								
							@@ -88,12 +88,12 @@ export class TaskCompleteEvent implements IEvent {
 | 
			
		||||
  public id = 0;
 | 
			
		||||
  public success = false;
 | 
			
		||||
  public error: string | undefined = undefined;
 | 
			
		||||
  public params: any[] = [];
 | 
			
		||||
  public params: unknown[] = [];
 | 
			
		||||
  public get_name() {
 | 
			
		||||
    return "task_complete";
 | 
			
		||||
  }
 | 
			
		||||
  public get_args() {
 | 
			
		||||
    if (this.success) return [this.id, this.success].concat(this.params);
 | 
			
		||||
    if (this.success) return [this.id, this.success].concat(this.params as []);
 | 
			
		||||
    else return [this.id, this.success, this.error];
 | 
			
		||||
  }
 | 
			
		||||
  public static init(args: unknown[]): IEvent | undefined {
 | 
			
		||||
@@ -120,10 +120,10 @@ export class RedstoneEvent implements IEvent {
 | 
			
		||||
  public get_args() {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
  public static init(args: any[]): IEvent | undefined {
 | 
			
		||||
    if (!(typeof args[0] === "string") || (args[0] as string) != "redstone")
 | 
			
		||||
  public static init(args: unknown[]): IEvent | undefined {
 | 
			
		||||
    if (!(typeof args[0] === "string") || args[0] != "redstone")
 | 
			
		||||
      return undefined;
 | 
			
		||||
    let ev = new RedstoneEvent();
 | 
			
		||||
    const ev = new RedstoneEvent();
 | 
			
		||||
    return ev;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -135,62 +135,61 @@ export class TerminateEvent implements IEvent {
 | 
			
		||||
  public get_args() {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
  public static init(args: any[]): IEvent | undefined {
 | 
			
		||||
    if (!(typeof args[0] === "string") || (args[0] as string) != "terminate")
 | 
			
		||||
  public static init(args: unknown[]): IEvent | undefined {
 | 
			
		||||
    if (!(typeof args[0] === "string") || args[0] != "terminate")
 | 
			
		||||
      return undefined;
 | 
			
		||||
    let ev = new TerminateEvent();
 | 
			
		||||
    const ev = new TerminateEvent();
 | 
			
		||||
    return ev;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class DiskEvent implements IEvent {
 | 
			
		||||
  public side: string = "";
 | 
			
		||||
  public eject: boolean = false;
 | 
			
		||||
  public side = "";
 | 
			
		||||
  public eject = false;
 | 
			
		||||
  public get_name() {
 | 
			
		||||
    return this.eject ? "disk_eject" : "disk";
 | 
			
		||||
  }
 | 
			
		||||
  public get_args() {
 | 
			
		||||
    return [this.side];
 | 
			
		||||
  }
 | 
			
		||||
  public static init(args: any[]): IEvent | undefined {
 | 
			
		||||
  public static init(args: unknown[]): IEvent | undefined {
 | 
			
		||||
    if (
 | 
			
		||||
      !(typeof args[0] === "string") ||
 | 
			
		||||
      ((args[0] as string) != "disk" && (args[0] as string) != "disk_eject")
 | 
			
		||||
      (args[0] != "disk" && args[0] != "disk_eject")
 | 
			
		||||
    )
 | 
			
		||||
      return undefined;
 | 
			
		||||
    let ev = new DiskEvent();
 | 
			
		||||
    const ev = new DiskEvent();
 | 
			
		||||
    ev.side = args[1] as string;
 | 
			
		||||
    ev.eject = (args[0] as string) == "disk_eject";
 | 
			
		||||
    ev.eject = args[0] == "disk_eject";
 | 
			
		||||
    return ev;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class PeripheralEvent implements IEvent {
 | 
			
		||||
  public side: string = "";
 | 
			
		||||
  public detach: boolean = false;
 | 
			
		||||
  public side = "";
 | 
			
		||||
  public detach = false;
 | 
			
		||||
  public get_name() {
 | 
			
		||||
    return this.detach ? "peripheral_detach" : "peripheral";
 | 
			
		||||
  }
 | 
			
		||||
  public get_args() {
 | 
			
		||||
    return [this.side];
 | 
			
		||||
  }
 | 
			
		||||
  public static init(args: any[]): IEvent | undefined {
 | 
			
		||||
  public static init(args: unknown[]): IEvent | undefined {
 | 
			
		||||
    if (
 | 
			
		||||
      !(typeof args[0] === "string") ||
 | 
			
		||||
      ((args[0] as string) != "peripheral" &&
 | 
			
		||||
        (args[0] as string) != "peripheral_detach")
 | 
			
		||||
      (args[0] != "peripheral" && args[0] != "peripheral_detach")
 | 
			
		||||
    )
 | 
			
		||||
      return undefined;
 | 
			
		||||
    let ev = new PeripheralEvent();
 | 
			
		||||
    const ev = new PeripheralEvent();
 | 
			
		||||
    ev.side = args[1] as string;
 | 
			
		||||
    ev.detach = (args[0] as string) == "peripheral_detach";
 | 
			
		||||
    ev.detach = args[0] == "peripheral_detach";
 | 
			
		||||
    return ev;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class RednetMessageEvent implements IEvent {
 | 
			
		||||
  public sender: number = 0;
 | 
			
		||||
  public message: any;
 | 
			
		||||
  public sender = 0;
 | 
			
		||||
  public message: unknown;
 | 
			
		||||
  public protocol: string | undefined = undefined;
 | 
			
		||||
  public get_name() {
 | 
			
		||||
    return "rednet_message";
 | 
			
		||||
@@ -198,13 +197,10 @@ export class RednetMessageEvent implements IEvent {
 | 
			
		||||
  public get_args() {
 | 
			
		||||
    return [this.sender, this.message, this.protocol];
 | 
			
		||||
  }
 | 
			
		||||
  public static init(args: any[]): IEvent | undefined {
 | 
			
		||||
    if (
 | 
			
		||||
      !(typeof args[0] === "string") ||
 | 
			
		||||
      (args[0] as string) != "rednet_message"
 | 
			
		||||
    )
 | 
			
		||||
  public static init(args: unknown[]): IEvent | undefined {
 | 
			
		||||
    if (!(typeof args[0] === "string") || args[0] != "rednet_message")
 | 
			
		||||
      return undefined;
 | 
			
		||||
    let ev = new RednetMessageEvent();
 | 
			
		||||
    const ev = new RednetMessageEvent();
 | 
			
		||||
    ev.sender = args[1] as number;
 | 
			
		||||
    ev.message = args[2];
 | 
			
		||||
    ev.protocol = args[3] as string;
 | 
			
		||||
@@ -213,11 +209,11 @@ export class RednetMessageEvent implements IEvent {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ModemMessageEvent implements IEvent {
 | 
			
		||||
  public side: string = "";
 | 
			
		||||
  public channel: number = 0;
 | 
			
		||||
  public replyChannel: number = 0;
 | 
			
		||||
  public message: any;
 | 
			
		||||
  public distance: number = 0;
 | 
			
		||||
  public side = "";
 | 
			
		||||
  public channel = 0;
 | 
			
		||||
  public replyChannel = 0;
 | 
			
		||||
  public message: unknown;
 | 
			
		||||
  public distance = 0;
 | 
			
		||||
  public get_name() {
 | 
			
		||||
    return "modem_message";
 | 
			
		||||
  }
 | 
			
		||||
@@ -230,13 +226,10 @@ export class ModemMessageEvent implements IEvent {
 | 
			
		||||
      this.distance,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
  public static init(args: any[]): IEvent | undefined {
 | 
			
		||||
    if (
 | 
			
		||||
      !(typeof args[0] === "string") ||
 | 
			
		||||
      (args[0] as string) != "modem_message"
 | 
			
		||||
    )
 | 
			
		||||
  public static init(args: unknown[]): IEvent | undefined {
 | 
			
		||||
    if (!(typeof args[0] === "string") || args[0] != "modem_message")
 | 
			
		||||
      return undefined;
 | 
			
		||||
    let ev = new ModemMessageEvent();
 | 
			
		||||
    const ev = new ModemMessageEvent();
 | 
			
		||||
    ev.side = args[1] as string;
 | 
			
		||||
    ev.channel = args[2] as number;
 | 
			
		||||
    ev.replyChannel = args[3] as number;
 | 
			
		||||
@@ -247,7 +240,7 @@ export class ModemMessageEvent implements IEvent {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class HTTPEvent implements IEvent {
 | 
			
		||||
  public url: string = "";
 | 
			
		||||
  public url = "";
 | 
			
		||||
  public handle: HTTPResponse | undefined = undefined;
 | 
			
		||||
  public error: string | undefined = undefined;
 | 
			
		||||
  public get_name() {
 | 
			
		||||
@@ -256,25 +249,24 @@ export class HTTPEvent implements IEvent {
 | 
			
		||||
  public get_args() {
 | 
			
		||||
    return [
 | 
			
		||||
      this.url,
 | 
			
		||||
      this.error == undefined ? this.handle : this.error,
 | 
			
		||||
      this.error ?? this.handle,
 | 
			
		||||
      this.error != undefined ? this.handle : undefined,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
  public static init(args: any[]): IEvent | undefined {
 | 
			
		||||
  public static init(args: unknown[]): IEvent | undefined {
 | 
			
		||||
    if (
 | 
			
		||||
      !(typeof args[0] === "string") ||
 | 
			
		||||
      ((args[0] as string) != "http_success" &&
 | 
			
		||||
        (args[0] as string) != "http_failure")
 | 
			
		||||
      (args[0] != "http_success" && args[0] != "http_failure")
 | 
			
		||||
    )
 | 
			
		||||
      return undefined;
 | 
			
		||||
    let ev = new HTTPEvent();
 | 
			
		||||
    const ev = new HTTPEvent();
 | 
			
		||||
    ev.url = args[1] as string;
 | 
			
		||||
    if ((args[0] as string) == "http_success") {
 | 
			
		||||
    if (args[0] == "http_success") {
 | 
			
		||||
      ev.error = undefined;
 | 
			
		||||
      ev.handle = args[2] as HTTPResponse;
 | 
			
		||||
    } else {
 | 
			
		||||
      ev.error = args[2] as string;
 | 
			
		||||
      if (ev.error == undefined) ev.error = "";
 | 
			
		||||
      ev.error ??= "";
 | 
			
		||||
      ev.handle = args[3] as HTTPResponse;
 | 
			
		||||
    }
 | 
			
		||||
    return ev;
 | 
			
		||||
@@ -288,17 +280,16 @@ export class WebSocketEvent implements IEvent {
 | 
			
		||||
    return this.error == undefined ? "websocket_success" : "websocket_failure";
 | 
			
		||||
  }
 | 
			
		||||
  public get_args() {
 | 
			
		||||
    return [this.handle == undefined ? this.error : this.handle];
 | 
			
		||||
    return [this.handle ?? this.error];
 | 
			
		||||
  }
 | 
			
		||||
  public static init(args: any[]): IEvent | undefined {
 | 
			
		||||
  public static init(args: unknown[]): IEvent | undefined {
 | 
			
		||||
    if (
 | 
			
		||||
      !(typeof args[0] === "string") ||
 | 
			
		||||
      ((args[0] as string) != "websocket_success" &&
 | 
			
		||||
        (args[0] as string) != "websocket_failure")
 | 
			
		||||
      (args[0] != "websocket_success" && args[0] != "websocket_failure")
 | 
			
		||||
    )
 | 
			
		||||
      return undefined;
 | 
			
		||||
    let ev = new WebSocketEvent();
 | 
			
		||||
    if ((args[0] as string) == "websocket_success") {
 | 
			
		||||
    const ev = new WebSocketEvent();
 | 
			
		||||
    if (args[0] == "websocket_success") {
 | 
			
		||||
      ev.handle = args[1] as WebSocket;
 | 
			
		||||
      ev.error = undefined;
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -319,9 +310,9 @@ export enum MouseEventType {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class MouseEvent implements IEvent {
 | 
			
		||||
  public button: number = 0;
 | 
			
		||||
  public x: number = 0;
 | 
			
		||||
  public y: number = 0;
 | 
			
		||||
  public button = 0;
 | 
			
		||||
  public x = 0;
 | 
			
		||||
  public y = 0;
 | 
			
		||||
  public side: string | undefined = undefined;
 | 
			
		||||
  public type: MouseEventType = MouseEventType.Click;
 | 
			
		||||
  public get_name() {
 | 
			
		||||
@@ -341,10 +332,10 @@ export class MouseEvent implements IEvent {
 | 
			
		||||
      this.y,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
  public static init(args: any[]): IEvent | undefined {
 | 
			
		||||
  public static init(args: unknown[]): IEvent | undefined {
 | 
			
		||||
    if (!(typeof args[0] === "string")) return undefined;
 | 
			
		||||
    let ev = new MouseEvent();
 | 
			
		||||
    const type = args[0] as string;
 | 
			
		||||
    const ev = new MouseEvent();
 | 
			
		||||
    const type = args[0];
 | 
			
		||||
    if (type == "mouse_click") {
 | 
			
		||||
      ev.type = MouseEventType.Click;
 | 
			
		||||
      ev.button = args[1] as number;
 | 
			
		||||
@@ -384,15 +375,14 @@ export class ResizeEvent implements IEvent {
 | 
			
		||||
  public get_args() {
 | 
			
		||||
    return [this.side];
 | 
			
		||||
  }
 | 
			
		||||
  public static init(args: any[]): IEvent | undefined {
 | 
			
		||||
  public static init(args: unknown[]): IEvent | undefined {
 | 
			
		||||
    if (
 | 
			
		||||
      !(typeof args[0] === "string") ||
 | 
			
		||||
      ((args[0] as string) != "term_resize" &&
 | 
			
		||||
        (args[0] as string) != "monitor_resize")
 | 
			
		||||
      (args[0] != "term_resize" && args[0] != "monitor_resize")
 | 
			
		||||
    )
 | 
			
		||||
      return undefined;
 | 
			
		||||
    let ev = new ResizeEvent();
 | 
			
		||||
    if ((args[0] as string) == "monitor_resize") ev.side = args[1] as string;
 | 
			
		||||
    const ev = new ResizeEvent();
 | 
			
		||||
    if (args[0] == "monitor_resize") ev.side = args[1] as string;
 | 
			
		||||
    else ev.side = undefined;
 | 
			
		||||
    return ev;
 | 
			
		||||
  }
 | 
			
		||||
@@ -405,13 +395,10 @@ export class TurtleInventoryEvent implements IEvent {
 | 
			
		||||
  public get_args() {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
  public static init(args: any[]): IEvent | undefined {
 | 
			
		||||
    if (
 | 
			
		||||
      !(typeof args[0] === "string") ||
 | 
			
		||||
      (args[0] as string) != "turtle_inventory"
 | 
			
		||||
    )
 | 
			
		||||
  public static init(args: unknown[]): IEvent | undefined {
 | 
			
		||||
    if (!(typeof args[0] === "string") || args[0] != "turtle_inventory")
 | 
			
		||||
      return undefined;
 | 
			
		||||
    let ev = new TurtleInventoryEvent();
 | 
			
		||||
    const ev = new TurtleInventoryEvent();
 | 
			
		||||
    return ev;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -465,11 +452,11 @@ class Event implements IEvent {
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
export class ChatBoxEvent implements IEvent {
 | 
			
		||||
  public username: string = "";
 | 
			
		||||
  public message: string = "";
 | 
			
		||||
  public uuid: string = "";
 | 
			
		||||
  public isHidden: boolean = false;
 | 
			
		||||
  public messageUtf8: string = "";
 | 
			
		||||
  public username = "";
 | 
			
		||||
  public message = "";
 | 
			
		||||
  public uuid = "";
 | 
			
		||||
  public isHidden = false;
 | 
			
		||||
  public messageUtf8 = "";
 | 
			
		||||
 | 
			
		||||
  public get_name() {
 | 
			
		||||
    return "chat";
 | 
			
		||||
@@ -483,10 +470,9 @@ export class ChatBoxEvent implements IEvent {
 | 
			
		||||
      this.messageUtf8,
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
  public static init(args: any[]): IEvent | undefined {
 | 
			
		||||
    if (!(typeof args[0] === "string") || (args[0] as string) != "chat")
 | 
			
		||||
      return undefined;
 | 
			
		||||
    let ev = new ChatBoxEvent();
 | 
			
		||||
  public static init(args: unknown[]): IEvent | undefined {
 | 
			
		||||
    if (!(typeof args[0] === "string") || args[0] != "chat") return undefined;
 | 
			
		||||
    const ev = new ChatBoxEvent();
 | 
			
		||||
    ev.username = args[1] as string;
 | 
			
		||||
    ev.message = args[2] as string;
 | 
			
		||||
    ev.uuid = args[3] as string;
 | 
			
		||||
@@ -497,21 +483,21 @@ export class ChatBoxEvent implements IEvent {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class GenericEvent implements IEvent {
 | 
			
		||||
  public args: any[] = [];
 | 
			
		||||
  public args: unknown[] = [];
 | 
			
		||||
  public get_name() {
 | 
			
		||||
    return this.args[0] as string;
 | 
			
		||||
  }
 | 
			
		||||
  public get_args() {
 | 
			
		||||
    return this.args.slice(1);
 | 
			
		||||
  }
 | 
			
		||||
  public static init(args: any[]): IEvent | undefined {
 | 
			
		||||
    let ev = new GenericEvent();
 | 
			
		||||
  public static init(args: unknown[]): IEvent | undefined {
 | 
			
		||||
    const ev = new GenericEvent();
 | 
			
		||||
    ev.args = args;
 | 
			
		||||
    return ev;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let eventInitializers: ((args: unknown[]) => IEvent | undefined)[] = [
 | 
			
		||||
const eventInitializers: ((args: unknown[]) => IEvent | undefined)[] = [
 | 
			
		||||
  (args) => CharEvent.init(args),
 | 
			
		||||
  (args) => KeyEvent.init(args),
 | 
			
		||||
  (args) => PasteEvent.init(args),
 | 
			
		||||
@@ -534,13 +520,13 @@ let eventInitializers: ((args: unknown[]) => IEvent | undefined)[] = [
 | 
			
		||||
  (args) => GenericEvent.init(args),
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
type Constructor<T extends {} = {}> = new (...args: any[]) => T;
 | 
			
		||||
type Constructor<T> = new (...args: unknown[]) => T;
 | 
			
		||||
export function pullEventRaw(
 | 
			
		||||
  filter: string | undefined = undefined,
 | 
			
		||||
): IEvent | undefined {
 | 
			
		||||
  let args = table.pack(...coroutine.yield(filter));
 | 
			
		||||
  for (let init of eventInitializers) {
 | 
			
		||||
    let ev = init(args);
 | 
			
		||||
  const args = table.pack(...coroutine.yield(filter));
 | 
			
		||||
  for (const init of eventInitializers) {
 | 
			
		||||
    const ev = init(args);
 | 
			
		||||
    if (ev != undefined) return ev;
 | 
			
		||||
  }
 | 
			
		||||
  return GenericEvent.init(args);
 | 
			
		||||
@@ -548,23 +534,23 @@ export function pullEventRaw(
 | 
			
		||||
export function pullEvent(
 | 
			
		||||
  filter: string | undefined = undefined,
 | 
			
		||||
): IEvent | undefined {
 | 
			
		||||
  let ev = pullEventRaw(filter);
 | 
			
		||||
  if (ev instanceof TerminateEvent) throw "Terminated";
 | 
			
		||||
  const ev = pullEventRaw(filter);
 | 
			
		||||
  if (ev instanceof TerminateEvent) throw Error("Terminated");
 | 
			
		||||
  return ev;
 | 
			
		||||
}
 | 
			
		||||
export function pullEventRawAs<T extends IEvent>(
 | 
			
		||||
  type: Constructor<T>,
 | 
			
		||||
  filter: string | undefined = undefined,
 | 
			
		||||
): T | undefined {
 | 
			
		||||
  let ev = pullEventRaw(filter);
 | 
			
		||||
  if (ev instanceof type) return ev as T;
 | 
			
		||||
  const ev = pullEventRaw(filter);
 | 
			
		||||
  if (ev instanceof type) return ev;
 | 
			
		||||
  else return undefined;
 | 
			
		||||
}
 | 
			
		||||
export function pullEventAs<T extends IEvent>(
 | 
			
		||||
  type: Constructor<T>,
 | 
			
		||||
  filter: string | undefined = undefined,
 | 
			
		||||
): T | undefined {
 | 
			
		||||
  let ev = pullEvent(filter);
 | 
			
		||||
  if (ev instanceof type) return ev as T;
 | 
			
		||||
  const ev = pullEvent(filter);
 | 
			
		||||
  if (ev instanceof type) return ev;
 | 
			
		||||
  else return undefined;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,126 +1,47 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Example usage of the ComputerCraft TUI framework
 | 
			
		||||
 * Example using the new ccTUI framework with reactive components
 | 
			
		||||
 * Demonstrates the SolidJS-inspired API
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  TUIApplication,
 | 
			
		||||
  TextLabel,
 | 
			
		||||
  InputField,
 | 
			
		||||
  OptionSelector,
 | 
			
		||||
  TabBar,
 | 
			
		||||
  Button,
 | 
			
		||||
  createSignal,
 | 
			
		||||
  div,
 | 
			
		||||
  h3,
 | 
			
		||||
  label,
 | 
			
		||||
  button,
 | 
			
		||||
  render,
 | 
			
		||||
} from "../lib/ccTUI";
 | 
			
		||||
 | 
			
		||||
// Create the main application
 | 
			
		||||
const app = new TUIApplication();
 | 
			
		||||
 | 
			
		||||
// Get terminal size
 | 
			
		||||
const [termWidth, _termHeight] = term.getSize();
 | 
			
		||||
 | 
			
		||||
// Create UI components
 | 
			
		||||
const title = new TextLabel(
 | 
			
		||||
  "LabelTitle",
 | 
			
		||||
  Math.floor(termWidth / 2) - 10,
 | 
			
		||||
  2,
 | 
			
		||||
  "CC TUI Framework Demo",
 | 
			
		||||
  colors.yellow,
 | 
			
		||||
  colors.black,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const label1 = new TextLabel("Label1", 5, 5, "Enter your name:");
 | 
			
		||||
 | 
			
		||||
const inputField = new InputField("LabelInput", 5, 6, 30, "", "Type here...");
 | 
			
		||||
 | 
			
		||||
const optionLabel = new TextLabel("LableOption", 5, 8, "Select an option:");
 | 
			
		||||
 | 
			
		||||
const options = ["Option 1", "Option 2", "Option 3", "Option 4"];
 | 
			
		||||
const optionSelector = new OptionSelector(
 | 
			
		||||
  "OptionSelector",
 | 
			
		||||
  5,
 | 
			
		||||
  9,
 | 
			
		||||
  options,
 | 
			
		||||
  "Choose:",
 | 
			
		||||
  0,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const statusLabel = new TextLabel("LableStatus", 5, 11, "Status: Ready");
 | 
			
		||||
 | 
			
		||||
// Create a button
 | 
			
		||||
const button = new Button(
 | 
			
		||||
  "ButtonSubmit",
 | 
			
		||||
  5,
 | 
			
		||||
  13,
 | 
			
		||||
  "Submit",
 | 
			
		||||
  colors.white,
 | 
			
		||||
  colors.blue,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// Create tab widget with sample tabs - using longer tab names for testing
 | 
			
		||||
const tabNames = [
 | 
			
		||||
  "Home",
 | 
			
		||||
  "Settings",
 | 
			
		||||
  "User Profile",
 | 
			
		||||
  "Messages",
 | 
			
		||||
  "About Us",
 | 
			
		||||
  "Documentation",
 | 
			
		||||
  "Advanced Settings",
 | 
			
		||||
  "Account Management",
 | 
			
		||||
];
 | 
			
		||||
const tabBar = new TabBar("TabWidget", 5, 3, 50, tabNames, 0);
 | 
			
		||||
 | 
			
		||||
// Add components to the application
 | 
			
		||||
app.addComponent(title);
 | 
			
		||||
app.addComponent(label1);
 | 
			
		||||
app.addComponent(inputField);
 | 
			
		||||
app.addComponent(optionLabel);
 | 
			
		||||
app.addComponent(optionSelector);
 | 
			
		||||
app.addComponent(statusLabel);
 | 
			
		||||
app.addComponent(button);
 | 
			
		||||
app.addComponent(tabBar);
 | 
			
		||||
 | 
			
		||||
// Set focus to the input field initially
 | 
			
		||||
app.getWindow().setFocusFor(tabBar);
 | 
			
		||||
 | 
			
		||||
// Connect events
 | 
			
		||||
optionSelector.onSelectionChanged.connect((data) => {
 | 
			
		||||
  statusLabel.setText(
 | 
			
		||||
    `Status: Selected ${data?.value} (index: ${data?.index})`,
 | 
			
		||||
/**
 | 
			
		||||
 * Simple counter example
 | 
			
		||||
 */
 | 
			
		||||
const CounterApp = () => {
 | 
			
		||||
  const [count, setCount] = createSignal(0);
 | 
			
		||||
  
 | 
			
		||||
  return div({ class: "flex flex-col" },
 | 
			
		||||
    h3("Counter Example"),
 | 
			
		||||
    label({}, () => `Count: ${count()}`),
 | 
			
		||||
    div({ class: "flex flex-row" },
 | 
			
		||||
      button({ onClick: () => setCount(count() - 1) }, "-"),
 | 
			
		||||
      button({ onClick: () => setCount(count() + 1) }, "+")
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
inputField.onTextChanged.connect((value) => {
 | 
			
		||||
  if (value != undefined && value.length > 0) {
 | 
			
		||||
    statusLabel.setText(`Status: Input changed to "${value}"`);
 | 
			
		||||
  } else {
 | 
			
		||||
    statusLabel.setText("Status: Input cleared");
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tabBar.onTabChanged.connect((data) => {
 | 
			
		||||
  statusLabel.setText(
 | 
			
		||||
    `Status: Tab changed to ${data?.name} (index: ${data?.index})`,
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
button.onClick.connect(() => {
 | 
			
		||||
  const inputValue = inputField.getValue();
 | 
			
		||||
  const selectedOption = optionSelector.getSelectedValue();
 | 
			
		||||
  statusLabel.setText(
 | 
			
		||||
    `Status: Submitted - Input: "${inputValue}", Option: "${selectedOption}"`,
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Run the application
 | 
			
		||||
/**
 | 
			
		||||
 * Main entry point
 | 
			
		||||
 */
 | 
			
		||||
try {
 | 
			
		||||
  print("Starting CC TUI Demo. Press Ctrl+T to quit.");
 | 
			
		||||
  app.run();
 | 
			
		||||
  print("Starting ccTUI Reactive Demo. Press Ctrl+T to quit.");
 | 
			
		||||
  
 | 
			
		||||
  // Render the application
 | 
			
		||||
  render(CounterApp);
 | 
			
		||||
  
 | 
			
		||||
} catch (e) {
 | 
			
		||||
  if (e === "Terminated") {
 | 
			
		||||
    print("Application terminated by user.");
 | 
			
		||||
  } else {
 | 
			
		||||
    print(`Error running application:`);
 | 
			
		||||
    print("Error running application:");
 | 
			
		||||
    printError(e);
 | 
			
		||||
  }
 | 
			
		||||
} finally {
 | 
			
		||||
  app.stop();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,5 +5,5 @@
 | 
			
		||||
    "luaBundle": "build/tuiExample.lua",
 | 
			
		||||
    "luaBundleEntry": "src/tuiExample/main.ts"
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src/tuiExample/*.ts"]
 | 
			
		||||
  "include": ["src/tuiExample/*.ts", "src/lib/ccTUI/*.ts"]
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user