mirror of
https://github.com/SikongJueluo/cc-utils.git
synced 2025-11-04 19:27:50 +08:00
Compare commits
3 Commits
d41117cecc
...
b9ce947b9b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9ce947b9b | ||
|
|
da2c6c1ebb | ||
|
|
c85c072376 |
@@ -9,7 +9,6 @@ build-autocraft:
|
||||
|
||||
build-accesscontrol:
|
||||
pnpm tstl -p ./tsconfig.accesscontrol.json
|
||||
cp ./src/accesscontrol/access.config.json ./build/
|
||||
|
||||
build-test:
|
||||
pnpm tstl -p ./tsconfig.test.json
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"detectRange": 256,
|
||||
"detectInterval": 1,
|
||||
"watchInterval": 10,
|
||||
"noticeTimes": 2,
|
||||
"isWarn": false,
|
||||
"adminGroupConfig": {
|
||||
"groupName": "Admin",
|
||||
"groupUsers": ["Selcon"],
|
||||
"isAllowed": true,
|
||||
"isNotice": true
|
||||
},
|
||||
"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 User %playerName%",
|
||||
"color": "green"
|
||||
},
|
||||
"prefix": "Taohuayuan",
|
||||
"brackets": "[]",
|
||||
"bracketColor": ""
|
||||
},
|
||||
"noticeToastConfig": {
|
||||
"title": {
|
||||
"text": "Welcome",
|
||||
"color": "green"
|
||||
},
|
||||
"msg": {
|
||||
"text": "Hello User %playerName%",
|
||||
"color": "green"
|
||||
},
|
||||
"prefix": "Taohuayuan",
|
||||
"brackets": "[]",
|
||||
"bracketColor": ""
|
||||
},
|
||||
"warnToastConfig": {
|
||||
"title": {
|
||||
"text": "Attention!!!",
|
||||
"color": "red"
|
||||
},
|
||||
"msg": {
|
||||
"text": "%playerName% you are not allowed to be here",
|
||||
"color": "red"
|
||||
},
|
||||
"prefix": "Taohuayuan",
|
||||
"brackets": "[]",
|
||||
"bracketColor": ""
|
||||
}
|
||||
}
|
||||
@@ -75,12 +75,12 @@ const defaultConfig: AccessConfig = {
|
||||
},
|
||||
noticeToastConfig: {
|
||||
title: {
|
||||
text: "Welcome",
|
||||
color: "green",
|
||||
text: "Notice",
|
||||
color: "red",
|
||||
},
|
||||
msg: {
|
||||
text: "Hello User %playerName%",
|
||||
color: "green",
|
||||
text: "Unfamiliar player %playerName% appeared at Position %playerPosX%, %playerPosY%, %playerPosZ%",
|
||||
color: "red",
|
||||
},
|
||||
prefix: "Taohuayuan",
|
||||
brackets: "[]",
|
||||
@@ -105,12 +105,16 @@ function loadConfig(filepath: string): AccessConfig {
|
||||
const [fp] = io.open(filepath, "r");
|
||||
if (fp == undefined) {
|
||||
print("Failed to open config file " + filepath);
|
||||
print("Use default config");
|
||||
saveConfig(defaultConfig, filepath);
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
const configJson = fp.read("*a");
|
||||
if (configJson == undefined) {
|
||||
print("Failed to read config file");
|
||||
print("Use default config");
|
||||
saveConfig(defaultConfig, filepath);
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@ const DEBUG = false;
|
||||
const args = [...$vararg];
|
||||
|
||||
// Init Log
|
||||
const log = new CCLog("accesscontrol.log", true, DAY);
|
||||
const logger = new CCLog("accesscontrol.log", true, DAY);
|
||||
|
||||
// Load Config
|
||||
const configFilepath = `${shell.dir()}/access.config.json`;
|
||||
const config = loadConfig(configFilepath);
|
||||
log.info("Load config successfully!");
|
||||
if (DEBUG) log.debug(textutils.serialise(config, { allow_repetitions: true }));
|
||||
logger.info("Load config successfully!");
|
||||
if (DEBUG)
|
||||
logger.debug(textutils.serialise(config, { allow_repetitions: true }));
|
||||
const groupNames = config.usersGroups.map((value) => value.groupName);
|
||||
let noticeTargetPlayers: string[];
|
||||
const playerDetector = peripheralManager.findByNameRequired("playerDetector");
|
||||
@@ -23,26 +24,56 @@ const chatBox = peripheralManager.findByNameRequired("chatBox");
|
||||
let inRangePlayers: string[] = [];
|
||||
let watchPlayersInfo: { name: string; hasNoticeTimes: number }[] = [];
|
||||
|
||||
interface ParseParams {
|
||||
name?: string;
|
||||
group?: string;
|
||||
info?: PlayerInfo;
|
||||
}
|
||||
|
||||
function safeParseTextComponent(
|
||||
component: MinecraftTextComponent,
|
||||
playerName: string,
|
||||
groupName?: string,
|
||||
params?: ParseParams,
|
||||
): string {
|
||||
if (component.text == undefined) {
|
||||
component.text = "Wrong text, please contanct with admin";
|
||||
} else if (component.text.includes("%")) {
|
||||
component.text = component.text.replace("%playerName%", playerName);
|
||||
if (groupName != undefined)
|
||||
component.text = component.text.replace("%groupName%", groupName);
|
||||
component.text = component.text.replace(
|
||||
"%playerName%",
|
||||
params?.name ?? "UnknowPlayer",
|
||||
);
|
||||
component.text = component.text.replace(
|
||||
"%groupName%",
|
||||
params?.group ?? "UnknowGroup",
|
||||
);
|
||||
component.text = component.text.replace(
|
||||
"%playerPosX%",
|
||||
params?.info?.x.toString() ?? "UnknowPosX",
|
||||
);
|
||||
component.text = component.text.replace(
|
||||
"%playerPosY%",
|
||||
params?.info?.y.toString() ?? "UnknowPosY",
|
||||
);
|
||||
component.text = component.text.replace(
|
||||
"%playerPosZ%",
|
||||
params?.info?.z.toString() ?? "UnknowPosZ",
|
||||
);
|
||||
}
|
||||
return textutils.serialiseJSON(component);
|
||||
}
|
||||
|
||||
function sendToast(toastConfig: ToastConfig, targetPlayer: string) {
|
||||
function sendToast(
|
||||
toastConfig: ToastConfig,
|
||||
targetPlayer: string,
|
||||
params: ParseParams,
|
||||
) {
|
||||
return chatBox.sendFormattedToastToPlayer(
|
||||
textutils.serialiseJSON(toastConfig.msg ?? config.welcomeToastConfig.msg),
|
||||
textutils.serialiseJSON(
|
||||
safeParseTextComponent(
|
||||
toastConfig.msg ?? config.welcomeToastConfig.msg,
|
||||
params,
|
||||
),
|
||||
safeParseTextComponent(
|
||||
toastConfig.title ?? config.welcomeToastConfig.title,
|
||||
params,
|
||||
),
|
||||
targetPlayer,
|
||||
toastConfig.prefix ?? config.welcomeToastConfig.prefix,
|
||||
@@ -62,29 +93,22 @@ function sendNotice(player: string, playerInfo?: PlayerInfo) {
|
||||
.flat(),
|
||||
);
|
||||
|
||||
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);
|
||||
sendToast(config.noticeToastConfig, targetPlayer, {
|
||||
name: player,
|
||||
info: playerInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function sendWarn(player: string) {
|
||||
const warnMsg = `Not Allowed Player ${player} Break in Home `;
|
||||
log.warn(warnMsg);
|
||||
logger.warn(warnMsg);
|
||||
|
||||
sendToast(config.warnToastConfig, player);
|
||||
sendToast(config.warnToastConfig, player, { name: player });
|
||||
chatBox.sendFormattedMessageToPlayer(
|
||||
safeParseTextComponent(config.warnToastConfig.msg, player),
|
||||
safeParseTextComponent(config.warnToastConfig.msg, { name: player }),
|
||||
player,
|
||||
"AccessControl",
|
||||
"[]",
|
||||
@@ -110,7 +134,7 @@ function watchLoop() {
|
||||
if (config.isWarn) sendWarn(player.name);
|
||||
|
||||
// Record
|
||||
log.warn(
|
||||
logger.warn(
|
||||
`${player.name} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||
);
|
||||
} else {
|
||||
@@ -118,6 +142,7 @@ function watchLoop() {
|
||||
watchPlayersInfo = watchPlayersInfo.filter(
|
||||
(value) => value.name != player.name,
|
||||
);
|
||||
logger.info(`${player.name} has left the range`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,14 +155,14 @@ function mainLoop() {
|
||||
const players = playerDetector.getPlayersInRange(config.detectRange);
|
||||
if (DEBUG) {
|
||||
const playersList = "[ " + players.join(",") + " ]";
|
||||
log.debug(`Detected ${players.length} players: ${playersList}`);
|
||||
logger.debug(`Detected ${players.length} players: ${playersList}`);
|
||||
}
|
||||
|
||||
for (const player of players) {
|
||||
if (inRangePlayers.includes(player)) continue;
|
||||
|
||||
if (config.adminGroupConfig.groupUsers.includes(player)) {
|
||||
log.info(`Admin ${player} appear`);
|
||||
logger.info(`Admin ${player} appear`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -154,7 +179,7 @@ function mainLoop() {
|
||||
if (!userGroupConfig.groupUsers.includes(player)) continue;
|
||||
|
||||
groupConfig = userGroupConfig;
|
||||
log.info(
|
||||
logger.info(
|
||||
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||
);
|
||||
|
||||
@@ -162,7 +187,7 @@ function mainLoop() {
|
||||
}
|
||||
if (groupConfig.isAllowed) continue;
|
||||
|
||||
log.warn(
|
||||
logger.warn(
|
||||
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
|
||||
);
|
||||
if (config.isWarn) sendWarn(player);
|
||||
@@ -178,26 +203,29 @@ function keyboardLoop() {
|
||||
while (true) {
|
||||
const [eventType, key] = os.pullEvent("key");
|
||||
if (eventType === "key" && key === keys.c) {
|
||||
log.info("Launching Access Control TUI...");
|
||||
logger.info("Launching Access Control TUI...");
|
||||
try {
|
||||
logger.setInTerminal(false);
|
||||
launchAccessControlTUI();
|
||||
log.info("TUI closed, resuming normal operation");
|
||||
logger.info("TUI closed, resuming normal operation");
|
||||
} catch (error) {
|
||||
log.error(`TUI error: ${textutils.serialise(error as object)}`);
|
||||
logger.error(`TUI error: ${textutils.serialise(error as object)}`);
|
||||
} finally {
|
||||
logger.setInTerminal(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main(args: string[]) {
|
||||
log.info("Starting access control system, get args: " + args.join(", "));
|
||||
logger.info("Starting access control system, get args: " + args.join(", "));
|
||||
if (args.length == 1) {
|
||||
if (args[0] == "start") {
|
||||
// 创建CLI处理器
|
||||
const cli = createAccessControlCLI(
|
||||
config,
|
||||
configFilepath,
|
||||
log,
|
||||
logger,
|
||||
chatBox,
|
||||
groupNames,
|
||||
);
|
||||
@@ -221,12 +249,12 @@ function main(args: string[]) {
|
||||
);
|
||||
return;
|
||||
} else if (args[0] == "config") {
|
||||
log.info("Launching Access Control TUI...");
|
||||
log.setInTerminal(false);
|
||||
logger.info("Launching Access Control TUI...");
|
||||
logger.setInTerminal(false);
|
||||
try {
|
||||
launchAccessControlTUI();
|
||||
} catch (error) {
|
||||
log.error(`TUI error: ${textutils.serialise(error as object)}`);
|
||||
logger.error(`TUI error: ${textutils.serialise(error as object)}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -240,7 +268,7 @@ function main(args: string[]) {
|
||||
try {
|
||||
main(args);
|
||||
} catch (error: unknown) {
|
||||
log.error(textutils.serialise(error as object));
|
||||
logger.error(textutils.serialise(error as object));
|
||||
} finally {
|
||||
log.close();
|
||||
logger.close();
|
||||
}
|
||||
|
||||
@@ -243,6 +243,18 @@ const AccessControlTUI = () => {
|
||||
/**
|
||||
* Basic Configuration Tab
|
||||
*/
|
||||
const [getDetectInterval, setDetectInterval] = createSignal(
|
||||
config().detectInterval.toString(),
|
||||
);
|
||||
const [getWatchInterval, setWatchInterval] = createSignal(
|
||||
config().watchInterval.toString(),
|
||||
);
|
||||
const [getNoticeTimes, setNoticeTimes] = createSignal(
|
||||
config().noticeTimes.toString(),
|
||||
);
|
||||
const [getDetectRange, setDetectRange] = createSignal(
|
||||
config().detectRange.toString(),
|
||||
);
|
||||
const BasicTab = () => {
|
||||
return div(
|
||||
{ class: "flex flex-col" },
|
||||
@@ -251,10 +263,12 @@ const AccessControlTUI = () => {
|
||||
label({}, "Detect Interval (ms):"),
|
||||
input({
|
||||
type: "text",
|
||||
value: () => config().detectInterval?.toString() ?? "",
|
||||
onInput: (value) => {
|
||||
const num = validateNumber(value);
|
||||
value: () => getDetectInterval(),
|
||||
onInput: (value) => setDetectInterval(value),
|
||||
onFocusChanged: () => {
|
||||
const num = validateNumber(getDetectInterval());
|
||||
if (num !== null) setConfig("detectInterval", num);
|
||||
else setDetectInterval(config().detectInterval.toString());
|
||||
},
|
||||
}),
|
||||
),
|
||||
@@ -263,10 +277,12 @@ const AccessControlTUI = () => {
|
||||
label({}, "Watch Interval (ms):"),
|
||||
input({
|
||||
type: "text",
|
||||
value: () => config().watchInterval?.toString() ?? "",
|
||||
onInput: (value) => {
|
||||
const num = validateNumber(value);
|
||||
value: () => getWatchInterval(),
|
||||
onInput: (value) => setWatchInterval(value),
|
||||
onFocusChanged: () => {
|
||||
const num = validateNumber(getWatchInterval());
|
||||
if (num !== null) setConfig("watchInterval", num);
|
||||
else setWatchInterval(config().watchInterval.toString());
|
||||
},
|
||||
}),
|
||||
),
|
||||
@@ -275,10 +291,12 @@ const AccessControlTUI = () => {
|
||||
label({}, "Notice Times:"),
|
||||
input({
|
||||
type: "text",
|
||||
value: () => config().noticeTimes?.toString() ?? "",
|
||||
onInput: (value) => {
|
||||
const num = validateNumber(value);
|
||||
value: () => getNoticeTimes(),
|
||||
onInput: (value) => setNoticeTimes(value),
|
||||
onFocusChanged: () => {
|
||||
const num = validateNumber(getNoticeTimes());
|
||||
if (num !== null) setConfig("noticeTimes", num);
|
||||
else setNoticeTimes(config().noticeTimes.toString());
|
||||
},
|
||||
}),
|
||||
),
|
||||
@@ -287,10 +305,12 @@ const AccessControlTUI = () => {
|
||||
label({}, "Detect Range:"),
|
||||
input({
|
||||
type: "text",
|
||||
value: () => config().detectRange?.toString() ?? "",
|
||||
onInput: (value) => {
|
||||
const num = validateNumber(value);
|
||||
value: () => getDetectRange(),
|
||||
onInput: (value) => setDetectRange(value),
|
||||
onFocusChanged: () => {
|
||||
const num = validateNumber(getDetectRange());
|
||||
if (num !== null) setConfig("detectRange", num);
|
||||
else setDetectRange(config().detectRange.toString());
|
||||
},
|
||||
}),
|
||||
),
|
||||
@@ -436,6 +456,13 @@ const AccessControlTUI = () => {
|
||||
) => {
|
||||
return () => {
|
||||
const toastConfig = config()[toastType];
|
||||
const [getTempToastConfig, setTempToastConfig] = createSignal({
|
||||
title: textutils.serialiseJSON(toastConfig.title),
|
||||
msg: textutils.serialiseJSON(toastConfig.msg),
|
||||
prefix: toastConfig.prefix ?? "",
|
||||
brackets: toastConfig.brackets ?? "",
|
||||
bracketColor: toastConfig.bracketColor ?? "",
|
||||
});
|
||||
|
||||
return div(
|
||||
{ class: "flex flex-col w-full" },
|
||||
@@ -443,20 +470,34 @@ const AccessControlTUI = () => {
|
||||
input({
|
||||
class: "w-full",
|
||||
type: "text",
|
||||
value: () => textutils.serialiseJSON(toastConfig?.title) ?? "",
|
||||
onInput: (value) => {
|
||||
value: () => getTempToastConfig().title,
|
||||
onInput: (value) =>
|
||||
setTempToastConfig({
|
||||
...getTempToastConfig(),
|
||||
title: value,
|
||||
}),
|
||||
onFocusChanged: () => {
|
||||
const currentToastConfig = config()[toastType];
|
||||
|
||||
try {
|
||||
const parsed = textutils.unserialiseJSON(value);
|
||||
if (parsed != undefined && typeof parsed === "object") {
|
||||
const currentConfig = config();
|
||||
const currentToast = currentConfig[toastType];
|
||||
const parsed = textutils.unserialiseJSON(
|
||||
getTempToastConfig().title,
|
||||
) as MinecraftTextComponent;
|
||||
if (
|
||||
typeof parsed === "object" &&
|
||||
parsed.text !== undefined &&
|
||||
parsed.color !== undefined
|
||||
) {
|
||||
setConfig(toastType, {
|
||||
...currentToast,
|
||||
title: parsed as MinecraftTextComponent,
|
||||
...currentToastConfig,
|
||||
title: parsed,
|
||||
});
|
||||
}
|
||||
} else throw new Error("Invalid JSON");
|
||||
} catch {
|
||||
// Invalid JSON, ignore
|
||||
setTempToastConfig({
|
||||
...getTempToastConfig(),
|
||||
title: textutils.serialiseJSON(currentToastConfig.title),
|
||||
});
|
||||
}
|
||||
},
|
||||
}),
|
||||
@@ -465,19 +506,31 @@ const AccessControlTUI = () => {
|
||||
input({
|
||||
class: "w-full",
|
||||
type: "text",
|
||||
value: () => textutils.serialiseJSON(toastConfig?.msg) ?? "",
|
||||
onInput: (value) => {
|
||||
value: () => getTempToastConfig().msg,
|
||||
onInput: (value) =>
|
||||
setTempToastConfig({ ...getTempToastConfig(), msg: value }),
|
||||
onFocusChanged: () => {
|
||||
const currentToastConfig = config()[toastType];
|
||||
|
||||
try {
|
||||
const parsed = textutils.unserialiseJSON(value);
|
||||
if (parsed != undefined && typeof parsed === "object") {
|
||||
const currentConfig = config();
|
||||
const currentToast = currentConfig[toastType];
|
||||
const parsed = textutils.unserialiseJSON(
|
||||
getTempToastConfig().msg,
|
||||
) as MinecraftTextComponent;
|
||||
if (
|
||||
typeof parsed === "object" &&
|
||||
parsed.text !== undefined &&
|
||||
parsed.color !== undefined
|
||||
) {
|
||||
setConfig(toastType, {
|
||||
...currentToast,
|
||||
msg: parsed as MinecraftTextComponent,
|
||||
...currentToastConfig,
|
||||
msg: parsed,
|
||||
});
|
||||
}
|
||||
} else throw new Error("Invalid JSON");
|
||||
} catch {
|
||||
setTempToastConfig({
|
||||
...getTempToastConfig(),
|
||||
msg: textutils.serialiseJSON(currentToastConfig.msg),
|
||||
});
|
||||
// Invalid JSON, ignore
|
||||
}
|
||||
},
|
||||
@@ -488,11 +541,15 @@ const AccessControlTUI = () => {
|
||||
label({}, "Prefix:"),
|
||||
input({
|
||||
type: "text",
|
||||
value: () => toastConfig?.prefix ?? "",
|
||||
onInput: (value) => {
|
||||
const currentConfig = config();
|
||||
const currentToast = currentConfig[toastType];
|
||||
setConfig(toastType, { ...currentToast, prefix: value });
|
||||
value: () => getTempToastConfig().prefix,
|
||||
onInput: (value) =>
|
||||
setTempToastConfig({ ...getTempToastConfig(), prefix: value }),
|
||||
onFocusChanged: () => {
|
||||
const currentToastConfig = config()[toastType];
|
||||
setConfig(toastType, {
|
||||
...currentToastConfig,
|
||||
prefix: getTempToastConfig().prefix,
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
@@ -502,11 +559,15 @@ const AccessControlTUI = () => {
|
||||
label({}, "Brackets:"),
|
||||
input({
|
||||
type: "text",
|
||||
value: () => toastConfig?.brackets ?? "",
|
||||
onInput: (value) => {
|
||||
const currentConfig = config();
|
||||
const currentToast = currentConfig[toastType];
|
||||
setConfig(toastType, { ...currentToast, brackets: value });
|
||||
value: () => getTempToastConfig().brackets,
|
||||
onInput: (value) =>
|
||||
setTempToastConfig({ ...getTempToastConfig(), brackets: value }),
|
||||
onFocusChanged: () => {
|
||||
const currentToastConfig = config()[toastType];
|
||||
setConfig(toastType, {
|
||||
...currentToastConfig,
|
||||
brackets: getTempToastConfig().brackets,
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
@@ -516,11 +577,18 @@ const AccessControlTUI = () => {
|
||||
label({}, "Bracket Color:"),
|
||||
input({
|
||||
type: "text",
|
||||
value: () => toastConfig?.bracketColor ?? "",
|
||||
onInput: (value) => {
|
||||
const currentConfig = config();
|
||||
const currentToast = currentConfig[toastType];
|
||||
setConfig(toastType, { ...currentToast, bracketColor: value });
|
||||
value: () => getTempToastConfig().bracketColor,
|
||||
onInput: (value) =>
|
||||
setTempToastConfig({
|
||||
...getTempToastConfig(),
|
||||
bracketColor: value,
|
||||
}),
|
||||
onFocusChanged: () => {
|
||||
const currentToastConfig = config()[toastType];
|
||||
setConfig(toastType, {
|
||||
...currentToastConfig,
|
||||
bracketColor: getTempToastConfig().bracketColor,
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
* Represents a node in the UI tree
|
||||
*/
|
||||
|
||||
import { Accessor } from "./reactivity";
|
||||
import { ButtonProps, DivProps, InputProps, LabelProps } from "./components";
|
||||
import { Accessor, Setter } from "./reactivity";
|
||||
import { ScrollContainerProps } from "./scrollContainer";
|
||||
|
||||
/**
|
||||
* Layout properties for flexbox layout
|
||||
@@ -34,7 +36,7 @@ export interface StyleProps {
|
||||
/**
|
||||
* Scroll properties for scroll containers
|
||||
*/
|
||||
export interface ScrollProps {
|
||||
export interface ScrollProps extends BaseProps {
|
||||
/** Current horizontal scroll position */
|
||||
scrollX: number;
|
||||
/** Current vertical scroll position */
|
||||
@@ -69,6 +71,9 @@ export interface ComputedLayout {
|
||||
export interface BaseProps {
|
||||
/** CSS-like class names for layout (e.g., "flex flex-col") */
|
||||
class?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
onFocusChanged?: Setter<boolean> | ((value: boolean) => void);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,6 +95,14 @@ export type UIObjectType =
|
||||
| "fragment"
|
||||
| "scroll-container";
|
||||
|
||||
export type UIObjectProps =
|
||||
| DivProps
|
||||
| LabelProps
|
||||
| InputProps
|
||||
| ButtonProps
|
||||
| ScrollProps
|
||||
| ScrollContainerProps;
|
||||
|
||||
/**
|
||||
* UIObject represents a node in the UI tree
|
||||
* It can be a component, text, or a control flow element
|
||||
@@ -99,7 +112,7 @@ export class UIObject {
|
||||
type: UIObjectType;
|
||||
|
||||
/** Props passed to the component */
|
||||
props: Record<string, unknown>;
|
||||
props: UIObjectProps;
|
||||
|
||||
/** Children UI objects */
|
||||
children: UIObject[];
|
||||
@@ -136,7 +149,7 @@ export class UIObject {
|
||||
|
||||
constructor(
|
||||
type: UIObjectType,
|
||||
props: Record<string, unknown> = {},
|
||||
props: UIObjectProps = {},
|
||||
children: UIObject[] = [],
|
||||
) {
|
||||
this.type = type;
|
||||
@@ -155,7 +168,7 @@ export class UIObject {
|
||||
this.extractHandlers();
|
||||
|
||||
// Initialize cursor position for text inputs
|
||||
if (type === "input" && props.type !== "checkbox") {
|
||||
if (type === "input" && (props as InputProps).type !== "checkbox") {
|
||||
this.cursorPos = 0;
|
||||
}
|
||||
|
||||
@@ -168,9 +181,9 @@ export class UIObject {
|
||||
maxScrollY: 0,
|
||||
contentWidth: 0,
|
||||
contentHeight: 0,
|
||||
showScrollbar: props.showScrollbar !== false,
|
||||
viewportWidth: (props.width as number) ?? 10,
|
||||
viewportHeight: (props.height as number) ?? 10,
|
||||
showScrollbar: (props as ScrollProps).showScrollbar !== false,
|
||||
viewportWidth: props.width ?? 10,
|
||||
viewportHeight: props.height ?? 10,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -208,7 +221,7 @@ export class UIObject {
|
||||
* Parse CSS-like class string into layout and style properties
|
||||
*/
|
||||
private parseClassNames(): void {
|
||||
const className = this.props.class as string | undefined;
|
||||
const className = this.props.class;
|
||||
if (className === undefined) return;
|
||||
|
||||
const classes = className.split(" ").filter((c) => c.length > 0);
|
||||
|
||||
@@ -7,6 +7,8 @@ import { calculateLayout } from "./layout";
|
||||
import { render as renderTree, clearScreen } from "./renderer";
|
||||
import { CCLog, HOUR } from "../ccLog";
|
||||
import { setLogger } from "./context";
|
||||
import { InputProps } from "./components";
|
||||
import { Setter } from "./reactivity";
|
||||
|
||||
/**
|
||||
* Main application class
|
||||
@@ -145,7 +147,7 @@ export class Application {
|
||||
if (
|
||||
this.focusedNode !== undefined &&
|
||||
this.focusedNode.type === "input" &&
|
||||
this.focusedNode.props.type !== "checkbox"
|
||||
(this.focusedNode.props as InputProps).type !== "checkbox"
|
||||
) {
|
||||
this.needsRender = true;
|
||||
}
|
||||
@@ -213,11 +215,13 @@ export class Application {
|
||||
this.needsRender = true;
|
||||
}
|
||||
} else if (this.focusedNode.type === "input") {
|
||||
const type = this.focusedNode.props.type as string | undefined;
|
||||
const type = (this.focusedNode.props as InputProps).type as
|
||||
| string
|
||||
| undefined;
|
||||
if (type === "checkbox") {
|
||||
// Toggle checkbox
|
||||
const onChangeProp = this.focusedNode.props.onChange;
|
||||
const checkedProp = this.focusedNode.props.checked;
|
||||
const onChangeProp = (this.focusedNode.props as InputProps).onChange;
|
||||
const checkedProp = (this.focusedNode.props as InputProps).checked;
|
||||
|
||||
if (
|
||||
typeof onChangeProp === "function" &&
|
||||
@@ -234,7 +238,9 @@ export class Application {
|
||||
this.focusedNode.type === "input"
|
||||
) {
|
||||
// Handle text input key events
|
||||
const type = this.focusedNode.props.type as string | undefined;
|
||||
const type = (this.focusedNode.props as InputProps).type as
|
||||
| string
|
||||
| undefined;
|
||||
if (type !== "checkbox") {
|
||||
this.handleTextInputKey(key);
|
||||
}
|
||||
@@ -247,8 +253,8 @@ export class Application {
|
||||
private handleTextInputKey(key: number): void {
|
||||
if (this.focusedNode === undefined) return;
|
||||
|
||||
const valueProp = this.focusedNode.props.value;
|
||||
const onInputProp = this.focusedNode.props.onInput;
|
||||
const valueProp = (this.focusedNode.props as InputProps).value;
|
||||
const onInputProp = (this.focusedNode.props as InputProps).onInput;
|
||||
|
||||
if (typeof valueProp !== "function" || typeof onInputProp !== "function") {
|
||||
return;
|
||||
@@ -292,11 +298,11 @@ export class Application {
|
||||
*/
|
||||
private handleCharEvent(char: string): void {
|
||||
if (this.focusedNode !== undefined && this.focusedNode.type === "input") {
|
||||
const type = this.focusedNode.props.type as string | undefined;
|
||||
const type = (this.focusedNode.props as InputProps).type;
|
||||
if (type !== "checkbox") {
|
||||
// Insert character at cursor position
|
||||
const onInputProp = this.focusedNode.props.onInput;
|
||||
const valueProp = this.focusedNode.props.value;
|
||||
const onInputProp = (this.focusedNode.props as InputProps).onInput;
|
||||
const valueProp = (this.focusedNode.props as InputProps).value;
|
||||
|
||||
if (
|
||||
typeof onInputProp === "function" &&
|
||||
@@ -331,11 +337,26 @@ export class Application {
|
||||
string.format("handleMouseClick: Found node of type %s.", clicked.type),
|
||||
);
|
||||
// Set focus
|
||||
if (
|
||||
this.focusedNode !== undefined &&
|
||||
typeof this.focusedNode.props.onFocusChanged === "function"
|
||||
) {
|
||||
const onFocusChanged = this.focusedNode.props
|
||||
.onFocusChanged as Setter<boolean>;
|
||||
onFocusChanged(false);
|
||||
}
|
||||
this.focusedNode = clicked;
|
||||
if (typeof clicked.props.onFocusChanged === "function") {
|
||||
const onFocusChanged = clicked.props.onFocusChanged as Setter<boolean>;
|
||||
onFocusChanged(true);
|
||||
}
|
||||
|
||||
// Initialize cursor position for text inputs on focus
|
||||
if (clicked.type === "input" && clicked.props.type !== "checkbox") {
|
||||
const valueProp = clicked.props.value;
|
||||
if (
|
||||
clicked.type === "input" &&
|
||||
(clicked.props as InputProps).type !== "checkbox"
|
||||
) {
|
||||
const valueProp = (clicked.props as InputProps).value;
|
||||
if (typeof valueProp === "function") {
|
||||
const currentValue = (valueProp as () => string)();
|
||||
clicked.cursorPos = currentValue.length;
|
||||
@@ -354,10 +375,10 @@ export class Application {
|
||||
this.needsRender = true;
|
||||
}
|
||||
} else if (clicked.type === "input") {
|
||||
const type = clicked.props.type as string | undefined;
|
||||
const type = (clicked.props as InputProps).type as string | undefined;
|
||||
if (type === "checkbox") {
|
||||
const onChangeProp = clicked.props.onChange;
|
||||
const checkedProp = clicked.props.checked;
|
||||
const onChangeProp = (clicked.props as InputProps).onChange;
|
||||
const checkedProp = (clicked.props as InputProps).checked;
|
||||
|
||||
if (
|
||||
typeof onChangeProp === "function" &&
|
||||
@@ -424,6 +445,14 @@ export class Application {
|
||||
|
||||
const interactive = this.collectInteractive(this.root);
|
||||
|
||||
if (
|
||||
this.focusedNode !== undefined &&
|
||||
typeof this.focusedNode.props.onFocusChanged === "function"
|
||||
) {
|
||||
const onFocusChanged = this.focusedNode.props
|
||||
.onFocusChanged as Setter<boolean>;
|
||||
onFocusChanged(false);
|
||||
}
|
||||
if (interactive.length === 0) {
|
||||
this.focusedNode = undefined;
|
||||
return;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { concatSentence } from "../common";
|
||||
/**
|
||||
* Props for div component
|
||||
*/
|
||||
export type DivProps = BaseProps & Record<string, unknown>;
|
||||
export type DivProps = BaseProps;
|
||||
|
||||
/**
|
||||
* Props for label component
|
||||
@@ -20,7 +20,7 @@ export type DivProps = BaseProps & Record<string, unknown>;
|
||||
export type LabelProps = BaseProps & {
|
||||
/** Whether to automatically wrap long text. Defaults to false. */
|
||||
wordWrap?: boolean;
|
||||
} & Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for button component
|
||||
@@ -28,7 +28,7 @@ export type LabelProps = BaseProps & {
|
||||
export type ButtonProps = BaseProps & {
|
||||
/** Click handler */
|
||||
onClick?: () => void;
|
||||
} & Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for input component
|
||||
@@ -46,7 +46,7 @@ export type InputProps = BaseProps & {
|
||||
onChange?: Setter<boolean> | ((checked: boolean) => void);
|
||||
/** Placeholder text */
|
||||
placeholder?: string;
|
||||
} & Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Props for form component
|
||||
@@ -54,7 +54,7 @@ export type InputProps = BaseProps & {
|
||||
export type FormProps = BaseProps & {
|
||||
/** Submit handler */
|
||||
onSubmit?: () => void;
|
||||
} & Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic container component for layout
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Calculates positions and sizes for UI elements based on flexbox rules
|
||||
*/
|
||||
|
||||
import { InputProps } from "./components";
|
||||
import { UIObject } from "./UIObject";
|
||||
|
||||
/**
|
||||
@@ -109,7 +110,7 @@ function measureNode(
|
||||
}
|
||||
|
||||
case "input": {
|
||||
const type = node.props.type as string | undefined;
|
||||
const type = (node.props as InputProps).type as string | undefined;
|
||||
if (type === "checkbox") {
|
||||
const naturalWidth = 3; // [X] or [ ]
|
||||
const naturalHeight = 1;
|
||||
@@ -119,7 +120,7 @@ function measureNode(
|
||||
};
|
||||
}
|
||||
// Text input - use a default width or from props
|
||||
const defaultWidth = (node.props.width as number | undefined) ?? 20;
|
||||
const defaultWidth = node.props.width ?? 20;
|
||||
const naturalHeight = 1;
|
||||
return {
|
||||
width: measuredWidth ?? defaultWidth,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { UIObject } from "./UIObject";
|
||||
import { Accessor } from "./reactivity";
|
||||
import { InputProps } from "./components";
|
||||
import { isScrollContainer } from "./scrollContainer";
|
||||
|
||||
/**
|
||||
@@ -189,14 +189,14 @@ function drawNode(
|
||||
}
|
||||
|
||||
case "input": {
|
||||
const type = node.props.type as string | undefined;
|
||||
const type = (node.props as InputProps).type as string | undefined;
|
||||
|
||||
if (type === "checkbox") {
|
||||
// Draw checkbox
|
||||
let isChecked = false;
|
||||
const checkedProp = node.props.checked;
|
||||
const checkedProp = (node.props as InputProps).checked;
|
||||
if (typeof checkedProp === "function") {
|
||||
isChecked = (checkedProp as Accessor<boolean>)();
|
||||
isChecked = checkedProp();
|
||||
}
|
||||
|
||||
if (focused) {
|
||||
@@ -212,12 +212,11 @@ function drawNode(
|
||||
} else {
|
||||
// Draw text input
|
||||
let displayText = "";
|
||||
const valueProp = node.props.value;
|
||||
const valueProp = (node.props as InputProps).value;
|
||||
if (typeof valueProp === "function") {
|
||||
displayText = (valueProp as Accessor<string>)();
|
||||
displayText = valueProp();
|
||||
}
|
||||
|
||||
const placeholder = node.props.placeholder as string | undefined;
|
||||
const placeholder = (node.props as InputProps).placeholder;
|
||||
const cursorPos = node.cursorPos ?? 0;
|
||||
let currentTextColor = textColor;
|
||||
let showPlaceholder = false;
|
||||
|
||||
@@ -31,7 +31,10 @@ const Counter = () => {
|
||||
div(
|
||||
{ class: "flex flex-row" },
|
||||
button({ onClick: () => setCount(count() - 1), class: "text-red" }, "-"),
|
||||
button({ onClick: () => setCount(count() + 1), class: "text-green" }, "+"),
|
||||
button(
|
||||
{ onClick: () => setCount(count() + 1), class: "text-green" },
|
||||
"+",
|
||||
),
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -81,10 +84,10 @@ const TodosApp = () => {
|
||||
},
|
||||
}),
|
||||
label(
|
||||
{
|
||||
class: todo.completed ? "ml-1 text-gray" : "ml-1 text-white"
|
||||
},
|
||||
() => todo.title
|
||||
{
|
||||
class: todo.completed ? "ml-1 text-gray" : "ml-1 text-white",
|
||||
},
|
||||
() => todo.title,
|
||||
),
|
||||
button(
|
||||
{
|
||||
@@ -308,7 +311,7 @@ const App = () => {
|
||||
{
|
||||
when: () => tabIndex() === 0,
|
||||
fallback: Show(
|
||||
{
|
||||
{
|
||||
when: () => tabIndex() === 1,
|
||||
fallback: Show(
|
||||
{
|
||||
@@ -318,13 +321,13 @@ const App = () => {
|
||||
when: () => tabIndex() === 3,
|
||||
fallback: MultiScrollExample(),
|
||||
},
|
||||
StaticScrollExample()
|
||||
)
|
||||
StaticScrollExample(),
|
||||
),
|
||||
},
|
||||
SimpleScrollExample()
|
||||
)
|
||||
},
|
||||
TodosApp()
|
||||
SimpleScrollExample(),
|
||||
),
|
||||
},
|
||||
TodosApp(),
|
||||
),
|
||||
},
|
||||
Counter(),
|
||||
@@ -347,4 +350,4 @@ try {
|
||||
print("Error running application:");
|
||||
printError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user