Compare commits

...

3 Commits

Author SHA1 Message Date
SikongJueluo
b9ce947b9b fix: conflict with log and tui, position parse nothing
fix:
- fix ui conflict with log and tui
- fix player position parse failed
2025-10-14 23:26:53 +08:00
SikongJueluo
da2c6c1ebb feature: auto-gen configuration, notice settings, player position parse
feature:
- auto generate configuration file
- adopt notice toast settings
- toast config add new parse parameter: player position x, y, z
reconstruct:
- justfile: remove copy config file command from project
2025-10-14 23:04:00 +08:00
SikongJueluo
c85c072376 feature: tui onFocusChanged event
feature: add focus event for tui
reconstruct: tui props and accesscontrol parse logic
2025-10-14 22:29:17 +08:00
11 changed files with 287 additions and 215 deletions

View File

@@ -9,7 +9,6 @@ build-autocraft:
build-accesscontrol: build-accesscontrol:
pnpm tstl -p ./tsconfig.accesscontrol.json pnpm tstl -p ./tsconfig.accesscontrol.json
cp ./src/accesscontrol/access.config.json ./build/
build-test: build-test:
pnpm tstl -p ./tsconfig.test.json pnpm tstl -p ./tsconfig.test.json

View File

@@ -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": ""
}
}

View File

@@ -75,12 +75,12 @@ const defaultConfig: AccessConfig = {
}, },
noticeToastConfig: { noticeToastConfig: {
title: { title: {
text: "Welcome", text: "Notice",
color: "green", color: "red",
}, },
msg: { msg: {
text: "Hello User %playerName%", text: "Unfamiliar player %playerName% appeared at Position %playerPosX%, %playerPosY%, %playerPosZ%",
color: "green", color: "red",
}, },
prefix: "Taohuayuan", prefix: "Taohuayuan",
brackets: "[]", brackets: "[]",
@@ -105,12 +105,16 @@ function loadConfig(filepath: string): AccessConfig {
const [fp] = io.open(filepath, "r"); const [fp] = io.open(filepath, "r");
if (fp == undefined) { if (fp == undefined) {
print("Failed to open config file " + filepath); print("Failed to open config file " + filepath);
print("Use default config");
saveConfig(defaultConfig, filepath);
return defaultConfig; return defaultConfig;
} }
const configJson = fp.read("*a"); const configJson = fp.read("*a");
if (configJson == undefined) { if (configJson == undefined) {
print("Failed to read config file"); print("Failed to read config file");
print("Use default config");
saveConfig(defaultConfig, filepath);
return defaultConfig; return defaultConfig;
} }

View File

@@ -8,13 +8,14 @@ const DEBUG = false;
const args = [...$vararg]; const args = [...$vararg];
// Init Log // Init Log
const log = new CCLog("accesscontrol.log", true, DAY); const logger = new CCLog("accesscontrol.log", true, DAY);
// Load Config // Load Config
const configFilepath = `${shell.dir()}/access.config.json`; const configFilepath = `${shell.dir()}/access.config.json`;
const config = loadConfig(configFilepath); const config = loadConfig(configFilepath);
log.info("Load config successfully!"); logger.info("Load config successfully!");
if (DEBUG) log.debug(textutils.serialise(config, { allow_repetitions: true })); if (DEBUG)
logger.debug(textutils.serialise(config, { allow_repetitions: true }));
const groupNames = config.usersGroups.map((value) => value.groupName); const groupNames = config.usersGroups.map((value) => value.groupName);
let noticeTargetPlayers: string[]; let noticeTargetPlayers: string[];
const playerDetector = peripheralManager.findByNameRequired("playerDetector"); const playerDetector = peripheralManager.findByNameRequired("playerDetector");
@@ -23,26 +24,56 @@ const chatBox = peripheralManager.findByNameRequired("chatBox");
let inRangePlayers: string[] = []; let inRangePlayers: string[] = [];
let watchPlayersInfo: { name: string; hasNoticeTimes: number }[] = []; let watchPlayersInfo: { name: string; hasNoticeTimes: number }[] = [];
interface ParseParams {
name?: string;
group?: string;
info?: PlayerInfo;
}
function safeParseTextComponent( function safeParseTextComponent(
component: MinecraftTextComponent, component: MinecraftTextComponent,
playerName: string, params?: ParseParams,
groupName?: string,
): string { ): string {
if (component.text == undefined) { if (component.text == undefined) {
component.text = "Wrong text, please contanct with admin"; component.text = "Wrong text, please contanct with admin";
} else if (component.text.includes("%")) { } else if (component.text.includes("%")) {
component.text = component.text.replace("%playerName%", playerName); component.text = component.text.replace(
if (groupName != undefined) "%playerName%",
component.text = component.text.replace("%groupName%", groupName); 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); return textutils.serialiseJSON(component);
} }
function sendToast(toastConfig: ToastConfig, targetPlayer: string) { function sendToast(
toastConfig: ToastConfig,
targetPlayer: string,
params: ParseParams,
) {
return chatBox.sendFormattedToastToPlayer( return chatBox.sendFormattedToastToPlayer(
textutils.serialiseJSON(toastConfig.msg ?? config.welcomeToastConfig.msg), safeParseTextComponent(
textutils.serialiseJSON( toastConfig.msg ?? config.welcomeToastConfig.msg,
params,
),
safeParseTextComponent(
toastConfig.title ?? config.welcomeToastConfig.title, toastConfig.title ?? config.welcomeToastConfig.title,
params,
), ),
targetPlayer, targetPlayer,
toastConfig.prefix ?? config.welcomeToastConfig.prefix, toastConfig.prefix ?? config.welcomeToastConfig.prefix,
@@ -62,29 +93,22 @@ function sendNotice(player: string, playerInfo?: PlayerInfo) {
.flat(), .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) { for (const targetPlayer of noticeTargetPlayers) {
if (!onlinePlayers.includes(targetPlayer)) continue; if (!onlinePlayers.includes(targetPlayer)) continue;
sendToast(toastConfig, targetPlayer); sendToast(config.noticeToastConfig, targetPlayer, {
name: player,
info: playerInfo,
});
} }
} }
function sendWarn(player: string) { function sendWarn(player: string) {
const warnMsg = `Not Allowed Player ${player} Break in Home `; 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( chatBox.sendFormattedMessageToPlayer(
safeParseTextComponent(config.warnToastConfig.msg, player), safeParseTextComponent(config.warnToastConfig.msg, { name: player }),
player, player,
"AccessControl", "AccessControl",
"[]", "[]",
@@ -110,7 +134,7 @@ function watchLoop() {
if (config.isWarn) sendWarn(player.name); if (config.isWarn) sendWarn(player.name);
// Record // Record
log.warn( logger.warn(
`${player.name} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`, `${player.name} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
); );
} else { } else {
@@ -118,6 +142,7 @@ function watchLoop() {
watchPlayersInfo = watchPlayersInfo.filter( watchPlayersInfo = watchPlayersInfo.filter(
(value) => value.name != player.name, (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); const players = playerDetector.getPlayersInRange(config.detectRange);
if (DEBUG) { if (DEBUG) {
const playersList = "[ " + players.join(",") + " ]"; const playersList = "[ " + players.join(",") + " ]";
log.debug(`Detected ${players.length} players: ${playersList}`); logger.debug(`Detected ${players.length} players: ${playersList}`);
} }
for (const player of players) { for (const player of players) {
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} appear`); logger.info(`Admin ${player} appear`);
continue; continue;
} }
@@ -154,7 +179,7 @@ function mainLoop() {
if (!userGroupConfig.groupUsers.includes(player)) continue; if (!userGroupConfig.groupUsers.includes(player)) continue;
groupConfig = userGroupConfig; groupConfig = userGroupConfig;
log.info( logger.info(
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`, `${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
); );
@@ -162,7 +187,7 @@ function mainLoop() {
} }
if (groupConfig.isAllowed) continue; if (groupConfig.isAllowed) continue;
log.warn( logger.warn(
`${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`, `${groupConfig.groupName} ${player} appear at ${playerInfo?.x}, ${playerInfo?.y}, ${playerInfo?.z}`,
); );
if (config.isWarn) sendWarn(player); if (config.isWarn) sendWarn(player);
@@ -178,26 +203,29 @@ function keyboardLoop() {
while (true) { while (true) {
const [eventType, key] = os.pullEvent("key"); const [eventType, key] = os.pullEvent("key");
if (eventType === "key" && key === keys.c) { if (eventType === "key" && key === keys.c) {
log.info("Launching Access Control TUI..."); logger.info("Launching Access Control TUI...");
try { try {
logger.setInTerminal(false);
launchAccessControlTUI(); launchAccessControlTUI();
log.info("TUI closed, resuming normal operation"); logger.info("TUI closed, resuming normal operation");
} catch (error) { } 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[]) { 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.length == 1) {
if (args[0] == "start") { if (args[0] == "start") {
// 创建CLI处理器 // 创建CLI处理器
const cli = createAccessControlCLI( const cli = createAccessControlCLI(
config, config,
configFilepath, configFilepath,
log, logger,
chatBox, chatBox,
groupNames, groupNames,
); );
@@ -221,12 +249,12 @@ function main(args: string[]) {
); );
return; return;
} else if (args[0] == "config") { } else if (args[0] == "config") {
log.info("Launching Access Control TUI..."); logger.info("Launching Access Control TUI...");
log.setInTerminal(false); logger.setInTerminal(false);
try { try {
launchAccessControlTUI(); launchAccessControlTUI();
} catch (error) { } catch (error) {
log.error(`TUI error: ${textutils.serialise(error as object)}`); logger.error(`TUI error: ${textutils.serialise(error as object)}`);
} }
return; return;
} }
@@ -240,7 +268,7 @@ function main(args: string[]) {
try { try {
main(args); main(args);
} catch (error: unknown) { } catch (error: unknown) {
log.error(textutils.serialise(error as object)); logger.error(textutils.serialise(error as object));
} finally { } finally {
log.close(); logger.close();
} }

View File

@@ -243,6 +243,18 @@ const AccessControlTUI = () => {
/** /**
* Basic Configuration Tab * 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 = () => { const BasicTab = () => {
return div( return div(
{ class: "flex flex-col" }, { class: "flex flex-col" },
@@ -251,10 +263,12 @@ const AccessControlTUI = () => {
label({}, "Detect Interval (ms):"), label({}, "Detect Interval (ms):"),
input({ input({
type: "text", type: "text",
value: () => config().detectInterval?.toString() ?? "", value: () => getDetectInterval(),
onInput: (value) => { onInput: (value) => setDetectInterval(value),
const num = validateNumber(value); onFocusChanged: () => {
const num = validateNumber(getDetectInterval());
if (num !== null) setConfig("detectInterval", num); if (num !== null) setConfig("detectInterval", num);
else setDetectInterval(config().detectInterval.toString());
}, },
}), }),
), ),
@@ -263,10 +277,12 @@ const AccessControlTUI = () => {
label({}, "Watch Interval (ms):"), label({}, "Watch Interval (ms):"),
input({ input({
type: "text", type: "text",
value: () => config().watchInterval?.toString() ?? "", value: () => getWatchInterval(),
onInput: (value) => { onInput: (value) => setWatchInterval(value),
const num = validateNumber(value); onFocusChanged: () => {
const num = validateNumber(getWatchInterval());
if (num !== null) setConfig("watchInterval", num); if (num !== null) setConfig("watchInterval", num);
else setWatchInterval(config().watchInterval.toString());
}, },
}), }),
), ),
@@ -275,10 +291,12 @@ const AccessControlTUI = () => {
label({}, "Notice Times:"), label({}, "Notice Times:"),
input({ input({
type: "text", type: "text",
value: () => config().noticeTimes?.toString() ?? "", value: () => getNoticeTimes(),
onInput: (value) => { onInput: (value) => setNoticeTimes(value),
const num = validateNumber(value); onFocusChanged: () => {
const num = validateNumber(getNoticeTimes());
if (num !== null) setConfig("noticeTimes", num); if (num !== null) setConfig("noticeTimes", num);
else setNoticeTimes(config().noticeTimes.toString());
}, },
}), }),
), ),
@@ -287,10 +305,12 @@ const AccessControlTUI = () => {
label({}, "Detect Range:"), label({}, "Detect Range:"),
input({ input({
type: "text", type: "text",
value: () => config().detectRange?.toString() ?? "", value: () => getDetectRange(),
onInput: (value) => { onInput: (value) => setDetectRange(value),
const num = validateNumber(value); onFocusChanged: () => {
const num = validateNumber(getDetectRange());
if (num !== null) setConfig("detectRange", num); if (num !== null) setConfig("detectRange", num);
else setDetectRange(config().detectRange.toString());
}, },
}), }),
), ),
@@ -436,6 +456,13 @@ const AccessControlTUI = () => {
) => { ) => {
return () => { return () => {
const toastConfig = config()[toastType]; 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( return div(
{ class: "flex flex-col w-full" }, { class: "flex flex-col w-full" },
@@ -443,20 +470,34 @@ const AccessControlTUI = () => {
input({ input({
class: "w-full", class: "w-full",
type: "text", type: "text",
value: () => textutils.serialiseJSON(toastConfig?.title) ?? "", value: () => getTempToastConfig().title,
onInput: (value) => { onInput: (value) =>
setTempToastConfig({
...getTempToastConfig(),
title: value,
}),
onFocusChanged: () => {
const currentToastConfig = config()[toastType];
try { try {
const parsed = textutils.unserialiseJSON(value); const parsed = textutils.unserialiseJSON(
if (parsed != undefined && typeof parsed === "object") { getTempToastConfig().title,
const currentConfig = config(); ) as MinecraftTextComponent;
const currentToast = currentConfig[toastType]; if (
typeof parsed === "object" &&
parsed.text !== undefined &&
parsed.color !== undefined
) {
setConfig(toastType, { setConfig(toastType, {
...currentToast, ...currentToastConfig,
title: parsed as MinecraftTextComponent, title: parsed,
}); });
} } else throw new Error("Invalid JSON");
} catch { } catch {
// Invalid JSON, ignore setTempToastConfig({
...getTempToastConfig(),
title: textutils.serialiseJSON(currentToastConfig.title),
});
} }
}, },
}), }),
@@ -465,19 +506,31 @@ const AccessControlTUI = () => {
input({ input({
class: "w-full", class: "w-full",
type: "text", type: "text",
value: () => textutils.serialiseJSON(toastConfig?.msg) ?? "", value: () => getTempToastConfig().msg,
onInput: (value) => { onInput: (value) =>
setTempToastConfig({ ...getTempToastConfig(), msg: value }),
onFocusChanged: () => {
const currentToastConfig = config()[toastType];
try { try {
const parsed = textutils.unserialiseJSON(value); const parsed = textutils.unserialiseJSON(
if (parsed != undefined && typeof parsed === "object") { getTempToastConfig().msg,
const currentConfig = config(); ) as MinecraftTextComponent;
const currentToast = currentConfig[toastType]; if (
typeof parsed === "object" &&
parsed.text !== undefined &&
parsed.color !== undefined
) {
setConfig(toastType, { setConfig(toastType, {
...currentToast, ...currentToastConfig,
msg: parsed as MinecraftTextComponent, msg: parsed,
}); });
} } else throw new Error("Invalid JSON");
} catch { } catch {
setTempToastConfig({
...getTempToastConfig(),
msg: textutils.serialiseJSON(currentToastConfig.msg),
});
// Invalid JSON, ignore // Invalid JSON, ignore
} }
}, },
@@ -488,11 +541,15 @@ const AccessControlTUI = () => {
label({}, "Prefix:"), label({}, "Prefix:"),
input({ input({
type: "text", type: "text",
value: () => toastConfig?.prefix ?? "", value: () => getTempToastConfig().prefix,
onInput: (value) => { onInput: (value) =>
const currentConfig = config(); setTempToastConfig({ ...getTempToastConfig(), prefix: value }),
const currentToast = currentConfig[toastType]; onFocusChanged: () => {
setConfig(toastType, { ...currentToast, prefix: value }); const currentToastConfig = config()[toastType];
setConfig(toastType, {
...currentToastConfig,
prefix: getTempToastConfig().prefix,
});
}, },
}), }),
), ),
@@ -502,11 +559,15 @@ const AccessControlTUI = () => {
label({}, "Brackets:"), label({}, "Brackets:"),
input({ input({
type: "text", type: "text",
value: () => toastConfig?.brackets ?? "", value: () => getTempToastConfig().brackets,
onInput: (value) => { onInput: (value) =>
const currentConfig = config(); setTempToastConfig({ ...getTempToastConfig(), brackets: value }),
const currentToast = currentConfig[toastType]; onFocusChanged: () => {
setConfig(toastType, { ...currentToast, brackets: value }); const currentToastConfig = config()[toastType];
setConfig(toastType, {
...currentToastConfig,
brackets: getTempToastConfig().brackets,
});
}, },
}), }),
), ),
@@ -516,11 +577,18 @@ const AccessControlTUI = () => {
label({}, "Bracket Color:"), label({}, "Bracket Color:"),
input({ input({
type: "text", type: "text",
value: () => toastConfig?.bracketColor ?? "", value: () => getTempToastConfig().bracketColor,
onInput: (value) => { onInput: (value) =>
const currentConfig = config(); setTempToastConfig({
const currentToast = currentConfig[toastType]; ...getTempToastConfig(),
setConfig(toastType, { ...currentToast, bracketColor: value }); bracketColor: value,
}),
onFocusChanged: () => {
const currentToastConfig = config()[toastType];
setConfig(toastType, {
...currentToastConfig,
bracketColor: getTempToastConfig().bracketColor,
});
}, },
}), }),
), ),

View File

@@ -3,7 +3,9 @@
* Represents a node in the UI tree * 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 * Layout properties for flexbox layout
@@ -34,7 +36,7 @@ export interface StyleProps {
/** /**
* Scroll properties for scroll containers * Scroll properties for scroll containers
*/ */
export interface ScrollProps { export interface ScrollProps extends BaseProps {
/** Current horizontal scroll position */ /** Current horizontal scroll position */
scrollX: number; scrollX: number;
/** Current vertical scroll position */ /** Current vertical scroll position */
@@ -69,6 +71,9 @@ export interface ComputedLayout {
export interface BaseProps { export interface BaseProps {
/** CSS-like class names for layout (e.g., "flex flex-col") */ /** CSS-like class names for layout (e.g., "flex flex-col") */
class?: string; class?: string;
width?: number;
height?: number;
onFocusChanged?: Setter<boolean> | ((value: boolean) => void);
} }
/** /**
@@ -90,6 +95,14 @@ export type UIObjectType =
| "fragment" | "fragment"
| "scroll-container"; | "scroll-container";
export type UIObjectProps =
| DivProps
| LabelProps
| InputProps
| ButtonProps
| ScrollProps
| ScrollContainerProps;
/** /**
* UIObject represents a node in the UI tree * UIObject represents a node in the UI tree
* It can be a component, text, or a control flow element * It can be a component, text, or a control flow element
@@ -99,7 +112,7 @@ export class UIObject {
type: UIObjectType; type: UIObjectType;
/** Props passed to the component */ /** Props passed to the component */
props: Record<string, unknown>; props: UIObjectProps;
/** Children UI objects */ /** Children UI objects */
children: UIObject[]; children: UIObject[];
@@ -136,7 +149,7 @@ export class UIObject {
constructor( constructor(
type: UIObjectType, type: UIObjectType,
props: Record<string, unknown> = {}, props: UIObjectProps = {},
children: UIObject[] = [], children: UIObject[] = [],
) { ) {
this.type = type; this.type = type;
@@ -155,7 +168,7 @@ export class UIObject {
this.extractHandlers(); this.extractHandlers();
// Initialize cursor position for text inputs // Initialize cursor position for text inputs
if (type === "input" && props.type !== "checkbox") { if (type === "input" && (props as InputProps).type !== "checkbox") {
this.cursorPos = 0; this.cursorPos = 0;
} }
@@ -168,9 +181,9 @@ export class UIObject {
maxScrollY: 0, maxScrollY: 0,
contentWidth: 0, contentWidth: 0,
contentHeight: 0, contentHeight: 0,
showScrollbar: props.showScrollbar !== false, showScrollbar: (props as ScrollProps).showScrollbar !== false,
viewportWidth: (props.width as number) ?? 10, viewportWidth: props.width ?? 10,
viewportHeight: (props.height as number) ?? 10, viewportHeight: props.height ?? 10,
}; };
} }
} }
@@ -208,7 +221,7 @@ export class UIObject {
* Parse CSS-like class string into layout and style properties * Parse CSS-like class string into layout and style properties
*/ */
private parseClassNames(): void { private parseClassNames(): void {
const className = this.props.class as string | undefined; const className = this.props.class;
if (className === undefined) return; if (className === undefined) return;
const classes = className.split(" ").filter((c) => c.length > 0); const classes = className.split(" ").filter((c) => c.length > 0);

View File

@@ -7,6 +7,8 @@ import { calculateLayout } from "./layout";
import { render as renderTree, clearScreen } from "./renderer"; import { render as renderTree, clearScreen } from "./renderer";
import { CCLog, HOUR } from "../ccLog"; import { CCLog, HOUR } from "../ccLog";
import { setLogger } from "./context"; import { setLogger } from "./context";
import { InputProps } from "./components";
import { Setter } from "./reactivity";
/** /**
* Main application class * Main application class
@@ -145,7 +147,7 @@ export class Application {
if ( if (
this.focusedNode !== undefined && this.focusedNode !== undefined &&
this.focusedNode.type === "input" && this.focusedNode.type === "input" &&
this.focusedNode.props.type !== "checkbox" (this.focusedNode.props as InputProps).type !== "checkbox"
) { ) {
this.needsRender = true; this.needsRender = true;
} }
@@ -213,11 +215,13 @@ export class Application {
this.needsRender = true; this.needsRender = true;
} }
} else if (this.focusedNode.type === "input") { } 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") { if (type === "checkbox") {
// Toggle checkbox // Toggle checkbox
const onChangeProp = this.focusedNode.props.onChange; const onChangeProp = (this.focusedNode.props as InputProps).onChange;
const checkedProp = this.focusedNode.props.checked; const checkedProp = (this.focusedNode.props as InputProps).checked;
if ( if (
typeof onChangeProp === "function" && typeof onChangeProp === "function" &&
@@ -234,7 +238,9 @@ export class Application {
this.focusedNode.type === "input" this.focusedNode.type === "input"
) { ) {
// Handle text input key events // 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") { if (type !== "checkbox") {
this.handleTextInputKey(key); this.handleTextInputKey(key);
} }
@@ -247,8 +253,8 @@ export class Application {
private handleTextInputKey(key: number): void { private handleTextInputKey(key: number): void {
if (this.focusedNode === undefined) return; if (this.focusedNode === undefined) return;
const valueProp = this.focusedNode.props.value; const valueProp = (this.focusedNode.props as InputProps).value;
const onInputProp = this.focusedNode.props.onInput; const onInputProp = (this.focusedNode.props as InputProps).onInput;
if (typeof valueProp !== "function" || typeof onInputProp !== "function") { if (typeof valueProp !== "function" || typeof onInputProp !== "function") {
return; return;
@@ -292,11 +298,11 @@ export class Application {
*/ */
private handleCharEvent(char: string): void { private handleCharEvent(char: string): void {
if (this.focusedNode !== undefined && this.focusedNode.type === "input") { 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") { if (type !== "checkbox") {
// Insert character at cursor position // Insert character at cursor position
const onInputProp = this.focusedNode.props.onInput; const onInputProp = (this.focusedNode.props as InputProps).onInput;
const valueProp = this.focusedNode.props.value; const valueProp = (this.focusedNode.props as InputProps).value;
if ( if (
typeof onInputProp === "function" && typeof onInputProp === "function" &&
@@ -331,11 +337,26 @@ export class Application {
string.format("handleMouseClick: Found node of type %s.", clicked.type), string.format("handleMouseClick: Found node of type %s.", clicked.type),
); );
// Set focus // 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; 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 // Initialize cursor position for text inputs on focus
if (clicked.type === "input" && clicked.props.type !== "checkbox") { if (
const valueProp = clicked.props.value; clicked.type === "input" &&
(clicked.props as InputProps).type !== "checkbox"
) {
const valueProp = (clicked.props as InputProps).value;
if (typeof valueProp === "function") { if (typeof valueProp === "function") {
const currentValue = (valueProp as () => string)(); const currentValue = (valueProp as () => string)();
clicked.cursorPos = currentValue.length; clicked.cursorPos = currentValue.length;
@@ -354,10 +375,10 @@ export class Application {
this.needsRender = true; this.needsRender = true;
} }
} else if (clicked.type === "input") { } 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") { if (type === "checkbox") {
const onChangeProp = clicked.props.onChange; const onChangeProp = (clicked.props as InputProps).onChange;
const checkedProp = clicked.props.checked; const checkedProp = (clicked.props as InputProps).checked;
if ( if (
typeof onChangeProp === "function" && typeof onChangeProp === "function" &&
@@ -424,6 +445,14 @@ export class Application {
const interactive = this.collectInteractive(this.root); 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) { if (interactive.length === 0) {
this.focusedNode = undefined; this.focusedNode = undefined;
return; return;

View File

@@ -12,7 +12,7 @@ import { concatSentence } from "../common";
/** /**
* Props for div component * Props for div component
*/ */
export type DivProps = BaseProps & Record<string, unknown>; export type DivProps = BaseProps;
/** /**
* Props for label component * Props for label component
@@ -20,7 +20,7 @@ export type DivProps = BaseProps & Record<string, unknown>;
export type LabelProps = BaseProps & { export type LabelProps = BaseProps & {
/** Whether to automatically wrap long text. Defaults to false. */ /** Whether to automatically wrap long text. Defaults to false. */
wordWrap?: boolean; wordWrap?: boolean;
} & Record<string, unknown>; };
/** /**
* Props for button component * Props for button component
@@ -28,7 +28,7 @@ export type LabelProps = BaseProps & {
export type ButtonProps = BaseProps & { export type ButtonProps = BaseProps & {
/** Click handler */ /** Click handler */
onClick?: () => void; onClick?: () => void;
} & Record<string, unknown>; };
/** /**
* Props for input component * Props for input component
@@ -46,7 +46,7 @@ export type InputProps = BaseProps & {
onChange?: Setter<boolean> | ((checked: boolean) => void); onChange?: Setter<boolean> | ((checked: boolean) => void);
/** Placeholder text */ /** Placeholder text */
placeholder?: string; placeholder?: string;
} & Record<string, unknown>; };
/** /**
* Props for form component * Props for form component
@@ -54,7 +54,7 @@ export type InputProps = BaseProps & {
export type FormProps = BaseProps & { export type FormProps = BaseProps & {
/** Submit handler */ /** Submit handler */
onSubmit?: () => void; onSubmit?: () => void;
} & Record<string, unknown>; };
/** /**
* Generic container component for layout * Generic container component for layout

View File

@@ -3,6 +3,7 @@
* Calculates positions and sizes for UI elements based on flexbox rules * Calculates positions and sizes for UI elements based on flexbox rules
*/ */
import { InputProps } from "./components";
import { UIObject } from "./UIObject"; import { UIObject } from "./UIObject";
/** /**
@@ -109,7 +110,7 @@ function measureNode(
} }
case "input": { case "input": {
const type = node.props.type as string | undefined; const type = (node.props as InputProps).type as string | undefined;
if (type === "checkbox") { if (type === "checkbox") {
const naturalWidth = 3; // [X] or [ ] const naturalWidth = 3; // [X] or [ ]
const naturalHeight = 1; const naturalHeight = 1;
@@ -119,7 +120,7 @@ function measureNode(
}; };
} }
// Text input - use a default width or from props // 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; const naturalHeight = 1;
return { return {
width: measuredWidth ?? defaultWidth, width: measuredWidth ?? defaultWidth,

View File

@@ -3,7 +3,7 @@
*/ */
import { UIObject } from "./UIObject"; import { UIObject } from "./UIObject";
import { Accessor } from "./reactivity"; import { InputProps } from "./components";
import { isScrollContainer } from "./scrollContainer"; import { isScrollContainer } from "./scrollContainer";
/** /**
@@ -189,14 +189,14 @@ function drawNode(
} }
case "input": { case "input": {
const type = node.props.type as string | undefined; const type = (node.props as InputProps).type as string | undefined;
if (type === "checkbox") { if (type === "checkbox") {
// Draw checkbox // Draw checkbox
let isChecked = false; let isChecked = false;
const checkedProp = node.props.checked; const checkedProp = (node.props as InputProps).checked;
if (typeof checkedProp === "function") { if (typeof checkedProp === "function") {
isChecked = (checkedProp as Accessor<boolean>)(); isChecked = checkedProp();
} }
if (focused) { if (focused) {
@@ -212,12 +212,11 @@ function drawNode(
} else { } else {
// Draw text input // Draw text input
let displayText = ""; let displayText = "";
const valueProp = node.props.value; const valueProp = (node.props as InputProps).value;
if (typeof valueProp === "function") { if (typeof valueProp === "function") {
displayText = (valueProp as Accessor<string>)(); displayText = valueProp();
} }
const placeholder = (node.props as InputProps).placeholder;
const placeholder = node.props.placeholder as string | undefined;
const cursorPos = node.cursorPos ?? 0; const cursorPos = node.cursorPos ?? 0;
let currentTextColor = textColor; let currentTextColor = textColor;
let showPlaceholder = false; let showPlaceholder = false;

View File

@@ -31,7 +31,10 @@ const Counter = () => {
div( div(
{ class: "flex flex-row" }, { class: "flex flex-row" },
button({ onClick: () => setCount(count() - 1), class: "text-red" }, "-"), 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( label(
{ {
class: todo.completed ? "ml-1 text-gray" : "ml-1 text-white" class: todo.completed ? "ml-1 text-gray" : "ml-1 text-white",
}, },
() => todo.title () => todo.title,
), ),
button( button(
{ {
@@ -308,7 +311,7 @@ const App = () => {
{ {
when: () => tabIndex() === 0, when: () => tabIndex() === 0,
fallback: Show( fallback: Show(
{ {
when: () => tabIndex() === 1, when: () => tabIndex() === 1,
fallback: Show( fallback: Show(
{ {
@@ -318,13 +321,13 @@ const App = () => {
when: () => tabIndex() === 3, when: () => tabIndex() === 3,
fallback: MultiScrollExample(), fallback: MultiScrollExample(),
}, },
StaticScrollExample() StaticScrollExample(),
) ),
}, },
SimpleScrollExample() SimpleScrollExample(),
) ),
}, },
TodosApp() TodosApp(),
), ),
}, },
Counter(), Counter(),
@@ -347,4 +350,4 @@ try {
print("Error running application:"); print("Error running application:");
printError(e); printError(e);
} }
} }