mirror of
				https://github.com/SikongJueluo/cc-utils.git
				synced 2025-11-04 11:17:50 +08:00 
			
		
		
		
	move package and add tab component for tui famework
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -3,6 +3,7 @@ node_modules
 | 
			
		||||
event/
 | 
			
		||||
build/
 | 
			
		||||
reference/
 | 
			
		||||
src/*/*.md
 | 
			
		||||
 | 
			
		||||
QWEN.md
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,9 @@
 | 
			
		||||
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
 | 
			
		||||
sync-path := if os_family() == "windows" {
 | 
			
		||||
    "C:\\Users\\sikongjueluo\\AppData\\Roaming\\CraftOS-PC\\computer\\0\\user\\"
 | 
			
		||||
} else {
 | 
			
		||||
    "/home/sikongjueluo/.local/share/craftos-pc/computer/0/user/"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
build: build-autocraft build-accesscontrol build-test build-example sync
 | 
			
		||||
 | 
			
		||||
@@ -16,4 +21,4 @@ build-example:
 | 
			
		||||
    pnpm tstl -p ./tsconfig.tuiExample.json
 | 
			
		||||
 | 
			
		||||
sync:
 | 
			
		||||
    cp -r "./build/*" "C:\\Users\\sikongjueluo\\AppData\\Roaming\\CraftOS-PC\\computer\\0\\user\\"
 | 
			
		||||
    rsync --delete -r "./build/" "{{sync-path}}"
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@
 | 
			
		||||
{
 | 
			
		||||
  packages = with pkgs; [
 | 
			
		||||
    pnpm
 | 
			
		||||
    craftos-pc
 | 
			
		||||
    qwen-code
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  # https://devenv.sh/languages/
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,8 @@
 | 
			
		||||
    "@jackmacwindows/lua-types": "^2.13.2",
 | 
			
		||||
    "@jackmacwindows/typescript-to-lua": "^1.28.1",
 | 
			
		||||
    "@sikongjueluo/advanced-peripherals-types": "file:types/advanced-peripherals",
 | 
			
		||||
    "@sikongjueluo/toml2lua-types": "file:types/toml2lua",
 | 
			
		||||
    "@sikongjueluo/dkjson-types": "file:types/dkjson",
 | 
			
		||||
    "@sikongjueluo/dkjson-types": "file:thirdparty/dkjson",
 | 
			
		||||
    "@sikongjueluo/toml2lua-types": "file:thirdparty/toml2lua",
 | 
			
		||||
    "@typescript-to-lua/language-extensions": "^1.19.0",
 | 
			
		||||
    "eslint": "^9.36.0",
 | 
			
		||||
    "typescript": "^5.7.2",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										20
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -27,11 +27,11 @@ importers:
 | 
			
		||||
        specifier: file:types/advanced-peripherals
 | 
			
		||||
        version: file:types/advanced-peripherals
 | 
			
		||||
      '@sikongjueluo/dkjson-types':
 | 
			
		||||
        specifier: file:types/dkjson
 | 
			
		||||
        version: file:types/dkjson
 | 
			
		||||
        specifier: file:thirdparty/dkjson
 | 
			
		||||
        version: file:thirdparty/dkjson
 | 
			
		||||
      '@sikongjueluo/toml2lua-types':
 | 
			
		||||
        specifier: file:types/toml2lua
 | 
			
		||||
        version: file:types/toml2lua
 | 
			
		||||
        specifier: file:thirdparty/toml2lua
 | 
			
		||||
        version: file:thirdparty/toml2lua
 | 
			
		||||
      '@typescript-to-lua/language-extensions':
 | 
			
		||||
        specifier: ^1.19.0
 | 
			
		||||
        version: 1.19.0
 | 
			
		||||
@@ -132,11 +132,11 @@ packages:
 | 
			
		||||
  '@sikongjueluo/advanced-peripherals-types@file:types/advanced-peripherals':
 | 
			
		||||
    resolution: {directory: types/advanced-peripherals, type: directory}
 | 
			
		||||
 | 
			
		||||
  '@sikongjueluo/dkjson-types@file:types/dkjson':
 | 
			
		||||
    resolution: {directory: types/dkjson, type: directory}
 | 
			
		||||
  '@sikongjueluo/dkjson-types@file:thirdparty/dkjson':
 | 
			
		||||
    resolution: {directory: thirdparty/dkjson, type: directory}
 | 
			
		||||
 | 
			
		||||
  '@sikongjueluo/toml2lua-types@file:types/toml2lua':
 | 
			
		||||
    resolution: {directory: types/toml2lua, type: directory}
 | 
			
		||||
  '@sikongjueluo/toml2lua-types@file:thirdparty/toml2lua':
 | 
			
		||||
    resolution: {directory: thirdparty/toml2lua, type: directory}
 | 
			
		||||
 | 
			
		||||
  '@types/estree@1.0.8':
 | 
			
		||||
    resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
 | 
			
		||||
@@ -689,9 +689,9 @@ snapshots:
 | 
			
		||||
 | 
			
		||||
  '@sikongjueluo/advanced-peripherals-types@file:types/advanced-peripherals': {}
 | 
			
		||||
 | 
			
		||||
  '@sikongjueluo/dkjson-types@file:types/dkjson': {}
 | 
			
		||||
  '@sikongjueluo/dkjson-types@file:thirdparty/dkjson': {}
 | 
			
		||||
 | 
			
		||||
  '@sikongjueluo/toml2lua-types@file:types/toml2lua': {}
 | 
			
		||||
  '@sikongjueluo/toml2lua-types@file:thirdparty/toml2lua': {}
 | 
			
		||||
 | 
			
		||||
  '@types/estree@1.0.8': {}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										538
									
								
								src/lib/ccTUI.ts
									
									
									
									
									
								
							
							
						
						
									
										538
									
								
								src/lib/ccTUI.ts
									
									
									
									
									
								
							@@ -5,7 +5,7 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// Import required types from the ComputerCraft environment
 | 
			
		||||
import { CharEvent, KeyEvent, pullEventAs } from "./event";
 | 
			
		||||
import { CharEvent, KeyEvent, pullEventAs, TimerEvent } from "./event";
 | 
			
		||||
import { CCLog, DAY } from "./ccLog";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -38,24 +38,34 @@ class Signal<T = void> {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
abstract class UIObject {
 | 
			
		||||
  private objectName: string;
 | 
			
		||||
  readonly objectName: string;
 | 
			
		||||
  private parent?: UIObject;
 | 
			
		||||
  private children: Record<string, UIObject> = {};
 | 
			
		||||
  private log?: CCLog;
 | 
			
		||||
 | 
			
		||||
  constructor(name: string) {
 | 
			
		||||
  public log?: CCLog;
 | 
			
		||||
 | 
			
		||||
  constructor(name: string, parent?: UIObject, log?: CCLog) {
 | 
			
		||||
    this.objectName = name;
 | 
			
		||||
    this.parent = parent;
 | 
			
		||||
    this.log = log;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setParent(parent: UIObject) {
 | 
			
		||||
    this.parent = parent;
 | 
			
		||||
    this.log ??= parent.log;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public addChild(child: UIObject) {
 | 
			
		||||
    this.children[child.objectName] = child;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public removeChild(child: UIObject) {}
 | 
			
		||||
  public removeChild(child: UIObject) {
 | 
			
		||||
    Object.entries(this.children).forEach(([key, value]) => {
 | 
			
		||||
      if (value === child) {
 | 
			
		||||
        delete this.children[key];
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -75,7 +85,8 @@ abstract class UIComponent extends UIObject {
 | 
			
		||||
  public onKeyPress = new Signal<KeyEvent>();
 | 
			
		||||
  public onMouseClick = new Signal<{ x: number; y: number }>();
 | 
			
		||||
 | 
			
		||||
  constructor(x: number, y: number, width = 0, height = 0) {
 | 
			
		||||
  constructor(objectName: string, x: number, y: number, width = 0, height = 0) {
 | 
			
		||||
    super(objectName);
 | 
			
		||||
    this.x = x;
 | 
			
		||||
    this.y = y;
 | 
			
		||||
    this.width = width;
 | 
			
		||||
@@ -85,11 +96,20 @@ abstract class UIComponent extends UIObject {
 | 
			
		||||
  // Render the component to the terminal
 | 
			
		||||
  abstract render(): void;
 | 
			
		||||
 | 
			
		||||
  // Handle input events
 | 
			
		||||
  // Handle events
 | 
			
		||||
  // Key
 | 
			
		||||
  abstract handleKeyInput(event: KeyEvent): void;
 | 
			
		||||
  handleKeyInput(_event: KeyEvent): void {
 | 
			
		||||
    // Do nothing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Char
 | 
			
		||||
  abstract handleCharInput(event: CharEvent): void;
 | 
			
		||||
  handleCharInput(_event: CharEvent): void {
 | 
			
		||||
    // Do nothing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleTimerTrigger(_event: TimerEvent): void {
 | 
			
		||||
    // Do nothing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get/set focus for the component
 | 
			
		||||
  focus(): void {
 | 
			
		||||
@@ -151,13 +171,14 @@ class TextLabel extends UIComponent {
 | 
			
		||||
  private bgColor: number;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    objectName: string,
 | 
			
		||||
    x: number,
 | 
			
		||||
    y: number,
 | 
			
		||||
    text: string,
 | 
			
		||||
    textColor: number = colors.white,
 | 
			
		||||
    bgColor: number = colors.black,
 | 
			
		||||
  ) {
 | 
			
		||||
    super(x, y, text.length, 1);
 | 
			
		||||
    super(objectName, x, y, text.length, 1);
 | 
			
		||||
    this.text = text;
 | 
			
		||||
    this.textColor = textColor;
 | 
			
		||||
    this.bgColor = bgColor;
 | 
			
		||||
@@ -180,14 +201,6 @@ class TextLabel extends UIComponent {
 | 
			
		||||
    term.setCursorPos(originalX, originalY);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleKeyInput(_event: KeyEvent): void {
 | 
			
		||||
    // Do nothing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleCharInput(_event: CharEvent): void {
 | 
			
		||||
    // Do nothing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setText(newText: string): void {
 | 
			
		||||
    this.text = newText;
 | 
			
		||||
    this.width = newText.length; // Update width based on new text
 | 
			
		||||
@@ -213,6 +226,7 @@ class InputField extends UIComponent {
 | 
			
		||||
  public onTextChanged = new Signal<string>();
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    objectName: string,
 | 
			
		||||
    x: number,
 | 
			
		||||
    y: number,
 | 
			
		||||
    width: number,
 | 
			
		||||
@@ -221,7 +235,7 @@ class InputField extends UIComponent {
 | 
			
		||||
    maxLength = 50,
 | 
			
		||||
    password = false,
 | 
			
		||||
  ) {
 | 
			
		||||
    super(x, y, width, 1);
 | 
			
		||||
    super(objectName, x, y, width, 1);
 | 
			
		||||
    this.value = value;
 | 
			
		||||
    this.placeholder = placeholder;
 | 
			
		||||
    this.maxLength = maxLength;
 | 
			
		||||
@@ -286,7 +300,7 @@ class InputField extends UIComponent {
 | 
			
		||||
    this.onKeyPress.emit(event);
 | 
			
		||||
 | 
			
		||||
    const key = event.key;
 | 
			
		||||
    log.debug(`[${InputField.name}]: Get key ${keys.getName(key)}`);
 | 
			
		||||
    this.log?.debug(`[${InputField.name}]: Get key ${keys.getName(key)}`);
 | 
			
		||||
 | 
			
		||||
    // Handle backspace
 | 
			
		||||
    if (key === keys.backspace) {
 | 
			
		||||
@@ -334,7 +348,7 @@ class InputField extends UIComponent {
 | 
			
		||||
    if (!this.focused) return;
 | 
			
		||||
 | 
			
		||||
    const character = event.character;
 | 
			
		||||
    log.debug(`[${InputField.name}]: Get character ${character}`);
 | 
			
		||||
    this.log?.debug(`[${InputField.name}]: Get character ${character}`);
 | 
			
		||||
 | 
			
		||||
    this.value += character;
 | 
			
		||||
    this.cursorPos++;
 | 
			
		||||
@@ -385,13 +399,14 @@ class OptionSelector extends UIComponent {
 | 
			
		||||
  public onSelectionChanged = new Signal<{ index: number; value: string }>();
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    objectName: string,
 | 
			
		||||
    x: number,
 | 
			
		||||
    y: number,
 | 
			
		||||
    options: string[],
 | 
			
		||||
    prompt = "Select:",
 | 
			
		||||
    initialIndex = 0,
 | 
			
		||||
  ) {
 | 
			
		||||
    super(x, y, 0, 1); // Width will be calculated dynamically
 | 
			
		||||
    super(objectName, x, y, 0, 1); // Width will be calculated dynamically
 | 
			
		||||
    this.options = options;
 | 
			
		||||
    this.currentIndex = initialIndex;
 | 
			
		||||
    this.prompt = prompt;
 | 
			
		||||
@@ -454,10 +469,6 @@ class OptionSelector extends UIComponent {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleCharInput(_event: CharEvent): void {
 | 
			
		||||
    //Do nothing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private previousOption(): void {
 | 
			
		||||
    this.currentIndex =
 | 
			
		||||
      (this.currentIndex - 1 + this.options.length) % this.options.length;
 | 
			
		||||
@@ -509,6 +520,430 @@ class OptionSelector extends UIComponent {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Tab component that allows switching between different pages
 | 
			
		||||
 * Similar to QT's TabWidget, currently implementing horizontal tabs only
 | 
			
		||||
 */
 | 
			
		||||
class TabWidget 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();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base Window class to manage UI components
 | 
			
		||||
 */
 | 
			
		||||
@@ -516,7 +951,7 @@ class UIWindow {
 | 
			
		||||
  private components: UIComponent[] = [];
 | 
			
		||||
  private focusedComponentIndex = -1;
 | 
			
		||||
 | 
			
		||||
  addComponent(component: UIComponent, manager: GlobalManager): void {
 | 
			
		||||
  addComponent(component: UIComponent): void {
 | 
			
		||||
    this.components.push(component);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -566,6 +1001,12 @@ class UIWindow {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleTimerTrigger(event: TimerEvent) {
 | 
			
		||||
    for (const component of this.components) {
 | 
			
		||||
      component.handleTimerTrigger(event);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setFocus(index: number): void {
 | 
			
		||||
    // Unfocus current component
 | 
			
		||||
    if (
 | 
			
		||||
@@ -613,22 +1054,16 @@ class UIWindow {
 | 
			
		||||
 */
 | 
			
		||||
class TUIApplication {
 | 
			
		||||
  private log = new CCLog(`TUI.log`, false, DAY);
 | 
			
		||||
  private manager: GlobalManager;
 | 
			
		||||
 | 
			
		||||
  private window: UIWindow;
 | 
			
		||||
  private running = false;
 | 
			
		||||
  private keyEvent?: KeyEvent;
 | 
			
		||||
  private charEvent?: CharEvent;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.window = new UIWindow();
 | 
			
		||||
    this.manager = {
 | 
			
		||||
      log: this.log,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addComponent(component: UIComponent): void {
 | 
			
		||||
    this.window.addComponent(component, this.manager);
 | 
			
		||||
    this.window.addComponent(component);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  run(): void {
 | 
			
		||||
@@ -653,7 +1088,7 @@ class TUIApplication {
 | 
			
		||||
 | 
			
		||||
  stop(): void {
 | 
			
		||||
    this.running = false;
 | 
			
		||||
    this.manager.log.close();
 | 
			
		||||
    this.log.close();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  mainLoop(): void {
 | 
			
		||||
@@ -670,24 +1105,36 @@ class TUIApplication {
 | 
			
		||||
  keyLoop(): void {
 | 
			
		||||
    while (this.running) {
 | 
			
		||||
      // Handle input events
 | 
			
		||||
      this.keyEvent = pullEventAs(KeyEvent, "key");
 | 
			
		||||
      this.manager.log.debug(
 | 
			
		||||
        `[${TUIApplication.name}]: Get Key Event: ${textutils.serialise(this.keyEvent ?? {})}`,
 | 
			
		||||
      const keyEvent = pullEventAs(KeyEvent, "key");
 | 
			
		||||
      this.log.debug(
 | 
			
		||||
        `[${TUIApplication.name}]: Get Key Event: ${textutils.serialise(keyEvent ?? {})}`,
 | 
			
		||||
      );
 | 
			
		||||
      if (this.keyEvent == undefined) continue;
 | 
			
		||||
      this.window.handleKeyInput(this.keyEvent);
 | 
			
		||||
      if (keyEvent == undefined) continue;
 | 
			
		||||
      this.window.handleKeyInput(keyEvent);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  charLoop(): void {
 | 
			
		||||
    while (this.running) {
 | 
			
		||||
      // Handle input events
 | 
			
		||||
      this.charEvent = pullEventAs(CharEvent, "char");
 | 
			
		||||
      this.manager.log.debug(
 | 
			
		||||
        `[${TUIApplication.name}]: Get Char Event: ${textutils.serialise(this.charEvent ?? {})}`,
 | 
			
		||||
      const charEvent = pullEventAs(CharEvent, "char");
 | 
			
		||||
      this.log.debug(
 | 
			
		||||
        `[${TUIApplication.name}]: Get Char Event: ${textutils.serialise(charEvent ?? {})}`,
 | 
			
		||||
      );
 | 
			
		||||
      if (this.charEvent == undefined) continue;
 | 
			
		||||
      this.window.handleCharInput(this.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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -703,6 +1150,7 @@ export {
 | 
			
		||||
  TextLabel,
 | 
			
		||||
  InputField,
 | 
			
		||||
  OptionSelector,
 | 
			
		||||
  TabWidget,
 | 
			
		||||
  UIWindow,
 | 
			
		||||
  TUIApplication,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import {
 | 
			
		||||
  TextLabel,
 | 
			
		||||
  InputField,
 | 
			
		||||
  OptionSelector,
 | 
			
		||||
  TabWidget,
 | 
			
		||||
} from "../lib/ccTUI";
 | 
			
		||||
 | 
			
		||||
// Create the main application
 | 
			
		||||
@@ -17,6 +18,7 @@ const [termWidth, _termHeight] = term.getSize();
 | 
			
		||||
 | 
			
		||||
// Create UI components
 | 
			
		||||
const title = new TextLabel(
 | 
			
		||||
  "LabelTitle",
 | 
			
		||||
  Math.floor(termWidth / 2) - 10,
 | 
			
		||||
  2,
 | 
			
		||||
  "CC TUI Framework Demo",
 | 
			
		||||
@@ -24,16 +26,27 @@ const title = new TextLabel(
 | 
			
		||||
  colors.black,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const label1 = new TextLabel(5, 5, "Enter your name:");
 | 
			
		||||
const label1 = new TextLabel("Label1", 5, 5, "Enter your name:");
 | 
			
		||||
 | 
			
		||||
const inputField = new InputField(5, 6, 30, "", "Type here...");
 | 
			
		||||
const inputField = new InputField("LabelInput", 5, 6, 30, "", "Type here...");
 | 
			
		||||
 | 
			
		||||
const optionLabel = new TextLabel(5, 8, "Select an option:");
 | 
			
		||||
const optionLabel = new TextLabel("LableOption", 5, 8, "Select an option:");
 | 
			
		||||
 | 
			
		||||
const options = ["Option 1", "Option 2", "Option 3", "Option 4"];
 | 
			
		||||
const optionSelector = new OptionSelector(5, 9, options, "Choose:", 0);
 | 
			
		||||
const optionSelector = new OptionSelector(
 | 
			
		||||
  "OptionSelector",
 | 
			
		||||
  5,
 | 
			
		||||
  9,
 | 
			
		||||
  options,
 | 
			
		||||
  "Choose:",
 | 
			
		||||
  0,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const statusLabel = new TextLabel(5, 11, "Status: Ready");
 | 
			
		||||
const statusLabel = new TextLabel("LableStatus", 5, 11, "Status: Ready");
 | 
			
		||||
 | 
			
		||||
// 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 tabWidget = new TabWidget("TabWidget", 5, 3, 50, tabNames, 0);
 | 
			
		||||
 | 
			
		||||
// Add components to the application
 | 
			
		||||
app.addComponent(title);
 | 
			
		||||
@@ -42,9 +55,10 @@ app.addComponent(inputField);
 | 
			
		||||
app.addComponent(optionLabel);
 | 
			
		||||
app.addComponent(optionSelector);
 | 
			
		||||
app.addComponent(statusLabel);
 | 
			
		||||
app.addComponent(tabWidget);
 | 
			
		||||
 | 
			
		||||
// Set focus to the input field initially
 | 
			
		||||
app.getWindow().setFocusFor(optionSelector);
 | 
			
		||||
app.getWindow().setFocusFor(tabWidget);
 | 
			
		||||
 | 
			
		||||
// Connect events
 | 
			
		||||
optionSelector.onSelectionChanged.connect((data) => {
 | 
			
		||||
@@ -61,6 +75,12 @@ inputField.onTextChanged.connect((value) => {
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tabWidget.onTabChanged.connect((data) => {
 | 
			
		||||
  statusLabel.setText(
 | 
			
		||||
    `Status: Tab changed to ${data?.name} (index: ${data?.index})`,
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Run the application
 | 
			
		||||
try {
 | 
			
		||||
  print("Starting CC TUI Demo. Press Ctrl+T to quit.");
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,14 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "@jackmacwindows/cc-types",
 | 
			
		||||
    "version": "1.0.1",
 | 
			
		||||
    "description": "TypeScript type definitions for CraftOS modules.",
 | 
			
		||||
    "types": "index.d.ts",
 | 
			
		||||
    "files": ["./*.d.ts", "./audio", "./image", "./shell"],
 | 
			
		||||
    "author": "JackMacWindows",
 | 
			
		||||
    "license": "MIT"
 | 
			
		||||
}
 | 
			
		||||
  "name": "@jackmacwindows/cc-types",
 | 
			
		||||
  "version": "1.0.1",
 | 
			
		||||
  "description": "TypeScript type definitions for CraftOS modules.",
 | 
			
		||||
  "types": "index.d.ts",
 | 
			
		||||
  "files": [
 | 
			
		||||
    "./*.d.ts",
 | 
			
		||||
    "./audio",
 | 
			
		||||
    "./image",
 | 
			
		||||
    "./shell"
 | 
			
		||||
  ],
 | 
			
		||||
  "author": "JackMacWindows",
 | 
			
		||||
  "license": "MIT"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user