mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-12-20 13:37:49 +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/
|
event/
|
||||||
build/
|
build/
|
||||||
reference/
|
reference/
|
||||||
|
|
||||||
|
QWEN.md
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]
|
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:
|
build-autocraft:
|
||||||
pnpm tstl -p ./tsconfig.autocraft.json
|
pnpm tstl -p ./tsconfig.autocraft.json
|
||||||
@@ -12,5 +12,8 @@ build-accesscontrol:
|
|||||||
build-test:
|
build-test:
|
||||||
pnpm tstl -p ./tsconfig.test.json
|
pnpm tstl -p ./tsconfig.test.json
|
||||||
|
|
||||||
|
build-example:
|
||||||
|
pnpm tstl -p ./tsconfig.tuiExample.json
|
||||||
|
|
||||||
sync:
|
sync:
|
||||||
cp -r "./build/*" "C:\\Users\\sikongjueluo\\AppData\\Roaming\\CraftOS-PC\\computer\\0\\user\\"
|
cp -r "./build/*" "C:\\Users\\sikongjueluo\\AppData\\Roaming\\CraftOS-PC\\computer\\0\\user\\"
|
||||||
|
|||||||
@@ -1,20 +1,55 @@
|
|||||||
{
|
{
|
||||||
"detectRange": 64,
|
"detectRange": 256,
|
||||||
"detectInterval": 1,
|
"detectInterval": 1,
|
||||||
"warnInterval": 7,
|
"watchInterval": 10,
|
||||||
|
"noticeTimes": 2,
|
||||||
|
"isWarn": false,
|
||||||
"adminGroupConfig": {
|
"adminGroupConfig": {
|
||||||
"groupName": "Admin",
|
"groupName": "Admin",
|
||||||
"groupUsers": ["Selcon"],
|
"groupUsers": ["Selcon"],
|
||||||
"isAllowed": true,
|
"isAllowed": true,
|
||||||
"isNotice": 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": {
|
"title": {
|
||||||
"text": "Welcome",
|
"text": "Welcome",
|
||||||
"color": "green"
|
"color": "green"
|
||||||
},
|
},
|
||||||
"msg": {
|
"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"
|
"color": "green"
|
||||||
},
|
},
|
||||||
"prefix": "Taohuayuan",
|
"prefix": "Taohuayuan",
|
||||||
@@ -33,35 +68,5 @@
|
|||||||
"prefix": "Taohuayuan",
|
"prefix": "Taohuayuan",
|
||||||
"brackets": "[]",
|
"brackets": "[]",
|
||||||
"bracketColor": ""
|
"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) {
|
switch (option) {
|
||||||
case "warnInterval":
|
case "warnInterval":
|
||||||
context.config.warnInterval = value;
|
context.config.watchInterval = value;
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Set warn interval to ${context.config.warnInterval}`,
|
message: `Set warn interval to ${context.config.watchInterval}`,
|
||||||
shouldSaveConfig: true,
|
shouldSaveConfig: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -549,20 +549,6 @@ class ShowConfigCommand implements CLICommand {
|
|||||||
groupsMessage += ` Users: [${(group.groupUsers ?? []).join(", ")}]\n`;
|
groupsMessage += ` Users: [${(group.groupUsers ?? []).join(", ")}]\n`;
|
||||||
groupsMessage += ` Allowed: ${group.isAllowed}\n`;
|
groupsMessage += ` Allowed: ${group.isAllowed}\n`;
|
||||||
groupsMessage += ` Notice: ${group.isNotice}\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";
|
groupsMessage += "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,11 +560,11 @@ class ShowConfigCommand implements CLICommand {
|
|||||||
|
|
||||||
case "toast": {
|
case "toast": {
|
||||||
let toastMessage = "Default Toast Config:\n";
|
let toastMessage = "Default Toast Config:\n";
|
||||||
toastMessage += ` Title: ${context.config.defaultToastConfig.title.text}\n`;
|
toastMessage += ` Title: ${context.config.welcomeToastConfig.title.text}\n`;
|
||||||
toastMessage += ` Message: ${context.config.defaultToastConfig.msg.text}\n`;
|
toastMessage += ` Message: ${context.config.welcomeToastConfig.msg.text}\n`;
|
||||||
toastMessage += ` Prefix: ${context.config.defaultToastConfig.prefix ?? "none"}\n`;
|
toastMessage += ` Prefix: ${context.config.welcomeToastConfig.prefix ?? "none"}\n`;
|
||||||
toastMessage += ` Brackets: ${context.config.defaultToastConfig.brackets ?? "none"}\n`;
|
toastMessage += ` Brackets: ${context.config.welcomeToastConfig.brackets ?? "none"}\n`;
|
||||||
toastMessage += ` Bracket Color: ${context.config.defaultToastConfig.bracketColor ?? "none"}\n\n`;
|
toastMessage += ` Bracket Color: ${context.config.welcomeToastConfig.bracketColor ?? "none"}\n\n`;
|
||||||
|
|
||||||
toastMessage += "Warn Toast Config:\n";
|
toastMessage += "Warn Toast Config:\n";
|
||||||
toastMessage += ` Title: ${context.config.warnToastConfig.title.text}\n`;
|
toastMessage += ` Title: ${context.config.warnToastConfig.title.text}\n`;
|
||||||
@@ -596,7 +582,7 @@ class ShowConfigCommand implements CLICommand {
|
|||||||
case "all": {
|
case "all": {
|
||||||
let allMessage = `Detect Range: ${context.config.detectRange}\n`;
|
let allMessage = `Detect Range: ${context.config.detectRange}\n`;
|
||||||
allMessage += `Detect Interval: ${context.config.detectInterval}\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 +=
|
allMessage +=
|
||||||
"Use 'showconfig groups' or 'showconfig toast' for detailed view";
|
"Use 'showconfig groups' or 'showconfig toast' for detailed view";
|
||||||
|
|
||||||
|
|||||||
@@ -16,28 +16,32 @@ interface UserGroupConfig {
|
|||||||
isAllowed: boolean;
|
isAllowed: boolean;
|
||||||
isNotice: boolean;
|
isNotice: boolean;
|
||||||
groupUsers: string[];
|
groupUsers: string[];
|
||||||
toastConfig?: ToastConfig;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AccessConfig {
|
interface AccessConfig {
|
||||||
detectInterval: number;
|
detectInterval: number;
|
||||||
warnInterval: number;
|
watchInterval: number;
|
||||||
|
noticeTimes: number;
|
||||||
detectRange: number;
|
detectRange: number;
|
||||||
|
isWarn: boolean;
|
||||||
adminGroupConfig: UserGroupConfig;
|
adminGroupConfig: UserGroupConfig;
|
||||||
defaultToastConfig: ToastConfig;
|
welcomeToastConfig: ToastConfig;
|
||||||
warnToastConfig: ToastConfig;
|
warnToastConfig: ToastConfig;
|
||||||
|
noticeToastConfig: ToastConfig;
|
||||||
usersGroups: UserGroupConfig[];
|
usersGroups: UserGroupConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultConfig: AccessConfig = {
|
const defaultConfig: AccessConfig = {
|
||||||
detectRange: 64,
|
detectRange: 256,
|
||||||
detectInterval: 3,
|
detectInterval: 1,
|
||||||
warnInterval: 7,
|
watchInterval: 10,
|
||||||
|
noticeTimes: 2,
|
||||||
|
isWarn: false,
|
||||||
adminGroupConfig: {
|
adminGroupConfig: {
|
||||||
groupName: "Admin",
|
groupName: "Admin",
|
||||||
groupUsers: ["Selcon"],
|
groupUsers: ["Selcon"],
|
||||||
isAllowed: true,
|
isAllowed: true,
|
||||||
isNotice: false,
|
isNotice: true,
|
||||||
},
|
},
|
||||||
usersGroups: [
|
usersGroups: [
|
||||||
{
|
{
|
||||||
@@ -57,19 +61,22 @@ const defaultConfig: AccessConfig = {
|
|||||||
groupUsers: [],
|
groupUsers: [],
|
||||||
isAllowed: false,
|
isAllowed: false,
|
||||||
isNotice: 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: {
|
title: {
|
||||||
text: "Welcome",
|
text: "Welcome",
|
||||||
color: "green",
|
color: "green",
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ const config = loadConfig(configFilepath);
|
|||||||
log.info("Load config successfully!");
|
log.info("Load config successfully!");
|
||||||
if (DEBUG) log.debug(textutils.serialise(config, { allow_repetitions: true }));
|
if (DEBUG) log.debug(textutils.serialise(config, { allow_repetitions: true }));
|
||||||
const groupNames = config.usersGroups.map((value) => value.groupName);
|
const groupNames = config.usersGroups.map((value) => value.groupName);
|
||||||
let warnTargetPlayers: string[];
|
let noticeTargetPlayers: string[];
|
||||||
const playerDetector = peripheralManager.findByNameRequired("playerDetector");
|
const playerDetector = peripheralManager.findByNameRequired("playerDetector");
|
||||||
const chatBox = peripheralManager.findByNameRequired("chatBox");
|
const chatBox = peripheralManager.findByNameRequired("chatBox");
|
||||||
|
|
||||||
let inRangePlayers: string[] = [];
|
let inRangePlayers: string[] = [];
|
||||||
let notAllowedPlayers: string[] = [];
|
let watchPlayersInfo: { name: string; hasNoticeTimes: number }[] = [];
|
||||||
|
|
||||||
function safeParseTextComponent(
|
function safeParseTextComponent(
|
||||||
component: MinecraftTextComponent,
|
component: MinecraftTextComponent,
|
||||||
@@ -38,43 +38,50 @@ function safeParseTextComponent(
|
|||||||
return textutils.serialiseJSON(component);
|
return textutils.serialiseJSON(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendToast(
|
function sendToast(toastConfig: ToastConfig, targetPlayer: string) {
|
||||||
toastConfig: ToastConfig,
|
|
||||||
player: string,
|
|
||||||
groupConfig?: UserGroupConfig,
|
|
||||||
) {
|
|
||||||
return chatBox.sendFormattedToastToPlayer(
|
return chatBox.sendFormattedToastToPlayer(
|
||||||
safeParseTextComponent(
|
textutils.serialiseJSON(toastConfig.msg ?? config.welcomeToastConfig.msg),
|
||||||
toastConfig.msg ?? config.defaultToastConfig.msg,
|
textutils.serialiseJSON(
|
||||||
player,
|
toastConfig.title ?? config.welcomeToastConfig.title,
|
||||||
groupConfig?.groupName,
|
|
||||||
),
|
),
|
||||||
safeParseTextComponent(
|
targetPlayer,
|
||||||
toastConfig.title ?? config.defaultToastConfig.title,
|
toastConfig.prefix ?? config.welcomeToastConfig.prefix,
|
||||||
player,
|
toastConfig.brackets ?? config.welcomeToastConfig.brackets,
|
||||||
groupConfig?.groupName,
|
toastConfig.bracketColor ?? config.welcomeToastConfig.bracketColor,
|
||||||
),
|
|
||||||
player,
|
|
||||||
toastConfig.prefix ?? config.defaultToastConfig.prefix,
|
|
||||||
toastConfig.brackets ?? config.defaultToastConfig.brackets,
|
|
||||||
toastConfig.bracketColor ?? config.defaultToastConfig.bracketColor,
|
|
||||||
undefined,
|
undefined,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendWarnAndNotice(player: string) {
|
function sendNotice(player: string, playerInfo?: PlayerInfo) {
|
||||||
const playerPos = playerDetector.getPlayerPos(player);
|
|
||||||
const onlinePlayers = playerDetector.getOnlinePlayers();
|
const onlinePlayers = playerDetector.getOnlinePlayers();
|
||||||
warnTargetPlayers = config.adminGroupConfig.groupUsers.concat(
|
noticeTargetPlayers = config.adminGroupConfig.groupUsers.concat(
|
||||||
config.usersGroups
|
config.usersGroups
|
||||||
.filter((value) => value.isNotice)
|
.filter((value) => value.isNotice)
|
||||||
.map((value) => value.groupUsers ?? [])
|
.map((value) => value.groupUsers ?? [])
|
||||||
.flat(),
|
.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);
|
log.warn(warnMsg);
|
||||||
|
|
||||||
sendToast(config.warnToastConfig, player);
|
sendToast(config.warnToastConfig, player);
|
||||||
chatBox.sendFormattedMessageToPlayer(
|
chatBox.sendFormattedMessageToPlayer(
|
||||||
safeParseTextComponent(config.warnToastConfig.msg, player),
|
safeParseTextComponent(config.warnToastConfig.msg, player),
|
||||||
@@ -85,37 +92,36 @@ function sendWarnAndNotice(player: string) {
|
|||||||
undefined,
|
undefined,
|
||||||
true,
|
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) {
|
while (true) {
|
||||||
for (const player of notAllowedPlayers) {
|
for (const player of watchPlayersInfo) {
|
||||||
if (inRangePlayers.includes(player)) {
|
if (inRangePlayers.includes(player.name)) {
|
||||||
// sendWarnAndNotice(player);
|
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 {
|
} else {
|
||||||
notAllowedPlayers = notAllowedPlayers.filter(
|
// Get rid of player from list
|
||||||
(value) => value != player,
|
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 (inRangePlayers.includes(player)) continue;
|
||||||
|
|
||||||
if (config.adminGroupConfig.groupUsers.includes(player)) {
|
if (config.adminGroupConfig.groupUsers.includes(player)) {
|
||||||
log.info(`Admin ${player} enter`);
|
log.info(`Admin ${player} appear`);
|
||||||
sendToast(
|
|
||||||
config.adminGroupConfig.toastConfig ?? config.defaultToastConfig,
|
|
||||||
player,
|
|
||||||
config.adminGroupConfig,
|
|
||||||
);
|
|
||||||
continue;
|
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) {
|
for (const userGroupConfig of config.usersGroups) {
|
||||||
if (userGroupConfig.groupUsers == undefined) continue;
|
if (userGroupConfig.groupUsers == undefined) continue;
|
||||||
if (!userGroupConfig.groupUsers.includes(player)) continue;
|
if (!userGroupConfig.groupUsers.includes(player)) continue;
|
||||||
|
|
||||||
if (!userGroupConfig.isAllowed) {
|
groupConfig = userGroupConfig;
|
||||||
sendWarnAndNotice(player);
|
log.info(
|
||||||
notAllowedPlayers.push(player);
|
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info(`${userGroupConfig.groupName} ${player} enter`);
|
|
||||||
sendToast(
|
|
||||||
userGroupConfig.toastConfig ?? config.defaultToastConfig,
|
|
||||||
player,
|
|
||||||
userGroupConfig,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
inUserGroup = true;
|
break;
|
||||||
}
|
}
|
||||||
if (inUserGroup) continue;
|
if (groupConfig.isAllowed) continue;
|
||||||
|
|
||||||
sendWarnAndNotice(player);
|
log.warn(
|
||||||
notAllowedPlayers.push(player);
|
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||||
|
);
|
||||||
|
if (config.isWarn) sendWarn(player);
|
||||||
|
watchPlayersInfo.push({ name: player, hasNoticeTimes: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
inRangePlayers = players;
|
inRangePlayers = players;
|
||||||
@@ -192,7 +195,7 @@ function main(args: string[]) {
|
|||||||
void cli.startConfigLoop();
|
void cli.startConfigLoop();
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
warnLoop();
|
watchLoop();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return;
|
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 interval: number;
|
||||||
private startTime: number;
|
private startTime: number;
|
||||||
private currentTimePeriod: string;
|
private currentTimePeriod: string;
|
||||||
|
private inTerm: boolean;
|
||||||
|
|
||||||
constructor(filename?: string, interval: number = DAY) {
|
constructor(filename?: string, inTerm = true, interval: number = DAY) {
|
||||||
term.clear();
|
term.clear();
|
||||||
term.setCursorPos(1, 1);
|
term.setCursorPos(1, 1);
|
||||||
|
|
||||||
this.interval = interval;
|
this.interval = interval;
|
||||||
|
this.inTerm = inTerm;
|
||||||
this.startTime = os.time(os.date("*t"));
|
this.startTime = os.time(os.date("*t"));
|
||||||
this.currentTimePeriod = this.getTimePeriodString(this.startTime);
|
this.currentTimePeriod = this.getTimePeriodString(this.startTime);
|
||||||
|
|
||||||
@@ -117,21 +119,23 @@ export class CCLog {
|
|||||||
// Check if we need to rotate the log file
|
// Check if we need to rotate the log file
|
||||||
this.checkAndRotateLogFile();
|
this.checkAndRotateLogFile();
|
||||||
|
|
||||||
let originalColor: Color = 0;
|
if (this.inTerm) {
|
||||||
if (color != undefined) {
|
let originalColor: Color = 0;
|
||||||
originalColor = term.getTextColor();
|
if (color != undefined) {
|
||||||
term.setTextColor(color);
|
originalColor = term.getTextColor();
|
||||||
|
term.setTextColor(color);
|
||||||
|
}
|
||||||
|
print(msg);
|
||||||
|
|
||||||
|
if (color != undefined) {
|
||||||
|
term.setTextColor(originalColor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
print(msg);
|
|
||||||
if (this.fp != undefined) {
|
if (this.fp != undefined) {
|
||||||
this.fp.write(msg + "\r\n");
|
this.fp.write(msg + "\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (color != undefined) {
|
|
||||||
term.setTextColor(originalColor);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public debug(msg: string) {
|
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