mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-05 03:37:50 +08:00
add tui framework but not finish, reconstruct accesscontrol
This commit is contained in:
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user