mirror of
				https://github.com/SikongJueluo/cc-utils.git
				synced 2025-11-04 19:27:50 +08:00 
			
		
		
		
	add tui framework but not finish, reconstruct accesscontrol
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -3,3 +3,5 @@ node_modules
 | 
			
		||||
event/
 | 
			
		||||
build/
 | 
			
		||||
reference/
 | 
			
		||||
 | 
			
		||||
QWEN.md
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
 | 
			
		||||
 | 
			
		||||
build: build-autocraft build-accesscontrol build-test sync
 | 
			
		||||
build: build-autocraft build-accesscontrol build-test build-example sync
 | 
			
		||||
 | 
			
		||||
build-autocraft:
 | 
			
		||||
    pnpm tstl -p ./tsconfig.autocraft.json
 | 
			
		||||
@@ -12,5 +12,8 @@ build-accesscontrol:
 | 
			
		||||
build-test:
 | 
			
		||||
    pnpm tstl -p ./tsconfig.test.json
 | 
			
		||||
 | 
			
		||||
build-example:
 | 
			
		||||
    pnpm tstl -p ./tsconfig.tuiExample.json
 | 
			
		||||
 | 
			
		||||
sync:
 | 
			
		||||
    cp -r "./build/*" "C:\\Users\\sikongjueluo\\AppData\\Roaming\\CraftOS-PC\\computer\\0\\user\\"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,55 @@
 | 
			
		||||
{
 | 
			
		||||
  "detectRange": 64,
 | 
			
		||||
  "detectRange": 256,
 | 
			
		||||
  "detectInterval": 1,
 | 
			
		||||
  "warnInterval": 7,
 | 
			
		||||
  "watchInterval": 10,
 | 
			
		||||
  "noticeTimes": 2,
 | 
			
		||||
  "isWarn": false,
 | 
			
		||||
  "adminGroupConfig": {
 | 
			
		||||
    "groupName": "Admin",
 | 
			
		||||
    "groupUsers": ["Selcon"],
 | 
			
		||||
    "isAllowed": true,
 | 
			
		||||
    "isNotice": true
 | 
			
		||||
  },
 | 
			
		||||
  "defaultToastConfig": {
 | 
			
		||||
  "usersGroups": [
 | 
			
		||||
    {
 | 
			
		||||
      "groupName": "user",
 | 
			
		||||
      "groupUsers": [],
 | 
			
		||||
      "isAllowed": true,
 | 
			
		||||
      "isNotice": true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "groupName": "VIP",
 | 
			
		||||
      "groupUsers": [],
 | 
			
		||||
      "isAllowed": true,
 | 
			
		||||
      "isNotice": false
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "groupName": "enemies",
 | 
			
		||||
      "groupUsers": [],
 | 
			
		||||
      "isAllowed": false,
 | 
			
		||||
      "isNotice": false
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "welcomeToastConfig": {
 | 
			
		||||
    "title": {
 | 
			
		||||
      "text": "Welcome",
 | 
			
		||||
      "color": "green"
 | 
			
		||||
    },
 | 
			
		||||
    "msg": {
 | 
			
		||||
      "text": "Hello %groupName% %playerName%",
 | 
			
		||||
      "text": "Hello User %playerName%",
 | 
			
		||||
      "color": "green"
 | 
			
		||||
    },
 | 
			
		||||
    "prefix": "Taohuayuan",
 | 
			
		||||
    "brackets": "[]",
 | 
			
		||||
    "bracketColor": ""
 | 
			
		||||
  },
 | 
			
		||||
  "noticeToastConfig": {
 | 
			
		||||
    "title": {
 | 
			
		||||
      "text": "Welcome",
 | 
			
		||||
      "color": "green"
 | 
			
		||||
    },
 | 
			
		||||
    "msg": {
 | 
			
		||||
      "text": "Hello User %playerName%",
 | 
			
		||||
      "color": "green"
 | 
			
		||||
    },
 | 
			
		||||
    "prefix": "Taohuayuan",
 | 
			
		||||
@@ -33,35 +68,5 @@
 | 
			
		||||
    "prefix": "Taohuayuan",
 | 
			
		||||
    "brackets": "[]",
 | 
			
		||||
    "bracketColor": ""
 | 
			
		||||
  },
 | 
			
		||||
  "usersGroups": [
 | 
			
		||||
    {
 | 
			
		||||
      "groupName": "user",
 | 
			
		||||
      "groupUsers": [],
 | 
			
		||||
      "isAllowed": true,
 | 
			
		||||
      "isNotice": true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "groupName": "VIP",
 | 
			
		||||
      "groupUsers": [],
 | 
			
		||||
      "isAllowed": true,
 | 
			
		||||
      "isNotice": false
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "groupName": "enemy",
 | 
			
		||||
      "groupUsers": [],
 | 
			
		||||
      "isAllowed": false,
 | 
			
		||||
      "isNotice": false,
 | 
			
		||||
      "toastConfig": {
 | 
			
		||||
        "title": {
 | 
			
		||||
          "text": "Warn",
 | 
			
		||||
          "color": "red"
 | 
			
		||||
        },
 | 
			
		||||
        "msg": {
 | 
			
		||||
          "text": "Warn %playerName%",
 | 
			
		||||
          "color": "red"
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -261,10 +261,10 @@ class SetCommand implements CLICommand {
 | 
			
		||||
 | 
			
		||||
    switch (option) {
 | 
			
		||||
      case "warnInterval":
 | 
			
		||||
        context.config.warnInterval = value;
 | 
			
		||||
        context.config.watchInterval = value;
 | 
			
		||||
        return {
 | 
			
		||||
          success: true,
 | 
			
		||||
          message: `Set warn interval to ${context.config.warnInterval}`,
 | 
			
		||||
          message: `Set warn interval to ${context.config.watchInterval}`,
 | 
			
		||||
          shouldSaveConfig: true,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
@@ -549,20 +549,6 @@ class ShowConfigCommand implements CLICommand {
 | 
			
		||||
          groupsMessage += `  Users: [${(group.groupUsers ?? []).join(", ")}]\n`;
 | 
			
		||||
          groupsMessage += `  Allowed: ${group.isAllowed}\n`;
 | 
			
		||||
          groupsMessage += `  Notice: ${group.isNotice}\n`;
 | 
			
		||||
          if (group.toastConfig !== undefined) {
 | 
			
		||||
            groupsMessage += `  Custom Toast Config:\n`;
 | 
			
		||||
            groupsMessage += `    Title: ${group.toastConfig.title.text}\n`;
 | 
			
		||||
            groupsMessage += `    Message: ${group.toastConfig.msg.text}\n`;
 | 
			
		||||
            if (group.toastConfig.prefix !== undefined) {
 | 
			
		||||
              groupsMessage += `    Prefix: ${group.toastConfig.prefix}\n`;
 | 
			
		||||
            }
 | 
			
		||||
            if (group.toastConfig.brackets !== undefined) {
 | 
			
		||||
              groupsMessage += `    Brackets: ${group.toastConfig.brackets}\n`;
 | 
			
		||||
            }
 | 
			
		||||
            if (group.toastConfig.bracketColor !== undefined) {
 | 
			
		||||
              groupsMessage += `    Bracket Color: ${group.toastConfig.bracketColor}\n`;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          groupsMessage += "\n";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -574,11 +560,11 @@ class ShowConfigCommand implements CLICommand {
 | 
			
		||||
 | 
			
		||||
      case "toast": {
 | 
			
		||||
        let toastMessage = "Default Toast Config:\n";
 | 
			
		||||
        toastMessage += `  Title: ${context.config.defaultToastConfig.title.text}\n`;
 | 
			
		||||
        toastMessage += `  Message: ${context.config.defaultToastConfig.msg.text}\n`;
 | 
			
		||||
        toastMessage += `  Prefix: ${context.config.defaultToastConfig.prefix ?? "none"}\n`;
 | 
			
		||||
        toastMessage += `  Brackets: ${context.config.defaultToastConfig.brackets ?? "none"}\n`;
 | 
			
		||||
        toastMessage += `  Bracket Color: ${context.config.defaultToastConfig.bracketColor ?? "none"}\n\n`;
 | 
			
		||||
        toastMessage += `  Title: ${context.config.welcomeToastConfig.title.text}\n`;
 | 
			
		||||
        toastMessage += `  Message: ${context.config.welcomeToastConfig.msg.text}\n`;
 | 
			
		||||
        toastMessage += `  Prefix: ${context.config.welcomeToastConfig.prefix ?? "none"}\n`;
 | 
			
		||||
        toastMessage += `  Brackets: ${context.config.welcomeToastConfig.brackets ?? "none"}\n`;
 | 
			
		||||
        toastMessage += `  Bracket Color: ${context.config.welcomeToastConfig.bracketColor ?? "none"}\n\n`;
 | 
			
		||||
 | 
			
		||||
        toastMessage += "Warn Toast Config:\n";
 | 
			
		||||
        toastMessage += `  Title: ${context.config.warnToastConfig.title.text}\n`;
 | 
			
		||||
@@ -596,7 +582,7 @@ class ShowConfigCommand implements CLICommand {
 | 
			
		||||
      case "all": {
 | 
			
		||||
        let allMessage = `Detect Range: ${context.config.detectRange}\n`;
 | 
			
		||||
        allMessage += `Detect Interval: ${context.config.detectInterval}\n`;
 | 
			
		||||
        allMessage += `Warn Interval: ${context.config.warnInterval}\n\n`;
 | 
			
		||||
        allMessage += `Warn Interval: ${context.config.watchInterval}\n\n`;
 | 
			
		||||
        allMessage +=
 | 
			
		||||
          "Use 'showconfig groups' or 'showconfig toast' for detailed view";
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -16,28 +16,32 @@ interface UserGroupConfig {
 | 
			
		||||
  isAllowed: boolean;
 | 
			
		||||
  isNotice: boolean;
 | 
			
		||||
  groupUsers: string[];
 | 
			
		||||
  toastConfig?: ToastConfig;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface AccessConfig {
 | 
			
		||||
  detectInterval: number;
 | 
			
		||||
  warnInterval: number;
 | 
			
		||||
  watchInterval: number;
 | 
			
		||||
  noticeTimes: number;
 | 
			
		||||
  detectRange: number;
 | 
			
		||||
  isWarn: boolean;
 | 
			
		||||
  adminGroupConfig: UserGroupConfig;
 | 
			
		||||
  defaultToastConfig: ToastConfig;
 | 
			
		||||
  welcomeToastConfig: ToastConfig;
 | 
			
		||||
  warnToastConfig: ToastConfig;
 | 
			
		||||
  noticeToastConfig: ToastConfig;
 | 
			
		||||
  usersGroups: UserGroupConfig[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaultConfig: AccessConfig = {
 | 
			
		||||
  detectRange: 64,
 | 
			
		||||
  detectInterval: 3,
 | 
			
		||||
  warnInterval: 7,
 | 
			
		||||
  detectRange: 256,
 | 
			
		||||
  detectInterval: 1,
 | 
			
		||||
  watchInterval: 10,
 | 
			
		||||
  noticeTimes: 2,
 | 
			
		||||
  isWarn: false,
 | 
			
		||||
  adminGroupConfig: {
 | 
			
		||||
    groupName: "Admin",
 | 
			
		||||
    groupUsers: ["Selcon"],
 | 
			
		||||
    isAllowed: true,
 | 
			
		||||
    isNotice: false,
 | 
			
		||||
    isNotice: true,
 | 
			
		||||
  },
 | 
			
		||||
  usersGroups: [
 | 
			
		||||
    {
 | 
			
		||||
@@ -57,19 +61,22 @@ const defaultConfig: AccessConfig = {
 | 
			
		||||
      groupUsers: [],
 | 
			
		||||
      isAllowed: false,
 | 
			
		||||
      isNotice: false,
 | 
			
		||||
      toastConfig: {
 | 
			
		||||
        title: {
 | 
			
		||||
          text: "Warn",
 | 
			
		||||
          color: "red",
 | 
			
		||||
        },
 | 
			
		||||
        msg: {
 | 
			
		||||
          text: "Warn %playerName%",
 | 
			
		||||
          color: "red",
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  defaultToastConfig: {
 | 
			
		||||
  welcomeToastConfig: {
 | 
			
		||||
    title: {
 | 
			
		||||
      text: "Welcome",
 | 
			
		||||
      color: "green",
 | 
			
		||||
    },
 | 
			
		||||
    msg: {
 | 
			
		||||
      text: "Hello User %playerName%",
 | 
			
		||||
      color: "green",
 | 
			
		||||
    },
 | 
			
		||||
    prefix: "Taohuayuan",
 | 
			
		||||
    brackets: "[]",
 | 
			
		||||
    bracketColor: "",
 | 
			
		||||
  },
 | 
			
		||||
  noticeToastConfig: {
 | 
			
		||||
    title: {
 | 
			
		||||
      text: "Welcome",
 | 
			
		||||
      color: "green",
 | 
			
		||||
 
 | 
			
		||||
@@ -16,12 +16,12 @@ const config = loadConfig(configFilepath);
 | 
			
		||||
log.info("Load config successfully!");
 | 
			
		||||
if (DEBUG) log.debug(textutils.serialise(config, { allow_repetitions: true }));
 | 
			
		||||
const groupNames = config.usersGroups.map((value) => value.groupName);
 | 
			
		||||
let warnTargetPlayers: string[];
 | 
			
		||||
let noticeTargetPlayers: string[];
 | 
			
		||||
const playerDetector = peripheralManager.findByNameRequired("playerDetector");
 | 
			
		||||
const chatBox = peripheralManager.findByNameRequired("chatBox");
 | 
			
		||||
 | 
			
		||||
let inRangePlayers: string[] = [];
 | 
			
		||||
let notAllowedPlayers: string[] = [];
 | 
			
		||||
let watchPlayersInfo: { name: string; hasNoticeTimes: number }[] = [];
 | 
			
		||||
 | 
			
		||||
function safeParseTextComponent(
 | 
			
		||||
  component: MinecraftTextComponent,
 | 
			
		||||
@@ -38,43 +38,50 @@ function safeParseTextComponent(
 | 
			
		||||
  return textutils.serialiseJSON(component);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sendToast(
 | 
			
		||||
  toastConfig: ToastConfig,
 | 
			
		||||
  player: string,
 | 
			
		||||
  groupConfig?: UserGroupConfig,
 | 
			
		||||
) {
 | 
			
		||||
function sendToast(toastConfig: ToastConfig, targetPlayer: string) {
 | 
			
		||||
  return chatBox.sendFormattedToastToPlayer(
 | 
			
		||||
    safeParseTextComponent(
 | 
			
		||||
      toastConfig.msg ?? config.defaultToastConfig.msg,
 | 
			
		||||
      player,
 | 
			
		||||
      groupConfig?.groupName,
 | 
			
		||||
    textutils.serialiseJSON(toastConfig.msg ?? config.welcomeToastConfig.msg),
 | 
			
		||||
    textutils.serialiseJSON(
 | 
			
		||||
      toastConfig.title ?? config.welcomeToastConfig.title,
 | 
			
		||||
    ),
 | 
			
		||||
    safeParseTextComponent(
 | 
			
		||||
      toastConfig.title ?? config.defaultToastConfig.title,
 | 
			
		||||
      player,
 | 
			
		||||
      groupConfig?.groupName,
 | 
			
		||||
    ),
 | 
			
		||||
    player,
 | 
			
		||||
    toastConfig.prefix ?? config.defaultToastConfig.prefix,
 | 
			
		||||
    toastConfig.brackets ?? config.defaultToastConfig.brackets,
 | 
			
		||||
    toastConfig.bracketColor ?? config.defaultToastConfig.bracketColor,
 | 
			
		||||
    targetPlayer,
 | 
			
		||||
    toastConfig.prefix ?? config.welcomeToastConfig.prefix,
 | 
			
		||||
    toastConfig.brackets ?? config.welcomeToastConfig.brackets,
 | 
			
		||||
    toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor,
 | 
			
		||||
    undefined,
 | 
			
		||||
    true,
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sendWarnAndNotice(player: string) {
 | 
			
		||||
  const playerPos = playerDetector.getPlayerPos(player);
 | 
			
		||||
function sendNotice(player: string, playerInfo?: PlayerInfo) {
 | 
			
		||||
  const onlinePlayers = playerDetector.getOnlinePlayers();
 | 
			
		||||
  warnTargetPlayers = config.adminGroupConfig.groupUsers.concat(
 | 
			
		||||
  noticeTargetPlayers = config.adminGroupConfig.groupUsers.concat(
 | 
			
		||||
    config.usersGroups
 | 
			
		||||
      .filter((value) => value.isNotice)
 | 
			
		||||
      .map((value) => value.groupUsers ?? [])
 | 
			
		||||
      .flat(),
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const warnMsg = `Not Allowed Player ${player} Break in Home at Position ${playerPos?.x}, ${playerPos?.y}, ${playerPos?.z}`;
 | 
			
		||||
  const toastConfig: ToastConfig = {
 | 
			
		||||
    title: {
 | 
			
		||||
      text: "Notice",
 | 
			
		||||
      color: "red",
 | 
			
		||||
    },
 | 
			
		||||
    msg: {
 | 
			
		||||
      text: `Unfamiliar Player ${player} appeared at\n Position ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
 | 
			
		||||
      color: "red",
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
  for (const targetPlayer of noticeTargetPlayers) {
 | 
			
		||||
    if (!onlinePlayers.includes(targetPlayer)) continue;
 | 
			
		||||
    sendToast(toastConfig, targetPlayer);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function sendWarn(player: string) {
 | 
			
		||||
  const warnMsg = `Not Allowed Player ${player} Break in Home `;
 | 
			
		||||
  log.warn(warnMsg);
 | 
			
		||||
 | 
			
		||||
  sendToast(config.warnToastConfig, player);
 | 
			
		||||
  chatBox.sendFormattedMessageToPlayer(
 | 
			
		||||
    safeParseTextComponent(config.warnToastConfig.msg, player),
 | 
			
		||||
@@ -85,37 +92,36 @@ function sendWarnAndNotice(player: string) {
 | 
			
		||||
    undefined,
 | 
			
		||||
    true,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  for (const targetPlayer of warnTargetPlayers) {
 | 
			
		||||
    if (!onlinePlayers.includes(targetPlayer)) continue;
 | 
			
		||||
    chatBox.sendFormattedMessageToPlayer(
 | 
			
		||||
      textutils.serialise({
 | 
			
		||||
        text: warnMsg,
 | 
			
		||||
        color: "red",
 | 
			
		||||
      } as MinecraftTextComponent),
 | 
			
		||||
      targetPlayer,
 | 
			
		||||
      "AccessControl",
 | 
			
		||||
      "[]",
 | 
			
		||||
      undefined,
 | 
			
		||||
      undefined,
 | 
			
		||||
      true,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function warnLoop() {
 | 
			
		||||
function watchLoop() {
 | 
			
		||||
  while (true) {
 | 
			
		||||
    for (const player of notAllowedPlayers) {
 | 
			
		||||
      if (inRangePlayers.includes(player)) {
 | 
			
		||||
        // sendWarnAndNotice(player);
 | 
			
		||||
    for (const player of watchPlayersInfo) {
 | 
			
		||||
      if (inRangePlayers.includes(player.name)) {
 | 
			
		||||
        const playerInfo = playerDetector.getPlayerPos(player.name);
 | 
			
		||||
 | 
			
		||||
        // Notice
 | 
			
		||||
        if (player.hasNoticeTimes < config.noticeTimes) {
 | 
			
		||||
          sendNotice(player.name, playerInfo);
 | 
			
		||||
          player.hasNoticeTimes += 1;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Warn
 | 
			
		||||
        if (config.isWarn) sendWarn(player.name);
 | 
			
		||||
 | 
			
		||||
        // Record
 | 
			
		||||
        log.warn(
 | 
			
		||||
          `${player.name} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        notAllowedPlayers = notAllowedPlayers.filter(
 | 
			
		||||
          (value) => value != player,
 | 
			
		||||
        // Get rid of player from list
 | 
			
		||||
        watchPlayersInfo = watchPlayersInfo.filter(
 | 
			
		||||
          (value) => value.name != player.name,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    os.sleep(config.warnInterval);
 | 
			
		||||
    os.sleep(config.watchInterval);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -131,39 +137,36 @@ function mainLoop() {
 | 
			
		||||
      if (inRangePlayers.includes(player)) continue;
 | 
			
		||||
 | 
			
		||||
      if (config.adminGroupConfig.groupUsers.includes(player)) {
 | 
			
		||||
        log.info(`Admin ${player} enter`);
 | 
			
		||||
        sendToast(
 | 
			
		||||
          config.adminGroupConfig.toastConfig ?? config.defaultToastConfig,
 | 
			
		||||
          player,
 | 
			
		||||
          config.adminGroupConfig,
 | 
			
		||||
        );
 | 
			
		||||
        log.info(`Admin ${player} appear`);
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let inUserGroup = false;
 | 
			
		||||
      // New player appear
 | 
			
		||||
      const playerInfo = playerDetector.getPlayerPos(player);
 | 
			
		||||
      let groupConfig: UserGroupConfig = {
 | 
			
		||||
        groupName: "Unfamiliar",
 | 
			
		||||
        groupUsers: [],
 | 
			
		||||
        isAllowed: false,
 | 
			
		||||
        isNotice: false,
 | 
			
		||||
      };
 | 
			
		||||
      for (const userGroupConfig of config.usersGroups) {
 | 
			
		||||
        if (userGroupConfig.groupUsers == undefined) continue;
 | 
			
		||||
        if (!userGroupConfig.groupUsers.includes(player)) continue;
 | 
			
		||||
 | 
			
		||||
        if (!userGroupConfig.isAllowed) {
 | 
			
		||||
          sendWarnAndNotice(player);
 | 
			
		||||
          notAllowedPlayers.push(player);
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        log.info(`${userGroupConfig.groupName} ${player} enter`);
 | 
			
		||||
        sendToast(
 | 
			
		||||
          userGroupConfig.toastConfig ?? config.defaultToastConfig,
 | 
			
		||||
          player,
 | 
			
		||||
          userGroupConfig,
 | 
			
		||||
        groupConfig = userGroupConfig;
 | 
			
		||||
        log.info(
 | 
			
		||||
          `${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        inUserGroup = true;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      if (inUserGroup) continue;
 | 
			
		||||
      if (groupConfig.isAllowed) continue;
 | 
			
		||||
 | 
			
		||||
      sendWarnAndNotice(player);
 | 
			
		||||
      notAllowedPlayers.push(player);
 | 
			
		||||
      log.warn(
 | 
			
		||||
        `${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
 | 
			
		||||
      );
 | 
			
		||||
      if (config.isWarn) sendWarn(player);
 | 
			
		||||
      watchPlayersInfo.push({ name: player, hasNoticeTimes: 0 });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inRangePlayers = players;
 | 
			
		||||
@@ -192,7 +195,7 @@ function main(args: string[]) {
 | 
			
		||||
          void cli.startConfigLoop();
 | 
			
		||||
        },
 | 
			
		||||
        () => {
 | 
			
		||||
          warnLoop();
 | 
			
		||||
          watchLoop();
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      return;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								src/accesscontrol/tui.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/accesscontrol/tui.ts
									
									
									
									
									
										Normal file
									
								
							@@ -18,12 +18,14 @@ export class CCLog {
 | 
			
		||||
  private interval: number;
 | 
			
		||||
  private startTime: number;
 | 
			
		||||
  private currentTimePeriod: string;
 | 
			
		||||
  private inTerm: boolean;
 | 
			
		||||
 | 
			
		||||
  constructor(filename?: string, interval: number = DAY) {
 | 
			
		||||
  constructor(filename?: string, inTerm = true, interval: number = DAY) {
 | 
			
		||||
    term.clear();
 | 
			
		||||
    term.setCursorPos(1, 1);
 | 
			
		||||
 | 
			
		||||
    this.interval = interval;
 | 
			
		||||
    this.inTerm = inTerm;
 | 
			
		||||
    this.startTime = os.time(os.date("*t"));
 | 
			
		||||
    this.currentTimePeriod = this.getTimePeriodString(this.startTime);
 | 
			
		||||
 | 
			
		||||
@@ -117,21 +119,23 @@ export class CCLog {
 | 
			
		||||
    // Check if we need to rotate the log file
 | 
			
		||||
    this.checkAndRotateLogFile();
 | 
			
		||||
 | 
			
		||||
    let originalColor: Color = 0;
 | 
			
		||||
    if (color != undefined) {
 | 
			
		||||
      originalColor = term.getTextColor();
 | 
			
		||||
      term.setTextColor(color);
 | 
			
		||||
    if (this.inTerm) {
 | 
			
		||||
      let originalColor: Color = 0;
 | 
			
		||||
      if (color != undefined) {
 | 
			
		||||
        originalColor = term.getTextColor();
 | 
			
		||||
        term.setTextColor(color);
 | 
			
		||||
      }
 | 
			
		||||
      print(msg);
 | 
			
		||||
 | 
			
		||||
      if (color != undefined) {
 | 
			
		||||
        term.setTextColor(originalColor);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Log
 | 
			
		||||
    print(msg);
 | 
			
		||||
    if (this.fp != undefined) {
 | 
			
		||||
      this.fp.write(msg + "\r\n");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (color != undefined) {
 | 
			
		||||
      term.setTextColor(originalColor);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public debug(msg: string) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										708
									
								
								src/lib/ccTUI.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										708
									
								
								src/lib/ccTUI.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,708 @@
 | 
			
		||||
/**
 | 
			
		||||
 * ComputerCraft TUI (Terminal User Interface) Framework
 | 
			
		||||
 * Based on Qt signal/slot principles for event handling
 | 
			
		||||
 * Provides input/output, option selection and keyboard event handling
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// Import required types from the ComputerCraft environment
 | 
			
		||||
import { CharEvent, KeyEvent, pullEventAs } from "./event";
 | 
			
		||||
import { CCLog, DAY } from "./ccLog";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Signal and Slot system similar to Qt
 | 
			
		||||
 * Allows components to communicate with each other
 | 
			
		||||
 */
 | 
			
		||||
class Signal<T = void> {
 | 
			
		||||
  private slots: ((data?: T) => void)[] = [];
 | 
			
		||||
 | 
			
		||||
  connect(slot: (data?: T) => void): void {
 | 
			
		||||
    this.slots.push(slot);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  disconnect(slot: (data?: T) => void): void {
 | 
			
		||||
    const index = this.slots.indexOf(slot);
 | 
			
		||||
    if (index !== -1) {
 | 
			
		||||
      this.slots.splice(index, 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  emit(data?: T): void {
 | 
			
		||||
    for (const slot of this.slots) {
 | 
			
		||||
      try {
 | 
			
		||||
        slot(data);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        printError(e);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
abstract class UIObject {
 | 
			
		||||
  private objectName: string;
 | 
			
		||||
  private parent?: UIObject;
 | 
			
		||||
  private children: Record<string, UIObject> = {};
 | 
			
		||||
  private log?: CCLog;
 | 
			
		||||
 | 
			
		||||
  constructor(name: string) {
 | 
			
		||||
    this.objectName = name;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setParent(parent: UIObject) {
 | 
			
		||||
    this.parent = parent;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public addChild(child: UIObject) {
 | 
			
		||||
    this.children[child.objectName] = child;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public removeChild(child: UIObject) {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base class for all UI components
 | 
			
		||||
 */
 | 
			
		||||
abstract class UIComponent extends UIObject {
 | 
			
		||||
  protected x: number;
 | 
			
		||||
  protected y: number;
 | 
			
		||||
  protected width: number;
 | 
			
		||||
  protected height: number;
 | 
			
		||||
  protected visible = true;
 | 
			
		||||
  protected focused = false;
 | 
			
		||||
 | 
			
		||||
  // Signals for UI events
 | 
			
		||||
  public onFocus = new Signal<void>();
 | 
			
		||||
  public onBlur = new Signal<void>();
 | 
			
		||||
  public onKeyPress = new Signal<KeyEvent>();
 | 
			
		||||
  public onMouseClick = new Signal<{ x: number; y: number }>();
 | 
			
		||||
 | 
			
		||||
  constructor(x: number, y: number, width = 0, height = 0) {
 | 
			
		||||
    this.x = x;
 | 
			
		||||
    this.y = y;
 | 
			
		||||
    this.width = width;
 | 
			
		||||
    this.height = height;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Render the component to the terminal
 | 
			
		||||
  abstract render(): void;
 | 
			
		||||
 | 
			
		||||
  // Handle input events
 | 
			
		||||
  // Key
 | 
			
		||||
  abstract handleKeyInput(event: KeyEvent): void;
 | 
			
		||||
  // Char
 | 
			
		||||
  abstract handleCharInput(event: CharEvent): void;
 | 
			
		||||
 | 
			
		||||
  // Get/set focus for the component
 | 
			
		||||
  focus(): void {
 | 
			
		||||
    this.focused = true;
 | 
			
		||||
    this.onFocus.emit();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  unfocus(): void {
 | 
			
		||||
    this.focused = false;
 | 
			
		||||
    this.onBlur.emit();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Show/hide the component
 | 
			
		||||
  show(): void {
 | 
			
		||||
    this.visible = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  hide(): void {
 | 
			
		||||
    this.visible = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Check if a point is inside the component
 | 
			
		||||
  contains(pointX: number, pointY: number): boolean {
 | 
			
		||||
    return (
 | 
			
		||||
      pointX >= this.x &&
 | 
			
		||||
      pointX < this.x + this.width &&
 | 
			
		||||
      pointY >= this.y &&
 | 
			
		||||
      pointY < this.y + this.height
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Getter methods
 | 
			
		||||
  getX(): number {
 | 
			
		||||
    return this.x;
 | 
			
		||||
  }
 | 
			
		||||
  getY(): number {
 | 
			
		||||
    return this.y;
 | 
			
		||||
  }
 | 
			
		||||
  getWidth(): number {
 | 
			
		||||
    return this.width;
 | 
			
		||||
  }
 | 
			
		||||
  getHeight(): number {
 | 
			
		||||
    return this.height;
 | 
			
		||||
  }
 | 
			
		||||
  isVisible(): boolean {
 | 
			
		||||
    return this.visible;
 | 
			
		||||
  }
 | 
			
		||||
  isFocused(): boolean {
 | 
			
		||||
    return this.focused;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Text output component
 | 
			
		||||
 */
 | 
			
		||||
class TextLabel extends UIComponent {
 | 
			
		||||
  private text: string;
 | 
			
		||||
  private textColor: number;
 | 
			
		||||
  private bgColor: number;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    x: number,
 | 
			
		||||
    y: number,
 | 
			
		||||
    text: string,
 | 
			
		||||
    textColor: number = colors.white,
 | 
			
		||||
    bgColor: number = colors.black,
 | 
			
		||||
  ) {
 | 
			
		||||
    super(x, y, text.length, 1);
 | 
			
		||||
    this.text = text;
 | 
			
		||||
    this.textColor = textColor;
 | 
			
		||||
    this.bgColor = bgColor;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render(): void {
 | 
			
		||||
    if (!this.visible) return;
 | 
			
		||||
 | 
			
		||||
    const [originalX, originalY] = term.getCursorPos();
 | 
			
		||||
 | 
			
		||||
    // Set colors
 | 
			
		||||
    term.setTextColor(this.textColor);
 | 
			
		||||
    term.setBackgroundColor(this.bgColor);
 | 
			
		||||
 | 
			
		||||
    // Move cursor to position and draw text
 | 
			
		||||
    term.setCursorPos(this.x, this.y);
 | 
			
		||||
    term.write(this.text);
 | 
			
		||||
 | 
			
		||||
    // Restore original cursor position
 | 
			
		||||
    term.setCursorPos(originalX, originalY);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleKeyInput(_event: KeyEvent): void {
 | 
			
		||||
    // Do nothing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleCharInput(_event: CharEvent): void {
 | 
			
		||||
    // Do nothing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setText(newText: string): void {
 | 
			
		||||
    this.text = newText;
 | 
			
		||||
    this.width = newText.length; // Update width based on new text
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getText(): string {
 | 
			
		||||
    return this.text;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Input field component
 | 
			
		||||
 */
 | 
			
		||||
class InputField extends UIComponent {
 | 
			
		||||
  private value: string;
 | 
			
		||||
  private placeholder: string;
 | 
			
		||||
  private maxLength: number;
 | 
			
		||||
  private password: boolean;
 | 
			
		||||
  private cursorPos = 0;
 | 
			
		||||
  private isCursorBlink = false;
 | 
			
		||||
 | 
			
		||||
  // Signal for when text changes
 | 
			
		||||
  public onTextChanged = new Signal<string>();
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    x: number,
 | 
			
		||||
    y: number,
 | 
			
		||||
    width: number,
 | 
			
		||||
    value: "",
 | 
			
		||||
    placeholder = "",
 | 
			
		||||
    maxLength = 50,
 | 
			
		||||
    password = false,
 | 
			
		||||
  ) {
 | 
			
		||||
    super(x, y, width, 1);
 | 
			
		||||
    this.value = value;
 | 
			
		||||
    this.placeholder = placeholder;
 | 
			
		||||
    this.maxLength = maxLength;
 | 
			
		||||
    this.password = password;
 | 
			
		||||
    this.cursorPos = value.length;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render(): void {
 | 
			
		||||
    if (!this.visible) return;
 | 
			
		||||
 | 
			
		||||
    const [originalX, originalY] = term.getCursorPos();
 | 
			
		||||
 | 
			
		||||
    // Set colors (different for focused vs unfocused)
 | 
			
		||||
    if (this.focused) {
 | 
			
		||||
      term.setTextColor(colors.black);
 | 
			
		||||
      term.setBackgroundColor(colors.white);
 | 
			
		||||
    } else {
 | 
			
		||||
      term.setTextColor(colors.white);
 | 
			
		||||
      term.setBackgroundColor(colors.black);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Move cursor to position
 | 
			
		||||
    term.setCursorPos(this.x, this.y);
 | 
			
		||||
 | 
			
		||||
    // Prepare text to display (mask if password)
 | 
			
		||||
    let displayText = this.value;
 | 
			
		||||
    if (this.password) {
 | 
			
		||||
      displayText = "*".repeat(this.value.length);
 | 
			
		||||
    } else if (this.value === "" && this.placeholder !== "") {
 | 
			
		||||
      displayText = this.placeholder;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Truncate or pad text to fit the field
 | 
			
		||||
    if (displayText.length > this.width) {
 | 
			
		||||
      displayText = displayText.substring(0, this.width);
 | 
			
		||||
    } else {
 | 
			
		||||
      displayText = displayText.padEnd(this.width);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Draw the field
 | 
			
		||||
    term.write(displayText);
 | 
			
		||||
 | 
			
		||||
    // Move cursor to the correct position if focused
 | 
			
		||||
    // if (this.focused) {
 | 
			
		||||
    //   const cursorX = Math.min(
 | 
			
		||||
    //     this.x + this.cursorPos,
 | 
			
		||||
    //     this.x + this.width - 1,
 | 
			
		||||
    //   );
 | 
			
		||||
    //   term.setCursorPos(cursorX, this.y);
 | 
			
		||||
    //   term.setCursorBlink(true);
 | 
			
		||||
    // } else {
 | 
			
		||||
    //   term.setCursorBlink(false);
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    // Restore original cursor position
 | 
			
		||||
    term.setCursorPos(originalX, originalY);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleKeyInput(event: KeyEvent): void {
 | 
			
		||||
    if (!this.focused) return;
 | 
			
		||||
 | 
			
		||||
    this.onKeyPress.emit(event);
 | 
			
		||||
 | 
			
		||||
    const key = event.key;
 | 
			
		||||
    log.debug(`[${InputField.name}]: Get key ${keys.getName(key)}`);
 | 
			
		||||
 | 
			
		||||
    // Handle backspace
 | 
			
		||||
    if (key === keys.backspace) {
 | 
			
		||||
      if (this.cursorPos > 0) {
 | 
			
		||||
        this.value =
 | 
			
		||||
          this.value.substring(0, this.cursorPos - 1) +
 | 
			
		||||
          this.value.substring(this.cursorPos);
 | 
			
		||||
        this.cursorPos--;
 | 
			
		||||
        this.onTextChanged.emit(this.value);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Handle delete
 | 
			
		||||
    else if (key === keys.delete) {
 | 
			
		||||
      if (this.cursorPos < this.value.length) {
 | 
			
		||||
        this.value =
 | 
			
		||||
          this.value.substring(0, this.cursorPos) +
 | 
			
		||||
          this.value.substring(this.cursorPos + 1);
 | 
			
		||||
        this.onTextChanged.emit(this.value);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Handle left arrow
 | 
			
		||||
    else if (key === keys.left) {
 | 
			
		||||
      if (this.cursorPos > 0) {
 | 
			
		||||
        this.cursorPos--;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Handle right arrow
 | 
			
		||||
    else if (key === keys.right) {
 | 
			
		||||
      if (this.cursorPos < this.value.length) {
 | 
			
		||||
        this.cursorPos++;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Handle enter (could be used to submit form)
 | 
			
		||||
    else if (key === keys.enter) {
 | 
			
		||||
      // Could emit a submit signal here
 | 
			
		||||
    }
 | 
			
		||||
    // Handle regular characters
 | 
			
		||||
    else {
 | 
			
		||||
      // For printable characters, we need to check if they are actual characters
 | 
			
		||||
      // Since the event system is complex, we'll implement a more direct approach
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleCharInput(event: CharEvent): void {
 | 
			
		||||
    if (!this.focused) return;
 | 
			
		||||
 | 
			
		||||
    const character = event.character;
 | 
			
		||||
    log.debug(`[${InputField.name}]: Get character ${character}`);
 | 
			
		||||
 | 
			
		||||
    this.value += character;
 | 
			
		||||
    this.cursorPos++;
 | 
			
		||||
    this.onTextChanged.emit(this.value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Method to get user input directly (more suitable for ComputerCraft)
 | 
			
		||||
  readInput(prompt = "", defaultValue = ""): string {
 | 
			
		||||
    // Since we can't await for events in a standard way in this context,
 | 
			
		||||
    // we'll use CC's read function which handles input internally
 | 
			
		||||
    const oldX = this.x;
 | 
			
		||||
    const oldY = this.y;
 | 
			
		||||
 | 
			
		||||
    // Move cursor to the input field position
 | 
			
		||||
    term.setCursorPos(oldX, oldY);
 | 
			
		||||
 | 
			
		||||
    // Print the prompt
 | 
			
		||||
    if (prompt != undefined) {
 | 
			
		||||
      print(prompt);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Use ComputerCraft's read function for actual input
 | 
			
		||||
    const result = read(undefined, undefined, undefined, defaultValue);
 | 
			
		||||
    return result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getValue(): string {
 | 
			
		||||
    return this.value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setValue(value: string): void {
 | 
			
		||||
    this.value = value.substring(0, this.maxLength);
 | 
			
		||||
    this.cursorPos = value.length;
 | 
			
		||||
    this.onTextChanged.emit(this.value);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Option selection component with prompt
 | 
			
		||||
 */
 | 
			
		||||
class OptionSelector extends UIComponent {
 | 
			
		||||
  private options: string[];
 | 
			
		||||
  private currentIndex: number;
 | 
			
		||||
  private prompt: string;
 | 
			
		||||
  private displayOption: string;
 | 
			
		||||
 | 
			
		||||
  // Signal for when selection changes
 | 
			
		||||
  public onSelectionChanged = new Signal<{ index: number; value: string }>();
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    x: number,
 | 
			
		||||
    y: number,
 | 
			
		||||
    options: string[],
 | 
			
		||||
    prompt = "Select:",
 | 
			
		||||
    initialIndex = 0,
 | 
			
		||||
  ) {
 | 
			
		||||
    super(x, y, 0, 1); // Width will be calculated dynamically
 | 
			
		||||
    this.options = options;
 | 
			
		||||
    this.currentIndex = initialIndex;
 | 
			
		||||
    this.prompt = prompt;
 | 
			
		||||
 | 
			
		||||
    // Calculate width based on prompt and longest option
 | 
			
		||||
    const promptWidth = prompt.length + 1; // +1 for space
 | 
			
		||||
    let maxOptionWidth = 0;
 | 
			
		||||
    for (const option of options) {
 | 
			
		||||
      if (option.length > maxOptionWidth) {
 | 
			
		||||
        maxOptionWidth = option.length;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.width = promptWidth + maxOptionWidth + 3; // +3 for brackets and space [ ]
 | 
			
		||||
 | 
			
		||||
    this.displayOption = `[${this.options[this.currentIndex]}]`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render(): void {
 | 
			
		||||
    if (!this.visible) return;
 | 
			
		||||
 | 
			
		||||
    const [originalX, originalY] = term.getCursorPos();
 | 
			
		||||
 | 
			
		||||
    // Set colors
 | 
			
		||||
    term.setTextColor(this.focused ? colors.yellow : colors.white);
 | 
			
		||||
    term.setBackgroundColor(colors.black);
 | 
			
		||||
 | 
			
		||||
    // Move cursor to position
 | 
			
		||||
    term.setCursorPos(this.x, this.y);
 | 
			
		||||
 | 
			
		||||
    // Draw prompt and selected option
 | 
			
		||||
    const fullText = `${this.prompt} ${this.displayOption}`;
 | 
			
		||||
    term.write(fullText.padEnd(this.width));
 | 
			
		||||
 | 
			
		||||
    // Restore original cursor position
 | 
			
		||||
    term.setCursorPos(originalX, originalY);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleKeyInput(event: KeyEvent): void {
 | 
			
		||||
    if (!this.focused) return;
 | 
			
		||||
 | 
			
		||||
    this.onKeyPress.emit(event);
 | 
			
		||||
 | 
			
		||||
    const key = event.key;
 | 
			
		||||
 | 
			
		||||
    // Handle left arrow to go to previous option
 | 
			
		||||
    if (key === keys.left) {
 | 
			
		||||
      this.previousOption();
 | 
			
		||||
    }
 | 
			
		||||
    // Handle right arrow to go to next option
 | 
			
		||||
    else if (key === keys.right) {
 | 
			
		||||
      this.nextOption();
 | 
			
		||||
    }
 | 
			
		||||
    // Handle up arrow to go to previous option
 | 
			
		||||
    else if (key === keys.up) {
 | 
			
		||||
      this.previousOption();
 | 
			
		||||
    }
 | 
			
		||||
    // Handle down arrow to go to next option
 | 
			
		||||
    else if (key === keys.down) {
 | 
			
		||||
      this.nextOption();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleCharInput(_event: CharEvent): void {
 | 
			
		||||
    //Do nothing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private previousOption(): void {
 | 
			
		||||
    this.currentIndex =
 | 
			
		||||
      (this.currentIndex - 1 + this.options.length) % this.options.length;
 | 
			
		||||
    this.updateDisplay();
 | 
			
		||||
    this.onSelectionChanged.emit({
 | 
			
		||||
      index: this.currentIndex,
 | 
			
		||||
      value: this.options[this.currentIndex],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private nextOption(): void {
 | 
			
		||||
    this.currentIndex = (this.currentIndex + 1) % this.options.length;
 | 
			
		||||
    this.updateDisplay();
 | 
			
		||||
    this.onSelectionChanged.emit({
 | 
			
		||||
      index: this.currentIndex,
 | 
			
		||||
      value: this.options[this.currentIndex],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateDisplay(): void {
 | 
			
		||||
    this.displayOption = `[${this.options[this.currentIndex]}]`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getSelectedIndex(): number {
 | 
			
		||||
    return this.currentIndex;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getSelectedValue(): string {
 | 
			
		||||
    return this.options[this.currentIndex];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setSelectedIndex(index: number): void {
 | 
			
		||||
    if (index >= 0 && index < this.options.length) {
 | 
			
		||||
      this.currentIndex = index;
 | 
			
		||||
      this.updateDisplay();
 | 
			
		||||
      this.onSelectionChanged.emit({
 | 
			
		||||
        index: this.currentIndex,
 | 
			
		||||
        value: this.options[this.currentIndex],
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setOptions(newOptions: string[]): void {
 | 
			
		||||
    this.options = newOptions;
 | 
			
		||||
    if (this.currentIndex >= newOptions.length) {
 | 
			
		||||
      this.currentIndex = 0;
 | 
			
		||||
    }
 | 
			
		||||
    this.updateDisplay();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Base Window class to manage UI components
 | 
			
		||||
 */
 | 
			
		||||
class UIWindow {
 | 
			
		||||
  private components: UIComponent[] = [];
 | 
			
		||||
  private focusedComponentIndex = -1;
 | 
			
		||||
 | 
			
		||||
  addComponent(component: UIComponent, manager: GlobalManager): void {
 | 
			
		||||
    this.components.push(component);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeComponent(component: UIComponent): void {
 | 
			
		||||
    const index = this.components.indexOf(component);
 | 
			
		||||
    if (index !== -1) {
 | 
			
		||||
      this.components.splice(index, 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render(): void {
 | 
			
		||||
    // Clear the terminal
 | 
			
		||||
    term.clear();
 | 
			
		||||
    term.setCursorPos(1, 1);
 | 
			
		||||
 | 
			
		||||
    // Render all visible components
 | 
			
		||||
    for (const component of this.components) {
 | 
			
		||||
      if (component.isVisible()) {
 | 
			
		||||
        component.render();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleKeyInput(event: KeyEvent): void {
 | 
			
		||||
    // Handle input for the currently focused component
 | 
			
		||||
    if (
 | 
			
		||||
      this.focusedComponentIndex >= 0 &&
 | 
			
		||||
      this.focusedComponentIndex < this.components.length
 | 
			
		||||
    ) {
 | 
			
		||||
      const focusedComponent = this.components[this.focusedComponentIndex];
 | 
			
		||||
      if (focusedComponent.isFocused()) {
 | 
			
		||||
        focusedComponent.handleKeyInput(event);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleCharInput(event: CharEvent): void {
 | 
			
		||||
    // Handle input for the currently focused component
 | 
			
		||||
    if (
 | 
			
		||||
      this.focusedComponentIndex >= 0 &&
 | 
			
		||||
      this.focusedComponentIndex < this.components.length
 | 
			
		||||
    ) {
 | 
			
		||||
      const focusedComponent = this.components[this.focusedComponentIndex];
 | 
			
		||||
      if (focusedComponent.isFocused()) {
 | 
			
		||||
        focusedComponent.handleCharInput(event);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setFocus(index: number): void {
 | 
			
		||||
    // Unfocus current component
 | 
			
		||||
    if (
 | 
			
		||||
      this.focusedComponentIndex >= 0 &&
 | 
			
		||||
      this.focusedComponentIndex < this.components.length
 | 
			
		||||
    ) {
 | 
			
		||||
      this.components[this.focusedComponentIndex].unfocus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Change focus
 | 
			
		||||
    this.focusedComponentIndex = index;
 | 
			
		||||
 | 
			
		||||
    // Focus new component
 | 
			
		||||
    if (
 | 
			
		||||
      this.focusedComponentIndex >= 0 &&
 | 
			
		||||
      this.focusedComponentIndex < this.components.length
 | 
			
		||||
    ) {
 | 
			
		||||
      this.components[this.focusedComponentIndex].focus();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setFocusFor(component: UIComponent): void {
 | 
			
		||||
    const index = this.components.indexOf(component);
 | 
			
		||||
    if (index !== -1) {
 | 
			
		||||
      this.setFocus(index);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getComponent(index: number): UIComponent | undefined {
 | 
			
		||||
    return this.components[index];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getComponents(): UIComponent[] {
 | 
			
		||||
    return this.components;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clear(): void {
 | 
			
		||||
    term.clear();
 | 
			
		||||
    term.setCursorPos(1, 1);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Main TUI Application class
 | 
			
		||||
 */
 | 
			
		||||
class TUIApplication {
 | 
			
		||||
  private log = new CCLog(`TUI.log`, false, DAY);
 | 
			
		||||
  private manager: GlobalManager;
 | 
			
		||||
 | 
			
		||||
  private window: UIWindow;
 | 
			
		||||
  private running = false;
 | 
			
		||||
  private keyEvent?: KeyEvent;
 | 
			
		||||
  private charEvent?: CharEvent;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.window = new UIWindow();
 | 
			
		||||
    this.manager = {
 | 
			
		||||
      log: this.log,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addComponent(component: UIComponent): void {
 | 
			
		||||
    this.window.addComponent(component, this.manager);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  run(): void {
 | 
			
		||||
    this.running = true;
 | 
			
		||||
 | 
			
		||||
    // Initial render
 | 
			
		||||
    term.setCursorBlink(false);
 | 
			
		||||
    this.window.render();
 | 
			
		||||
 | 
			
		||||
    parallel.waitForAll(
 | 
			
		||||
      () => {
 | 
			
		||||
        this.mainLoop();
 | 
			
		||||
      },
 | 
			
		||||
      () => {
 | 
			
		||||
        this.keyLoop();
 | 
			
		||||
      },
 | 
			
		||||
      () => {
 | 
			
		||||
        this.charLoop();
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  stop(): void {
 | 
			
		||||
    this.running = false;
 | 
			
		||||
    this.manager.log.close();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  mainLoop(): void {
 | 
			
		||||
    // Main event loop
 | 
			
		||||
    while (this.running) {
 | 
			
		||||
      // Render the UI
 | 
			
		||||
      this.window.render();
 | 
			
		||||
 | 
			
		||||
      // Small delay to prevent excessive CPU usage
 | 
			
		||||
      os.sleep(0.05);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  keyLoop(): void {
 | 
			
		||||
    while (this.running) {
 | 
			
		||||
      // Handle input events
 | 
			
		||||
      this.keyEvent = pullEventAs(KeyEvent, "key");
 | 
			
		||||
      this.manager.log.debug(
 | 
			
		||||
        `[${TUIApplication.name}]: Get Key Event: ${textutils.serialise(this.keyEvent ?? {})}`,
 | 
			
		||||
      );
 | 
			
		||||
      if (this.keyEvent == undefined) continue;
 | 
			
		||||
      this.window.handleKeyInput(this.keyEvent);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  charLoop(): void {
 | 
			
		||||
    while (this.running) {
 | 
			
		||||
      // Handle input events
 | 
			
		||||
      this.charEvent = pullEventAs(CharEvent, "char");
 | 
			
		||||
      this.manager.log.debug(
 | 
			
		||||
        `[${TUIApplication.name}]: Get Char Event: ${textutils.serialise(this.charEvent ?? {})}`,
 | 
			
		||||
      );
 | 
			
		||||
      if (this.charEvent == undefined) continue;
 | 
			
		||||
      this.window.handleCharInput(this.charEvent);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getWindow(): UIWindow {
 | 
			
		||||
    return this.window;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Export the main classes for use in other modules
 | 
			
		||||
export {
 | 
			
		||||
  Signal,
 | 
			
		||||
  UIComponent,
 | 
			
		||||
  TextLabel,
 | 
			
		||||
  InputField,
 | 
			
		||||
  OptionSelector,
 | 
			
		||||
  UIWindow,
 | 
			
		||||
  TUIApplication,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										77
									
								
								src/tuiExample/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								src/tuiExample/main.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,77 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Example usage of the ComputerCraft TUI framework
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  TUIApplication,
 | 
			
		||||
  TextLabel,
 | 
			
		||||
  InputField,
 | 
			
		||||
  OptionSelector,
 | 
			
		||||
} 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(
 | 
			
		||||
  Math.floor(termWidth / 2) - 10,
 | 
			
		||||
  2,
 | 
			
		||||
  "CC TUI Framework Demo",
 | 
			
		||||
  colors.yellow,
 | 
			
		||||
  colors.black,
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const label1 = new TextLabel(5, 5, "Enter your name:");
 | 
			
		||||
 | 
			
		||||
const inputField = new InputField(5, 6, 30, "", "Type here...");
 | 
			
		||||
 | 
			
		||||
const optionLabel = new TextLabel(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 statusLabel = new TextLabel(5, 11, "Status: Ready");
 | 
			
		||||
 | 
			
		||||
// Add components to the application
 | 
			
		||||
app.addComponent(title);
 | 
			
		||||
app.addComponent(label1);
 | 
			
		||||
app.addComponent(inputField);
 | 
			
		||||
app.addComponent(optionLabel);
 | 
			
		||||
app.addComponent(optionSelector);
 | 
			
		||||
app.addComponent(statusLabel);
 | 
			
		||||
 | 
			
		||||
// Set focus to the input field initially
 | 
			
		||||
app.getWindow().setFocusFor(optionSelector);
 | 
			
		||||
 | 
			
		||||
// Connect events
 | 
			
		||||
optionSelector.onSelectionChanged.connect((data) => {
 | 
			
		||||
  statusLabel.setText(
 | 
			
		||||
    `Status: Selected ${data?.value} (index: ${data?.index})`,
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
inputField.onTextChanged.connect((value) => {
 | 
			
		||||
  if (value != undefined && value.length > 0) {
 | 
			
		||||
    statusLabel.setText(`Status: Input changed to "${value}"`);
 | 
			
		||||
  } else {
 | 
			
		||||
    statusLabel.setText("Status: Input cleared");
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Run the application
 | 
			
		||||
try {
 | 
			
		||||
  print("Starting CC TUI Demo. Press Ctrl+T to quit.");
 | 
			
		||||
  app.run();
 | 
			
		||||
} catch (e) {
 | 
			
		||||
  if (e === "Terminated") {
 | 
			
		||||
    print("Application terminated by user.");
 | 
			
		||||
  } else {
 | 
			
		||||
    print(`Error running application:`);
 | 
			
		||||
    printError(e);
 | 
			
		||||
  }
 | 
			
		||||
} finally {
 | 
			
		||||
  app.stop();
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								tsconfig.tuiExample.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								tsconfig.tuiExample.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://raw.githubusercontent.com/MCJack123/TypeScriptToLua/master/tsconfig-schema.json",
 | 
			
		||||
  "extends": "./tsconfig.json",
 | 
			
		||||
  "tstl": {
 | 
			
		||||
    "luaBundle": "build/tuiExample.lua",
 | 
			
		||||
    "luaBundleEntry": "src/tuiExample/main.ts"
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src/tuiExample/*.ts"]
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user