mirror of
				https://github.com/SikongJueluo/cc-utils.git
				synced 2025-11-04 11:17:50 +08:00 
			
		
		
		
	update and add tui for accesscontrol
This commit is contained in:
		@@ -3,10 +3,10 @@
 | 
			
		||||
    "devenv": {
 | 
			
		||||
      "locked": {
 | 
			
		||||
        "dir": "src/modules",
 | 
			
		||||
        "lastModified": 1759939975,
 | 
			
		||||
        "lastModified": 1760162706,
 | 
			
		||||
        "owner": "cachix",
 | 
			
		||||
        "repo": "devenv",
 | 
			
		||||
        "rev": "6eda3b7af3010d289e6e8e047435956fc80c1395",
 | 
			
		||||
        "rev": "0d5ad578728fe4bce66eb4398b8b1e66deceb4e4",
 | 
			
		||||
        "type": "github"
 | 
			
		||||
      },
 | 
			
		||||
      "original": {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								devenv.nix
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								devenv.nix
									
									
									
									
									
								
							@@ -1,11 +1,13 @@
 | 
			
		||||
{ pkgs, lib, config, inputs, ... }:
 | 
			
		||||
 | 
			
		||||
{
 | 
			
		||||
  pkgs,
 | 
			
		||||
  lib,
 | 
			
		||||
  config,
 | 
			
		||||
  inputs,
 | 
			
		||||
  ...
 | 
			
		||||
}: {
 | 
			
		||||
  packages = with pkgs; [
 | 
			
		||||
    pnpm
 | 
			
		||||
    craftos-pc
 | 
			
		||||
    qwen-code
 | 
			
		||||
    gemini-cli
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  # https://devenv.sh/languages/
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,8 @@
 | 
			
		||||
inputs:
 | 
			
		||||
  nixpkgs:
 | 
			
		||||
    url: github:cachix/devenv-nixpkgs/rolling
 | 
			
		||||
 | 
			
		||||
# If you're using non-OSS software, you can set allowUnfree to true.
 | 
			
		||||
# allowUnfree: true
 | 
			
		||||
 | 
			
		||||
allowUnfree: true
 | 
			
		||||
# If you're willing to use a package that's vulnerable
 | 
			
		||||
# permittedInsecurePackages:
 | 
			
		||||
#  - "openssl-1.1.1w"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										110
									
								
								src/accesscontrol/logviewer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/accesscontrol/logviewer.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Access Control Log Viewer
 | 
			
		||||
 * Simple log viewer that allows launching the TUI with 'c' key
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { launchAccessControlTUI } from "./tui";
 | 
			
		||||
 | 
			
		||||
const args = [...$vararg];
 | 
			
		||||
 | 
			
		||||
function displayLog(filepath: string, lines = 20) {
 | 
			
		||||
  const [file] = io.open(filepath, "r");
 | 
			
		||||
  if (!file) {
 | 
			
		||||
    print(`Failed to open log file: ${filepath}`);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const content = file.read("*a");
 | 
			
		||||
  file.close();
 | 
			
		||||
 | 
			
		||||
  if (content === null || content === undefined || content === "") {
 | 
			
		||||
    print("Log file is empty");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const logLines = content.split("\n");
 | 
			
		||||
  const startIndex = Math.max(0, logLines.length - lines);
 | 
			
		||||
  const displayLines = logLines.slice(startIndex);
 | 
			
		||||
 | 
			
		||||
  term.clear();
 | 
			
		||||
  term.setCursorPos(1, 1);
 | 
			
		||||
 | 
			
		||||
  print("=== Access Control Log Viewer ===");
 | 
			
		||||
  print("Press 'c' to open configuration TUI, 'q' to quit, 'r' to refresh");
 | 
			
		||||
  print("==========================================");
 | 
			
		||||
  print("");
 | 
			
		||||
 | 
			
		||||
  for (const line of displayLines) {
 | 
			
		||||
    if (line.trim() !== "") {
 | 
			
		||||
      print(line);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  print("");
 | 
			
		||||
  print("==========================================");
 | 
			
		||||
  print(`Showing last ${displayLines.length} lines of ${filepath}`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function main(args: string[]) {
 | 
			
		||||
  const logFilepath = args[0] || `${shell.dir()}/accesscontrol.log`;
 | 
			
		||||
  const lines = args[1] ? parseInt(args[1]) : 20;
 | 
			
		||||
 | 
			
		||||
  if (isNaN(lines) || lines <= 0) {
 | 
			
		||||
    print("Usage: logviewer [logfile] [lines]");
 | 
			
		||||
    print("  logfile - Path to log file (default: accesscontrol.log)");
 | 
			
		||||
    print("  lines   - Number of lines to display (default: 20)");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let running = true;
 | 
			
		||||
 | 
			
		||||
  // Initial display
 | 
			
		||||
  displayLog(logFilepath, lines);
 | 
			
		||||
 | 
			
		||||
  while (running) {
 | 
			
		||||
    const [eventType, key] = os.pullEvent();
 | 
			
		||||
 | 
			
		||||
    if (eventType === "key") {
 | 
			
		||||
      if (key === keys.c) {
 | 
			
		||||
        // Launch TUI
 | 
			
		||||
        print("Launching Access Control TUI...");
 | 
			
		||||
        try {
 | 
			
		||||
          launchAccessControlTUI();
 | 
			
		||||
          // Refresh display after TUI closes
 | 
			
		||||
          displayLog(logFilepath, lines);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          if (error === "TUI_CLOSE" || error === "Terminated") {
 | 
			
		||||
            displayLog(logFilepath, lines);
 | 
			
		||||
          } else {
 | 
			
		||||
            print(`TUI error: ${String(error)}`);
 | 
			
		||||
            os.sleep(2);
 | 
			
		||||
            displayLog(logFilepath, lines);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } else if (key === keys.q) {
 | 
			
		||||
        // Quit
 | 
			
		||||
        running = false;
 | 
			
		||||
      } else if (key === keys.r) {
 | 
			
		||||
        // Refresh
 | 
			
		||||
        displayLog(logFilepath, lines);
 | 
			
		||||
      }
 | 
			
		||||
    } else if (eventType === "terminate") {
 | 
			
		||||
      running = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  term.clear();
 | 
			
		||||
  term.setCursorPos(1, 1);
 | 
			
		||||
  print("Log viewer closed.");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
  main(args);
 | 
			
		||||
} catch (error) {
 | 
			
		||||
  if (error === "Terminated") {
 | 
			
		||||
    print("Log viewer terminated by user.");
 | 
			
		||||
  } else {
 | 
			
		||||
    print("Error in log viewer:");
 | 
			
		||||
    printError(error);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +1,14 @@
 | 
			
		||||
import { CCLog, DAY } from "@/lib/ccLog";
 | 
			
		||||
import { ToastConfig, UserGroupConfig, loadConfig, setLog } from "./config";
 | 
			
		||||
import { createAccessControlCLI } from "./cli";
 | 
			
		||||
import { launchAccessControlTUI } from "./tui";
 | 
			
		||||
import * as peripheralManager from "../lib/PeripheralManager";
 | 
			
		||||
 | 
			
		||||
const DEBUG = false;
 | 
			
		||||
const args = [...$vararg];
 | 
			
		||||
 | 
			
		||||
// Init Log
 | 
			
		||||
const log = new CCLog("accesscontrol.log", DAY);
 | 
			
		||||
const log = new CCLog("accesscontrol.log", true, DAY);
 | 
			
		||||
setLog(log);
 | 
			
		||||
 | 
			
		||||
// Load Config
 | 
			
		||||
@@ -174,6 +175,21 @@ function mainLoop() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function keyboardLoop() {
 | 
			
		||||
  while (true) {
 | 
			
		||||
    const [eventType, key] = os.pullEvent("key");
 | 
			
		||||
    if (eventType === "key" && key === keys.c) {
 | 
			
		||||
      log.info("Launching Access Control TUI...");
 | 
			
		||||
      try {
 | 
			
		||||
        launchAccessControlTUI();
 | 
			
		||||
        log.info("TUI closed, resuming normal operation");
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        log.error(`TUI error: ${textutils.serialise(error as object)}`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function main(args: string[]) {
 | 
			
		||||
  log.info("Starting access control system, get args: " + args.join(", "));
 | 
			
		||||
  if (args.length == 1) {
 | 
			
		||||
@@ -187,6 +203,9 @@ function main(args: string[]) {
 | 
			
		||||
        groupNames,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      print(
 | 
			
		||||
        "Access Control System started. Press 'c' to open configuration TUI.",
 | 
			
		||||
      );
 | 
			
		||||
      parallel.waitForAll(
 | 
			
		||||
        () => {
 | 
			
		||||
          mainLoop();
 | 
			
		||||
@@ -197,12 +216,25 @@ function main(args: string[]) {
 | 
			
		||||
        () => {
 | 
			
		||||
          watchLoop();
 | 
			
		||||
        },
 | 
			
		||||
        () => {
 | 
			
		||||
          keyboardLoop();
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      return;
 | 
			
		||||
    } else if (args[0] == "config") {
 | 
			
		||||
      log.info("Launching Access Control TUI...");
 | 
			
		||||
      try {
 | 
			
		||||
        launchAccessControlTUI();
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        log.error(`TUI error: ${textutils.serialise(error as object)}`);
 | 
			
		||||
      }
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  print(`Usage: accesscontrol start`);
 | 
			
		||||
  print(`Usage: accesscontrol start | config`);
 | 
			
		||||
  print("  start  - Start the access control system with monitoring");
 | 
			
		||||
  print("  config - Open configuration TUI");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,615 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Access Control TUI Implementation
 | 
			
		||||
 * A text-based user interface for configuring access control settings
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  createSignal,
 | 
			
		||||
  createStore,
 | 
			
		||||
  div,
 | 
			
		||||
  label,
 | 
			
		||||
  button,
 | 
			
		||||
  input,
 | 
			
		||||
  h1,
 | 
			
		||||
  render,
 | 
			
		||||
  Show,
 | 
			
		||||
  For,
 | 
			
		||||
} from "../lib/ccTUI";
 | 
			
		||||
import {
 | 
			
		||||
  AccessConfig,
 | 
			
		||||
  UserGroupConfig,
 | 
			
		||||
  loadConfig,
 | 
			
		||||
  saveConfig,
 | 
			
		||||
} from "./config";
 | 
			
		||||
 | 
			
		||||
// Tab indices
 | 
			
		||||
const TABS = {
 | 
			
		||||
  BASIC: 0,
 | 
			
		||||
  GROUPS: 1,
 | 
			
		||||
  WELCOME_TOAST: 2,
 | 
			
		||||
  WARN_TOAST: 3,
 | 
			
		||||
  NOTICE_TOAST: 4,
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
type TabIndex = (typeof TABS)[keyof typeof TABS];
 | 
			
		||||
 | 
			
		||||
// Error dialog state
 | 
			
		||||
interface ErrorState {
 | 
			
		||||
  show: boolean;
 | 
			
		||||
  message: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Main TUI Application Component
 | 
			
		||||
 */
 | 
			
		||||
const AccessControlTUI = () => {
 | 
			
		||||
  // Configuration state
 | 
			
		||||
  const [config, setConfig] = createStore<AccessConfig>({} as AccessConfig);
 | 
			
		||||
 | 
			
		||||
  // UI state
 | 
			
		||||
  const [currentTab, setCurrentTab] = createSignal<TabIndex>(TABS.BASIC);
 | 
			
		||||
  const [selectedGroupIndex, setSelectedGroupIndex] = createSignal(0);
 | 
			
		||||
  const [errorState, setErrorState] = createStore<ErrorState>({
 | 
			
		||||
    show: false,
 | 
			
		||||
    message: "",
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // New user input for group management
 | 
			
		||||
  const [newUserName, setNewUserName] = createSignal("");
 | 
			
		||||
 | 
			
		||||
  // Load configuration on initialization
 | 
			
		||||
  const configFilepath = `${shell.dir()}/access.config.json`;
 | 
			
		||||
  const loadedConfig = loadConfig(configFilepath);
 | 
			
		||||
  setConfig(() => loadedConfig);
 | 
			
		||||
 | 
			
		||||
  // Tab navigation functions
 | 
			
		||||
  const tabNames = [
 | 
			
		||||
    "Basic",
 | 
			
		||||
    "Groups",
 | 
			
		||||
    "Welcome Toast",
 | 
			
		||||
    "Warn Toast",
 | 
			
		||||
    "Notice Toast",
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const showError = (message: string) => {
 | 
			
		||||
    setErrorState("show", true);
 | 
			
		||||
    setErrorState("message", message);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const hideError = () => {
 | 
			
		||||
    setErrorState("show", false);
 | 
			
		||||
    setErrorState("message", "");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Validation functions
 | 
			
		||||
  const validateNumber = (value: string): number | null => {
 | 
			
		||||
    const num = parseInt(value);
 | 
			
		||||
    return isNaN(num) ? null : num;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const validateTextComponent = (value: string): boolean => {
 | 
			
		||||
    try {
 | 
			
		||||
      const parsed = textutils.unserialiseJSON(value);
 | 
			
		||||
      return parsed !== undefined && typeof parsed === "object";
 | 
			
		||||
    } catch {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Save configuration with validation
 | 
			
		||||
  const handleSave = () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const currentConfig = config();
 | 
			
		||||
 | 
			
		||||
      // Validate numbers
 | 
			
		||||
      if (
 | 
			
		||||
        validateNumber(currentConfig.detectInterval?.toString() ?? "") === null
 | 
			
		||||
      ) {
 | 
			
		||||
        showError("Invalid Detect Interval: must be a number");
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (
 | 
			
		||||
        validateNumber(currentConfig.watchInterval?.toString() ?? "") === null
 | 
			
		||||
      ) {
 | 
			
		||||
        showError("Invalid Watch Interval: must be a number");
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (
 | 
			
		||||
        validateNumber(currentConfig.noticeTimes?.toString() ?? "") === null
 | 
			
		||||
      ) {
 | 
			
		||||
        showError("Invalid Notice Times: must be a number");
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (
 | 
			
		||||
        validateNumber(currentConfig.detectRange?.toString() ?? "") === null
 | 
			
		||||
      ) {
 | 
			
		||||
        showError("Invalid Detect Range: must be a number");
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Validate text components for toast configs
 | 
			
		||||
      const toastConfigs = [
 | 
			
		||||
        {
 | 
			
		||||
          name: "Welcome Toast Title",
 | 
			
		||||
          value: currentConfig.welcomeToastConfig?.title,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: "Welcome Toast Message",
 | 
			
		||||
          value: currentConfig.welcomeToastConfig?.msg,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: "Warn Toast Title",
 | 
			
		||||
          value: currentConfig.warnToastConfig?.title,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: "Warn Toast Message",
 | 
			
		||||
          value: currentConfig.warnToastConfig?.msg,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: "Notice Toast Title",
 | 
			
		||||
          value: currentConfig.noticeToastConfig?.title,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: "Notice Toast Message",
 | 
			
		||||
          value: currentConfig.noticeToastConfig?.msg,
 | 
			
		||||
        },
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
      for (const toastConfig of toastConfigs) {
 | 
			
		||||
        if (toastConfig.value != undefined) {
 | 
			
		||||
          const serialized = textutils.serialiseJSON(toastConfig.value);
 | 
			
		||||
          if (!validateTextComponent(serialized)) {
 | 
			
		||||
            showError(
 | 
			
		||||
              `Invalid ${toastConfig.name}: must be valid MinecraftTextComponent JSON`,
 | 
			
		||||
            );
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Save configuration
 | 
			
		||||
      saveConfig(currentConfig, configFilepath);
 | 
			
		||||
      showError("Configuration saved successfully!");
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      showError(`Failed to save configuration: ${String(error)}`);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Add user to selected group
 | 
			
		||||
  const addUser = () => {
 | 
			
		||||
    const userName = newUserName().trim();
 | 
			
		||||
    if (userName === "") return;
 | 
			
		||||
 | 
			
		||||
    const groupIndex = selectedGroupIndex();
 | 
			
		||||
    if (groupIndex === 0) {
 | 
			
		||||
      // Admin group
 | 
			
		||||
      const currentAdmin = config().adminGroupConfig;
 | 
			
		||||
      setConfig("adminGroupConfig", {
 | 
			
		||||
        ...currentAdmin,
 | 
			
		||||
        groupUsers: [...currentAdmin.groupUsers, userName],
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      // Regular group
 | 
			
		||||
      const actualIndex = groupIndex - 1;
 | 
			
		||||
      const currentGroups = config().usersGroups;
 | 
			
		||||
      const currentGroup = currentGroups[actualIndex];
 | 
			
		||||
      const newGroups = [...currentGroups];
 | 
			
		||||
      newGroups[actualIndex] = {
 | 
			
		||||
        ...currentGroup,
 | 
			
		||||
        groupUsers: [...(currentGroup?.groupUsers ?? []), userName],
 | 
			
		||||
      };
 | 
			
		||||
      setConfig("usersGroups", newGroups);
 | 
			
		||||
    }
 | 
			
		||||
    setNewUserName("");
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Remove user from selected group
 | 
			
		||||
  const removeUser = (userName: string) => {
 | 
			
		||||
    const groupIndex = selectedGroupIndex();
 | 
			
		||||
    if (groupIndex === 0) {
 | 
			
		||||
      // Admin group
 | 
			
		||||
      const currentAdmin = config().adminGroupConfig;
 | 
			
		||||
      setConfig("adminGroupConfig", {
 | 
			
		||||
        ...currentAdmin,
 | 
			
		||||
        groupUsers: currentAdmin.groupUsers.filter((user) => user !== userName),
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      // Regular group
 | 
			
		||||
      const actualIndex = groupIndex - 1;
 | 
			
		||||
      const currentGroups = config().usersGroups;
 | 
			
		||||
      const currentGroup = currentGroups[actualIndex];
 | 
			
		||||
      const newGroups = [...currentGroups];
 | 
			
		||||
      newGroups[actualIndex] = {
 | 
			
		||||
        ...currentGroup,
 | 
			
		||||
        groupUsers: (currentGroup?.groupUsers ?? []).filter(
 | 
			
		||||
          (user) => user !== userName,
 | 
			
		||||
        ),
 | 
			
		||||
      };
 | 
			
		||||
      setConfig("usersGroups", newGroups);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Get all groups for selection
 | 
			
		||||
  const getAllGroups = (): UserGroupConfig[] => {
 | 
			
		||||
    const currentConfig = config();
 | 
			
		||||
    return [currentConfig.adminGroupConfig, ...currentConfig.usersGroups];
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Get currently selected group
 | 
			
		||||
  const getSelectedGroup = (): UserGroupConfig => {
 | 
			
		||||
    const groups = getAllGroups();
 | 
			
		||||
    return groups[selectedGroupIndex()] ?? config().adminGroupConfig;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Basic Configuration Tab
 | 
			
		||||
   */
 | 
			
		||||
  const BasicTab = () => {
 | 
			
		||||
    return div(
 | 
			
		||||
      { class: "flex flex-col" },
 | 
			
		||||
      label({}, "Detect Interval (ms):"),
 | 
			
		||||
      input({
 | 
			
		||||
        type: "text",
 | 
			
		||||
        value: () => config().detectInterval?.toString() ?? "",
 | 
			
		||||
        onInput: (value) => {
 | 
			
		||||
          const num = validateNumber(value);
 | 
			
		||||
          if (num !== null) setConfig("detectInterval", num);
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
 | 
			
		||||
      label({}, "Watch Interval (ms):"),
 | 
			
		||||
      input({
 | 
			
		||||
        type: "text",
 | 
			
		||||
        value: () => config().watchInterval?.toString() ?? "",
 | 
			
		||||
        onInput: (value) => {
 | 
			
		||||
          const num = validateNumber(value);
 | 
			
		||||
          if (num !== null) setConfig("watchInterval", num);
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
 | 
			
		||||
      label({}, "Notice Times:"),
 | 
			
		||||
      input({
 | 
			
		||||
        type: "text",
 | 
			
		||||
        value: () => config().noticeTimes?.toString() ?? "",
 | 
			
		||||
        onInput: (value) => {
 | 
			
		||||
          const num = validateNumber(value);
 | 
			
		||||
          if (num !== null) setConfig("noticeTimes", num);
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
 | 
			
		||||
      label({}, "Detect Range:"),
 | 
			
		||||
      input({
 | 
			
		||||
        type: "text",
 | 
			
		||||
        value: () => config().detectRange?.toString() ?? "",
 | 
			
		||||
        onInput: (value) => {
 | 
			
		||||
          const num = validateNumber(value);
 | 
			
		||||
          if (num !== null) setConfig("detectRange", num);
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
 | 
			
		||||
      label({}, "Is Warn:"),
 | 
			
		||||
      input({
 | 
			
		||||
        type: "checkbox",
 | 
			
		||||
        checked: () => config().isWarn ?? false,
 | 
			
		||||
        onChange: (checked) => setConfig("isWarn", checked),
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Groups Configuration Tab
 | 
			
		||||
   */
 | 
			
		||||
  const GroupsTab = () => {
 | 
			
		||||
    const groups = getAllGroups();
 | 
			
		||||
    const selectedGroup = getSelectedGroup();
 | 
			
		||||
 | 
			
		||||
    return div(
 | 
			
		||||
      { class: "flex flex-row" },
 | 
			
		||||
      // Left side - Groups list
 | 
			
		||||
      div(
 | 
			
		||||
        { class: "flex flex-col" },
 | 
			
		||||
        label({}, "Groups:"),
 | 
			
		||||
        For({ each: () => groups }, (group, index) =>
 | 
			
		||||
          button(
 | 
			
		||||
            {
 | 
			
		||||
              class:
 | 
			
		||||
                selectedGroupIndex() === index() ? "bg-blue text-white" : "",
 | 
			
		||||
              onClick: () => setSelectedGroupIndex(index()),
 | 
			
		||||
            },
 | 
			
		||||
            group.groupName,
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
 | 
			
		||||
      // Right side - Group details
 | 
			
		||||
      div(
 | 
			
		||||
        { class: "flex flex-col ml-2" },
 | 
			
		||||
        label({}, () => `Group: ${selectedGroup.groupName}`),
 | 
			
		||||
 | 
			
		||||
        label({}, "Is Allowed:"),
 | 
			
		||||
        input({
 | 
			
		||||
          type: "checkbox",
 | 
			
		||||
          checked: () => selectedGroup.isAllowed,
 | 
			
		||||
          onChange: (checked) => {
 | 
			
		||||
            const groupIndex = selectedGroupIndex();
 | 
			
		||||
            if (groupIndex === 0) {
 | 
			
		||||
              const currentAdmin = config().adminGroupConfig;
 | 
			
		||||
              setConfig("adminGroupConfig", {
 | 
			
		||||
                ...currentAdmin,
 | 
			
		||||
                isAllowed: checked,
 | 
			
		||||
              });
 | 
			
		||||
            } else {
 | 
			
		||||
              const actualIndex = groupIndex - 1;
 | 
			
		||||
              const currentGroups = config().usersGroups;
 | 
			
		||||
              const currentGroup = currentGroups[actualIndex];
 | 
			
		||||
              const newGroups = [...currentGroups];
 | 
			
		||||
              newGroups[actualIndex] = {
 | 
			
		||||
                ...currentGroup,
 | 
			
		||||
                isAllowed: checked,
 | 
			
		||||
              };
 | 
			
		||||
              setConfig("usersGroups", newGroups);
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        }),
 | 
			
		||||
 | 
			
		||||
        label({}, "Is Notice:"),
 | 
			
		||||
        input({
 | 
			
		||||
          type: "checkbox",
 | 
			
		||||
          checked: () => selectedGroup.isNotice,
 | 
			
		||||
          onChange: (checked) => {
 | 
			
		||||
            const groupIndex = selectedGroupIndex();
 | 
			
		||||
            if (groupIndex === 0) {
 | 
			
		||||
              const currentAdmin = config().adminGroupConfig;
 | 
			
		||||
              setConfig("adminGroupConfig", {
 | 
			
		||||
                ...currentAdmin,
 | 
			
		||||
                isNotice: checked,
 | 
			
		||||
              });
 | 
			
		||||
            } else {
 | 
			
		||||
              const actualIndex = groupIndex - 1;
 | 
			
		||||
              const currentGroups = config().usersGroups;
 | 
			
		||||
              const currentGroup = currentGroups[actualIndex];
 | 
			
		||||
              const newGroups = [...currentGroups];
 | 
			
		||||
              newGroups[actualIndex] = {
 | 
			
		||||
                ...currentGroup,
 | 
			
		||||
                isNotice: checked,
 | 
			
		||||
              };
 | 
			
		||||
              setConfig("usersGroups", newGroups);
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        }),
 | 
			
		||||
 | 
			
		||||
        label({}, "Group Users:"),
 | 
			
		||||
        // User management
 | 
			
		||||
        div(
 | 
			
		||||
          { class: "flex flex-row" },
 | 
			
		||||
          input({
 | 
			
		||||
            type: "text",
 | 
			
		||||
            value: newUserName,
 | 
			
		||||
            onInput: setNewUserName,
 | 
			
		||||
            placeholder: "Enter username",
 | 
			
		||||
          }),
 | 
			
		||||
          button({ onClick: addUser }, "Add"),
 | 
			
		||||
        ),
 | 
			
		||||
 | 
			
		||||
        // Users list
 | 
			
		||||
        For({ each: () => selectedGroup.groupUsers ?? [] }, (user) =>
 | 
			
		||||
          div(
 | 
			
		||||
            { class: "flex flex-row items-center" },
 | 
			
		||||
            label({}, user),
 | 
			
		||||
            button(
 | 
			
		||||
              {
 | 
			
		||||
                class: "ml-1 bg-red text-white",
 | 
			
		||||
                onClick: () => removeUser(user),
 | 
			
		||||
              },
 | 
			
		||||
              "Remove",
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Toast Configuration Tab Factory
 | 
			
		||||
   */
 | 
			
		||||
  const createToastTab = (
 | 
			
		||||
    toastType: "welcomeToastConfig" | "warnToastConfig" | "noticeToastConfig",
 | 
			
		||||
  ) => {
 | 
			
		||||
    return () => {
 | 
			
		||||
      const toastConfig = config()[toastType];
 | 
			
		||||
 | 
			
		||||
      return div(
 | 
			
		||||
        { class: "flex flex-col" },
 | 
			
		||||
        label({}, "Title (JSON):"),
 | 
			
		||||
        input({
 | 
			
		||||
          type: "text",
 | 
			
		||||
          value: () => textutils.serialiseJSON(toastConfig?.title) ?? "",
 | 
			
		||||
          onInput: (value) => {
 | 
			
		||||
            try {
 | 
			
		||||
              const parsed = textutils.unserialiseJSON(value);
 | 
			
		||||
              if (parsed != undefined && typeof parsed === "object") {
 | 
			
		||||
                const currentConfig = config();
 | 
			
		||||
                const currentToast = currentConfig[toastType];
 | 
			
		||||
                setConfig(toastType, {
 | 
			
		||||
                  ...currentToast,
 | 
			
		||||
                  title: parsed as MinecraftTextComponent,
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
            } catch {
 | 
			
		||||
              // Invalid JSON, ignore
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        }),
 | 
			
		||||
 | 
			
		||||
        label({}, "Message (JSON):"),
 | 
			
		||||
        input({
 | 
			
		||||
          type: "text",
 | 
			
		||||
          value: () => textutils.serialiseJSON(toastConfig?.msg) ?? "",
 | 
			
		||||
          onInput: (value) => {
 | 
			
		||||
            try {
 | 
			
		||||
              const parsed = textutils.unserialiseJSON(value);
 | 
			
		||||
              if (parsed != undefined && typeof parsed === "object") {
 | 
			
		||||
                const currentConfig = config();
 | 
			
		||||
                const currentToast = currentConfig[toastType];
 | 
			
		||||
                setConfig(toastType, {
 | 
			
		||||
                  ...currentToast,
 | 
			
		||||
                  msg: parsed as MinecraftTextComponent,
 | 
			
		||||
                });
 | 
			
		||||
              }
 | 
			
		||||
            } catch {
 | 
			
		||||
              // Invalid JSON, ignore
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
        }),
 | 
			
		||||
 | 
			
		||||
        label({}, "Prefix:"),
 | 
			
		||||
        input({
 | 
			
		||||
          type: "text",
 | 
			
		||||
          value: () => toastConfig?.prefix ?? "",
 | 
			
		||||
          onInput: (value) => {
 | 
			
		||||
            const currentConfig = config();
 | 
			
		||||
            const currentToast = currentConfig[toastType];
 | 
			
		||||
            setConfig(toastType, { ...currentToast, prefix: value });
 | 
			
		||||
          },
 | 
			
		||||
        }),
 | 
			
		||||
 | 
			
		||||
        label({}, "Brackets:"),
 | 
			
		||||
        input({
 | 
			
		||||
          type: "text",
 | 
			
		||||
          value: () => toastConfig?.brackets ?? "",
 | 
			
		||||
          onInput: (value) => {
 | 
			
		||||
            const currentConfig = config();
 | 
			
		||||
            const currentToast = currentConfig[toastType];
 | 
			
		||||
            setConfig(toastType, { ...currentToast, brackets: value });
 | 
			
		||||
          },
 | 
			
		||||
        }),
 | 
			
		||||
 | 
			
		||||
        label({}, "Bracket Color:"),
 | 
			
		||||
        input({
 | 
			
		||||
          type: "text",
 | 
			
		||||
          value: () => toastConfig?.bracketColor ?? "",
 | 
			
		||||
          onInput: (value) => {
 | 
			
		||||
            const currentConfig = config();
 | 
			
		||||
            const currentToast = currentConfig[toastType];
 | 
			
		||||
            setConfig(toastType, { ...currentToast, bracketColor: value });
 | 
			
		||||
          },
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Create toast tab components
 | 
			
		||||
  const WelcomeToastTab = createToastTab("welcomeToastConfig");
 | 
			
		||||
  const WarnToastTab = createToastTab("warnToastConfig");
 | 
			
		||||
  const NoticeToastTab = createToastTab("noticeToastConfig");
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Error Dialog
 | 
			
		||||
   */
 | 
			
		||||
  const ErrorDialog = () => {
 | 
			
		||||
    return Show(
 | 
			
		||||
      { when: () => errorState().show },
 | 
			
		||||
      div(
 | 
			
		||||
        {
 | 
			
		||||
          class:
 | 
			
		||||
            "fixed top-1/4 left-1/4 right-1/4 bottom-1/4 bg-red text-white border",
 | 
			
		||||
        },
 | 
			
		||||
        div(
 | 
			
		||||
          { class: "flex flex-col p-2" },
 | 
			
		||||
          label({}, () => errorState().message),
 | 
			
		||||
          button(
 | 
			
		||||
            {
 | 
			
		||||
              class: "mt-2 bg-white text-black",
 | 
			
		||||
              onClick: hideError,
 | 
			
		||||
            },
 | 
			
		||||
            "OK",
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Tab Content Renderer
 | 
			
		||||
   */
 | 
			
		||||
  const TabContent = () => {
 | 
			
		||||
    const tab = currentTab();
 | 
			
		||||
    if (tab === TABS.BASIC) return BasicTab();
 | 
			
		||||
    if (tab === TABS.GROUPS) return GroupsTab();
 | 
			
		||||
    if (tab === TABS.WELCOME_TOAST) return WelcomeToastTab();
 | 
			
		||||
    if (tab === TABS.WARN_TOAST) return WarnToastTab();
 | 
			
		||||
    if (tab === TABS.NOTICE_TOAST) return NoticeToastTab();
 | 
			
		||||
    return BasicTab(); // fallback
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Main UI Layout
 | 
			
		||||
   */
 | 
			
		||||
  return div(
 | 
			
		||||
    { class: "flex flex-col h-full" },
 | 
			
		||||
    // Header
 | 
			
		||||
    h1("Access Control Configuration"),
 | 
			
		||||
 | 
			
		||||
    // Tab bar
 | 
			
		||||
    div(
 | 
			
		||||
      { class: "flex flex-row" },
 | 
			
		||||
      For({ each: () => tabNames }, (tabName, index) =>
 | 
			
		||||
        button(
 | 
			
		||||
          {
 | 
			
		||||
            class: currentTab() === index() ? "bg-blue text-white" : "",
 | 
			
		||||
            onClick: () => setCurrentTab(index() as TabIndex),
 | 
			
		||||
          },
 | 
			
		||||
          tabName,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    ),
 | 
			
		||||
 | 
			
		||||
    // Content area
 | 
			
		||||
    div({ class: "flex-1 p-2" }, TabContent()),
 | 
			
		||||
 | 
			
		||||
    // Action buttons
 | 
			
		||||
    div(
 | 
			
		||||
      { class: "flex flex-row justify-center p-2" },
 | 
			
		||||
      button(
 | 
			
		||||
        {
 | 
			
		||||
          class: "bg-green text-white mr-2",
 | 
			
		||||
          onClick: handleSave,
 | 
			
		||||
        },
 | 
			
		||||
        "Save",
 | 
			
		||||
      ),
 | 
			
		||||
      button(
 | 
			
		||||
        {
 | 
			
		||||
          class: "bg-gray text-white",
 | 
			
		||||
          onClick: () => {
 | 
			
		||||
            // Close TUI - this will be handled by the application framework
 | 
			
		||||
            error("TUI_CLOSE");
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        "Close",
 | 
			
		||||
      ),
 | 
			
		||||
    ),
 | 
			
		||||
 | 
			
		||||
    // Error dialog overlay
 | 
			
		||||
    ErrorDialog(),
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Launch the Access Control TUI
 | 
			
		||||
 */
 | 
			
		||||
export function launchAccessControlTUI(): void {
 | 
			
		||||
  try {
 | 
			
		||||
    render(AccessControlTUI);
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    if (e === "TUI_CLOSE" || e === "Terminated") {
 | 
			
		||||
      // Normal exit
 | 
			
		||||
      return;
 | 
			
		||||
    } else {
 | 
			
		||||
      print("Error in Access Control TUI:");
 | 
			
		||||
      printError(e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Export the main component for external use
 | 
			
		||||
export { AccessControlTUI };
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user