From 6983dcbaae02c16ec2886a21f60d9e7c80f57006 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Tue, 28 Feb 2023 23:06:18 +0100 Subject: [PATCH 01/27] feat: implement CLI for local testing --- package.json | 7 +- packages/cc/package.json | 6 +- packages/cli/.eslintrc.js | 9 + packages/cli/README.md | 21 + packages/cli/bin/cli.js | 2 + packages/cli/package.json | 58 +++ packages/cli/src/cli.tsx | 51 ++ packages/cli/src/components/confirmExit.tsx | 35 ++ packages/cli/src/components/header.tsx | 35 ++ packages/cli/src/components/mainMenu.tsx | 32 ++ packages/cli/src/components/menu.tsx | 55 +++ packages/cli/src/components/setUSBPath.tsx | 26 + packages/cli/tsconfig.build.json | 11 + packages/cli/tsconfig.json | 9 + packages/config/package.json | 6 +- packages/core/package.json | 6 +- packages/flash/package.json | 6 +- packages/host/package.json | 6 +- packages/maintenance/package.json | 6 +- packages/nvmedit/package.json | 6 +- packages/serial/package.json | 6 +- packages/shared/package.json | 6 +- packages/testing/package.json | 6 +- packages/transformers/package.json | 6 +- packages/zwave-js/package.json | 6 +- yarn.lock | 509 +++++++++++++++++++- 26 files changed, 872 insertions(+), 60 deletions(-) create mode 100644 packages/cli/.eslintrc.js create mode 100644 packages/cli/README.md create mode 100755 packages/cli/bin/cli.js create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/cli.tsx create mode 100644 packages/cli/src/components/confirmExit.tsx create mode 100644 packages/cli/src/components/header.tsx create mode 100644 packages/cli/src/components/mainMenu.tsx create mode 100644 packages/cli/src/components/menu.tsx create mode 100644 packages/cli/src/components/setUSBPath.tsx create mode 100644 packages/cli/tsconfig.build.json create mode 100644 packages/cli/tsconfig.json diff --git a/package.json b/package.json index d40b49d93d79..df095d776edc 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,13 @@ "RoboPhred (https://github.com/RoboPhred)" ], "license": "MIT", - "homepage": "https://github.com/AlCalzone/node-zwave-js#readme", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", "repository": { "type": "git", - "url": "git+https://github.com/AlCalzone/node-zwave-js.git" + "url": "git+https://github.com/zwave-js/node-zwave-js.git" }, "bugs": { - "url": "https://github.com/AlCalzone/node-zwave-js/issues" + "url": "https://github.com/zwave-js/node-zwave-js/issues" }, "funding": { "url": "https://github.com/sponsors/AlCalzone/" @@ -47,6 +47,7 @@ "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "@zwave-js/cc": "workspace:*", + "@zwave-js/cli": "workspace:*", "@zwave-js/config": "workspace:*", "@zwave-js/core": "workspace:*", "@zwave-js/flash": "workspace:*", diff --git a/packages/cc/package.json b/packages/cc/package.json index e6dff346465d..4f0d07d7fb17 100644 --- a/packages/cc/package.json +++ b/packages/cc/package.json @@ -45,13 +45,13 @@ "email": "d.griesel@gmx.net" }, "license": "MIT", - "homepage": "https://github.com/AlCalzone/node-zwave-js#readme", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", "repository": { "type": "git", - "url": "git+https://github.com/AlCalzone/node-zwave-js.git" + "url": "git+https://github.com/zwave-js/node-zwave-js.git" }, "bugs": { - "url": "https://github.com/AlCalzone/node-zwave-js/issues" + "url": "https://github.com/zwave-js/node-zwave-js/issues" }, "funding": { "url": "https://github.com/sponsors/AlCalzone/" diff --git a/packages/cli/.eslintrc.js b/packages/cli/.eslintrc.js new file mode 100644 index 000000000000..ed16d7ebf451 --- /dev/null +++ b/packages/cli/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + extends: "../.eslintrc.js", + parserOptions: { + ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features + sourceType: "module", // Allows for the use of imports + project: "tsconfig.json", + tsconfigRootDir: __dirname, + }, +}; diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 000000000000..ce2e89334688 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,21 @@ +# Z-Wave JS: Firmware Flasher + +CLI utility to flash the firmware on Z-Wave controllers + +**WARNING:** Flashing the wrong firmware may brick your controller. Use at your own risk! + +## Usage + +You can either execute the current version directly from `npm` using + +``` +npx @zwave-js/flash@latest [--verbose] +``` + +or you can execute the version in the checked out repository by executing + +``` +yarn ts packages/flash/src/cli.ts [--verbose] +``` + +The `--verbose` flag will cause the driver logs to be printed to console. diff --git a/packages/cli/bin/cli.js b/packages/cli/bin/cli.js new file mode 100755 index 000000000000..89394a709020 --- /dev/null +++ b/packages/cli/bin/cli.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require("../build/cli.js"); diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 000000000000..d1b846142b5c --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,58 @@ +{ + "name": "@zwave-js/cli", + "version": "10.10.0", + "description": "CLI for Z-Wave JS", + "keywords": [], + "publishConfig": { + "access": "public" + }, + "main": "build/cli.js", + "files": [ + "bin/", + "build/**/*.{js,d.ts,map}" + ], + "bin": "bin/cli.js", + "author": { + "name": "AlCalzone", + "email": "d.griesel@gmx.net" + }, + "license": "MIT", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/zwave-js/node-zwave-js.git" + }, + "bugs": { + "url": "https://github.com/zwave-js/node-zwave-js/issues" + }, + "funding": { + "url": "https://github.com/sponsors/AlCalzone/" + }, + "engines": { + "node": ">=14.13.0" + }, + "scripts": { + "build": "tsc -b tsconfig.build.json", + "clean": "del-cli build/ \"*.tsbuildinfo\"", + "lint:ts": "eslint --ext .ts --rule \"prettier/prettier: off\" \"src/**/*.ts\"", + "lint:ts:fix": "yarn run lint:ts --fix", + "lint:prettier": "prettier -c \"src/**/*.ts\"", + "lint:prettier:fix": "yarn run lint:prettier -w" + }, + "dependencies": { + "zwave-js": "workspace:*" + }, + "devDependencies": { + "@types/ink-big-text": "^1.2.1", + "@types/ink-text-input": "^2.0.2", + "@types/node": "^14.18.36", + "@types/react": "^18", + "del-cli": "^5.0.0", + "ink": "^3.2.0", + "ink-big-text": "^1.2.0", + "ink-text-input": "^4.0.3", + "prettier": "^2.8.1", + "react": "^18.2.0", + "typescript": "4.9.4" + } +} diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx new file mode 100644 index 000000000000..56ce6cfe2d99 --- /dev/null +++ b/packages/cli/src/cli.tsx @@ -0,0 +1,51 @@ +import { Box, render, useApp } from "ink"; +import { useState } from "react"; +import { ConfirmExit } from "./components/confirmExit"; +import { Header } from "./components/header"; +import { MainMenu } from "./components/mainMenu"; +import { SetUSBPath } from "./components/setUSBPath"; + +enum CLIPage { + SetUSBPath, + MainMenu, + StartingDriver, + ConfirmExit, +} + +const CLI: React.FC = () => { + const { exit } = useApp(); + + const [cliPage, setCLIPage] = useState(CLIPage.MainMenu); + const [usbPath, setUSBPath] = useState(undefined); + + return ( + +
+ {cliPage === CLIPage.SetUSBPath && ( + setCLIPage(CLIPage.MainMenu)} + onSubmit={(path) => { + setUSBPath(path); + setCLIPage(CLIPage.MainMenu); + }} + /> + )} + {cliPage === CLIPage.MainMenu && ( + setCLIPage(CLIPage.ConfirmExit)} + onSetUSBPath={() => setCLIPage(CLIPage.SetUSBPath)} + /> + )} + {cliPage === CLIPage.ConfirmExit && ( + setCLIPage(CLIPage.MainMenu)} + onExit={() => exit()} + /> + )} + + ); +}; + +const { waitUntilExit } = render(); +void waitUntilExit(); diff --git a/packages/cli/src/components/confirmExit.tsx b/packages/cli/src/components/confirmExit.tsx new file mode 100644 index 000000000000..39f333422c90 --- /dev/null +++ b/packages/cli/src/components/confirmExit.tsx @@ -0,0 +1,35 @@ +import { useInput } from "ink"; +import { Menu } from "./menu"; + +export interface ConfirmExitProps { + onExit: () => void; + onCancel: () => void; +} + +export const ConfirmExit: React.FC = (props) => { + useInput((input, key) => { + if (key.return) props.onExit(); + else if (key.escape) props.onCancel(); + }); + + return ( + + ); +}; diff --git a/packages/cli/src/components/header.tsx b/packages/cli/src/components/header.tsx new file mode 100644 index 000000000000..163633019298 --- /dev/null +++ b/packages/cli/src/components/header.tsx @@ -0,0 +1,35 @@ +import { Box, Text } from "ink"; +import BigText from "ink-big-text"; + +export interface HeaderProps { + usbPath?: string; +} + +export const Header: React.FC = (props) => { + return ( + + + + {" "} + USB Path:{" "} + {props.usbPath ? ( + {props.usbPath} + ) : ( + "(none)" + )} + + + ); +}; diff --git a/packages/cli/src/components/mainMenu.tsx b/packages/cli/src/components/mainMenu.tsx new file mode 100644 index 000000000000..0ede02b9dc4c --- /dev/null +++ b/packages/cli/src/components/mainMenu.tsx @@ -0,0 +1,32 @@ +import { Menu } from "./menu"; + +export interface MainMenuProps { + onSetUSBPath: () => void; + onExit: () => void; +} + +export const MainMenu: React.FC = (props) => { + return ( + + ); +}; diff --git a/packages/cli/src/components/menu.tsx b/packages/cli/src/components/menu.tsx new file mode 100644 index 000000000000..d010c0c34e00 --- /dev/null +++ b/packages/cli/src/components/menu.tsx @@ -0,0 +1,55 @@ +import { Box, Text, useInput } from "ink"; +import type { ComponentPropsWithoutRef } from "react"; + +export interface MenuProps { + label?: string; + layoutProps?: ComponentPropsWithoutRef; + textProps?: ComponentPropsWithoutRef; + options: { + input: string; + label: string; + textProps?: ComponentPropsWithoutRef; + onSelect: () => void; + }[]; +} + +export const Menu: React.FC = (props) => { + const { options } = props; + useInput((input, key) => { + const option = options.find((o) => o.input === input); + if (option) { + option.onSelect(); + } + }); + + const innerBox = ( + + {options.map(({ input, label, textProps }) => ( + + {input}. {label} + + ))} + + ); + + if (props.label) { + return ( + + {props.label} + {innerBox} + + ); + } else { + return innerBox; + } +}; diff --git a/packages/cli/src/components/setUSBPath.tsx b/packages/cli/src/components/setUSBPath.tsx new file mode 100644 index 000000000000..1336f4097d42 --- /dev/null +++ b/packages/cli/src/components/setUSBPath.tsx @@ -0,0 +1,26 @@ +import { Box, Text, useInput } from "ink"; +import { UncontrolledTextInput } from "ink-text-input"; + +export interface SetUSBPathProps { + path?: string; + onSubmit: (path: string) => void; + onCancel: () => void; +} + +export const SetUSBPath: React.FC = (props) => { + useInput((input, key) => { + if (key.escape) props.onCancel(); + }); + + return ( + + Enter USB path: + + + + + ); +}; diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json new file mode 100644 index 000000000000..01333ab04dbc --- /dev/null +++ b/packages/cli/tsconfig.build.json @@ -0,0 +1,11 @@ +// tsconfig for building - only applies to the src directory +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "jsx": "react-jsx" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 000000000000..0b343ce64925 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,9 @@ +// tsconfig for IntelliSense - active in all files in the current package +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "jsx": "react-jsx" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["build/**", "node_modules/**"] +} diff --git a/packages/config/package.json b/packages/config/package.json index 31685efbc08b..4f703351a068 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -37,13 +37,13 @@ "email": "d.griesel@gmx.net" }, "license": "MIT", - "homepage": "https://github.com/AlCalzone/node-zwave-js#readme", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", "repository": { "type": "git", - "url": "git+https://github.com/AlCalzone/node-zwave-js.git" + "url": "git+https://github.com/zwave-js/node-zwave-js.git" }, "bugs": { - "url": "https://github.com/AlCalzone/node-zwave-js/issues" + "url": "https://github.com/zwave-js/node-zwave-js/issues" }, "funding": { "url": "https://github.com/sponsors/AlCalzone/" diff --git a/packages/core/package.json b/packages/core/package.json index 2ea76ec57a66..5b367eda8d3a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -50,13 +50,13 @@ "email": "d.griesel@gmx.net" }, "license": "MIT", - "homepage": "https://github.com/AlCalzone/node-zwave-js#readme", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", "repository": { "type": "git", - "url": "git+https://github.com/AlCalzone/node-zwave-js.git" + "url": "git+https://github.com/zwave-js/node-zwave-js.git" }, "bugs": { - "url": "https://github.com/AlCalzone/node-zwave-js/issues" + "url": "https://github.com/zwave-js/node-zwave-js/issues" }, "funding": { "url": "https://github.com/sponsors/AlCalzone/" diff --git a/packages/flash/package.json b/packages/flash/package.json index 2a8cb16c7fd6..5ffdb0cc284b 100644 --- a/packages/flash/package.json +++ b/packages/flash/package.json @@ -17,13 +17,13 @@ "email": "d.griesel@gmx.net" }, "license": "MIT", - "homepage": "https://github.com/AlCalzone/node-zwave-js#readme", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", "repository": { "type": "git", - "url": "git+https://github.com/AlCalzone/node-zwave-js.git" + "url": "git+https://github.com/zwave-js/node-zwave-js.git" }, "bugs": { - "url": "https://github.com/AlCalzone/node-zwave-js/issues" + "url": "https://github.com/zwave-js/node-zwave-js/issues" }, "funding": { "url": "https://github.com/sponsors/AlCalzone/" diff --git a/packages/host/package.json b/packages/host/package.json index 3c771075aeab..3e4ea7ccff51 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -36,13 +36,13 @@ "email": "d.griesel@gmx.net" }, "license": "MIT", - "homepage": "https://github.com/AlCalzone/node-zwave-js#readme", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", "repository": { "type": "git", - "url": "git+https://github.com/AlCalzone/node-zwave-js.git" + "url": "git+https://github.com/zwave-js/node-zwave-js.git" }, "bugs": { - "url": "https://github.com/AlCalzone/node-zwave-js/issues" + "url": "https://github.com/zwave-js/node-zwave-js/issues" }, "funding": { "url": "https://github.com/sponsors/AlCalzone/" diff --git a/packages/maintenance/package.json b/packages/maintenance/package.json index 34357d587c2a..bfe0e4637f3e 100644 --- a/packages/maintenance/package.json +++ b/packages/maintenance/package.json @@ -14,13 +14,13 @@ "email": "d.griesel@gmx.net" }, "license": "MIT", - "homepage": "https://github.com/AlCalzone/node-zwave-js#readme", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", "repository": { "type": "git", - "url": "git+https://github.com/AlCalzone/node-zwave-js.git" + "url": "git+https://github.com/zwave-js/node-zwave-js.git" }, "bugs": { - "url": "https://github.com/AlCalzone/node-zwave-js/issues" + "url": "https://github.com/zwave-js/node-zwave-js/issues" }, "funding": { "url": "https://github.com/sponsors/AlCalzone/" diff --git a/packages/nvmedit/package.json b/packages/nvmedit/package.json index a1ad4c2be8b9..10f67bc643bb 100644 --- a/packages/nvmedit/package.json +++ b/packages/nvmedit/package.json @@ -38,13 +38,13 @@ "email": "d.griesel@gmx.net" }, "license": "MIT", - "homepage": "https://github.com/AlCalzone/node-zwave-js#readme", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", "repository": { "type": "git", - "url": "git+https://github.com/AlCalzone/node-zwave-js.git" + "url": "git+https://github.com/zwave-js/node-zwave-js.git" }, "bugs": { - "url": "https://github.com/AlCalzone/node-zwave-js/issues" + "url": "https://github.com/zwave-js/node-zwave-js/issues" }, "funding": { "url": "https://github.com/sponsors/AlCalzone/" diff --git a/packages/serial/package.json b/packages/serial/package.json index 3fc88001519c..3876c21683c6 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -43,13 +43,13 @@ "email": "d.griesel@gmx.net" }, "license": "MIT", - "homepage": "https://github.com/AlCalzone/node-zwave-js#readme", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", "repository": { "type": "git", - "url": "git+https://github.com/AlCalzone/node-zwave-js.git" + "url": "git+https://github.com/zwave-js/node-zwave-js.git" }, "bugs": { - "url": "https://github.com/AlCalzone/node-zwave-js/issues" + "url": "https://github.com/zwave-js/node-zwave-js/issues" }, "funding": { "url": "https://github.com/sponsors/AlCalzone/" diff --git a/packages/shared/package.json b/packages/shared/package.json index 9d95c9a51007..261151f21621 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -36,13 +36,13 @@ "email": "d.griesel@gmx.net" }, "license": "MIT", - "homepage": "https://github.com/AlCalzone/node-zwave-js#readme", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", "repository": { "type": "git", - "url": "git+https://github.com/AlCalzone/node-zwave-js.git" + "url": "git+https://github.com/zwave-js/node-zwave-js.git" }, "bugs": { - "url": "https://github.com/AlCalzone/node-zwave-js/issues" + "url": "https://github.com/zwave-js/node-zwave-js/issues" }, "funding": { "url": "https://github.com/sponsors/AlCalzone/" diff --git a/packages/testing/package.json b/packages/testing/package.json index 86fd5aad29b6..b255aa1c2b03 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -17,13 +17,13 @@ "email": "d.griesel@gmx.net" }, "license": "MIT", - "homepage": "https://github.com/AlCalzone/node-zwave-js#readme", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", "repository": { "type": "git", - "url": "git+https://github.com/AlCalzone/node-zwave-js.git" + "url": "git+https://github.com/zwave-js/node-zwave-js.git" }, "bugs": { - "url": "https://github.com/AlCalzone/node-zwave-js/issues" + "url": "https://github.com/zwave-js/node-zwave-js/issues" }, "funding": { "url": "https://github.com/sponsors/AlCalzone/" diff --git a/packages/transformers/package.json b/packages/transformers/package.json index d5b2b2b06556..23f55cfc8623 100644 --- a/packages/transformers/package.json +++ b/packages/transformers/package.json @@ -14,13 +14,13 @@ "email": "d.griesel@gmx.net" }, "license": "MIT", - "homepage": "https://github.com/AlCalzone/node-zwave-js#readme", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", "repository": { "type": "git", - "url": "git+https://github.com/AlCalzone/node-zwave-js.git" + "url": "git+https://github.com/zwave-js/node-zwave-js.git" }, "bugs": { - "url": "https://github.com/AlCalzone/node-zwave-js/issues" + "url": "https://github.com/zwave-js/node-zwave-js/issues" }, "funding": { "url": "https://github.com/sponsors/AlCalzone/" diff --git a/packages/zwave-js/package.json b/packages/zwave-js/package.json index 02a69a9bd254..73bce2727be4 100644 --- a/packages/zwave-js/package.json +++ b/packages/zwave-js/package.json @@ -84,13 +84,13 @@ "email": "d.griesel@gmx.net" }, "license": "MIT", - "homepage": "https://github.com/AlCalzone/node-zwave-js#readme", + "homepage": "https://github.com/zwave-js/node-zwave-js#readme", "repository": { "type": "git", - "url": "git+https://github.com/AlCalzone/node-zwave-js.git" + "url": "git+https://github.com/zwave-js/node-zwave-js.git" }, "bugs": { - "url": "https://github.com/AlCalzone/node-zwave-js/issues" + "url": "https://github.com/zwave-js/node-zwave-js/issues" }, "funding": { "url": "https://github.com/sponsors/AlCalzone/" diff --git a/yarn.lock b/yarn.lock index 105388363483..a2490818af7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1379,6 +1379,34 @@ __metadata: languageName: node linkType: hard +"@types/ink-big-text@npm:^1.2.1": + version: 1.2.1 + resolution: "@types/ink-big-text@npm:1.2.1" + dependencies: + "@types/react": "*" + checksum: 33f0974dab7f9988027db6e7e26610d9d3f007aa64df5fea5fbb7441541222e9d1292edb4772b710a0ab2cf40801cbac70246cedce1dde209314f7e367684b56 + languageName: node + linkType: hard + +"@types/ink-text-input@npm:^2.0.2": + version: 2.0.2 + resolution: "@types/ink-text-input@npm:2.0.2" + dependencies: + "@types/ink": ^0.5.2 + checksum: 47b879e042bf42fbf4c84bd507bf4ddebb0512cc009232607813248f03aaf49fb2c2d0db28675132d4a8849878e6c47eb56dfc59aa35acabc8c665d551586a35 + languageName: node + linkType: hard + +"@types/ink@npm:^0.5.2": + version: 0.5.2 + resolution: "@types/ink@npm:0.5.2" + dependencies: + "@types/node": "*" + "@types/prop-types": "*" + checksum: 38b9e2933792b48944d53157fa6c84ce2f170744e5f5d14463d1cc2077961d64c15718650c9035df58619a463225b39e6304042d6b0b19ddfbd61d0441a15232 + languageName: node + linkType: hard + "@types/js-levenshtein@npm:^1.1.1": version: 1.1.1 resolution: "@types/js-levenshtein@npm:1.1.1" @@ -1470,6 +1498,13 @@ __metadata: languageName: node linkType: hard +"@types/prop-types@npm:*": + version: 15.7.5 + resolution: "@types/prop-types@npm:15.7.5" + checksum: 5b43b8b15415e1f298243165f1d44390403bb2bd42e662bca3b5b5633fdd39c938e91b7fce3a9483699db0f7a715d08cef220c121f723a634972fdf596aec980 + languageName: node + linkType: hard + "@types/proper-lockfile@npm:^4.1.2": version: 4.1.2 resolution: "@types/proper-lockfile@npm:4.1.2" @@ -1486,6 +1521,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:*, @types/react@npm:^18": + version: 18.0.28 + resolution: "@types/react@npm:18.0.28" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: e752df961105e5127652460504785897ca6e77259e0da8f233f694f9e8f451cde7fa0709d4456ade0ff600c8ce909cfe29f9b08b9c247fa9b734e126ec53edd7 + languageName: node + linkType: hard + "@types/retry@npm:*": version: 0.12.0 resolution: "@types/retry@npm:0.12.0" @@ -1493,6 +1539,13 @@ __metadata: languageName: node linkType: hard +"@types/scheduler@npm:*": + version: 0.16.2 + resolution: "@types/scheduler@npm:0.16.2" + checksum: b6b4dcfeae6deba2e06a70941860fb1435730576d3689225a421280b7742318d1548b3d22c1f66ab68e414f346a9542f29240bc955b6332c5b11e561077583bc + languageName: node + linkType: hard + "@types/semver@npm:^7.3.12": version: 7.3.12 resolution: "@types/semver@npm:7.3.12" @@ -1564,6 +1617,13 @@ __metadata: languageName: node linkType: hard +"@types/yoga-layout@npm:1.9.2": + version: 1.9.2 + resolution: "@types/yoga-layout@npm:1.9.2" + checksum: dbc3d6ab997d50fe1fcca5dd6822982c8fe586145ab648e0e97c3bc4ebc93d0b40c9edd75febaba374d61f60c1379b639f6be652965c776a901bf1068f2eac87 + languageName: node + linkType: hard + "@typescript-eslint/eslint-plugin@npm:^5.48.0": version: 5.48.0 resolution: "@typescript-eslint/eslint-plugin@npm:5.48.0" @@ -1730,6 +1790,27 @@ __metadata: languageName: unknown linkType: soft +"@zwave-js/cli@workspace:*, @zwave-js/cli@workspace:packages/cli": + version: 0.0.0-use.local + resolution: "@zwave-js/cli@workspace:packages/cli" + dependencies: + "@types/ink-big-text": ^1.2.1 + "@types/ink-text-input": ^2.0.2 + "@types/node": ^14.18.36 + "@types/react": ^18 + del-cli: ^5.0.0 + ink: ^3.2.0 + ink-big-text: ^1.2.0 + ink-text-input: ^4.0.3 + prettier: ^2.8.1 + react: ^18.2.0 + typescript: 4.9.4 + zwave-js: "workspace:*" + bin: + cli: bin/cli.js + languageName: unknown + linkType: soft + "@zwave-js/config@workspace:*, @zwave-js/config@workspace:packages/config": version: 0.0.0-use.local resolution: "@zwave-js/config@workspace:packages/config" @@ -1916,6 +1997,7 @@ __metadata: "@typescript-eslint/eslint-plugin": ^5.48.0 "@typescript-eslint/parser": ^5.48.0 "@zwave-js/cc": "workspace:*" + "@zwave-js/cli": "workspace:*" "@zwave-js/config": "workspace:*" "@zwave-js/core": "workspace:*" "@zwave-js/flash": "workspace:*" @@ -2384,6 +2466,13 @@ __metadata: languageName: node linkType: hard +"astral-regex@npm:^2.0.0": + version: 2.0.0 + resolution: "astral-regex@npm:2.0.0" + checksum: 876231688c66400473ba505731df37ea436e574dd524520294cc3bbc54ea40334865e01fa0d074d74d036ee874ee7e62f486ea38bc421ee8e6a871c06f011766 + languageName: node + linkType: hard + "async@npm:^3.2.3": version: 3.2.3 resolution: "async@npm:3.2.3" @@ -2405,6 +2494,13 @@ __metadata: languageName: node linkType: hard +"auto-bind@npm:4.0.0": + version: 4.0.0 + resolution: "auto-bind@npm:4.0.0" + checksum: 00cad71cce5742faccb7dd65c1b55ebc4f45add4b0c9a1547b10b05bab22813230133b0c892c67ba3eb969a4524710c5e43cc45c72898ec84e56f3a596e7a04f + languageName: node + linkType: hard + "ava@npm:^4.3.3": version: 4.3.3 resolution: "ava@npm:4.3.3" @@ -2674,6 +2770,18 @@ __metadata: languageName: node linkType: hard +"cfonts@npm:^2.8.6": + version: 2.10.1 + resolution: "cfonts@npm:2.10.1" + dependencies: + chalk: ^4 + window-size: ^1.1.1 + bin: + cfonts: bin/index.js + checksum: 2250d0878609c5cd39eced4212b708257a515f17fed83943f01423f6e11c091b77e30d56254b356f74bbb0e3f7d5f6782088d6e7e9985f3c9d8ec4bb34a15ad2 + languageName: node + linkType: hard + "chalk@npm:^2.0.0, chalk@npm:^2.4.1, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -2685,23 +2793,23 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4.0.0": - version: 4.1.1 - resolution: "chalk@npm:4.1.1" +"chalk@npm:^4, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" dependencies: ansi-styles: ^4.1.0 supports-color: ^7.1.0 - checksum: 036e973e665ba1a32c975e291d5f3d549bceeb7b1b983320d4598fb75d70fe20c5db5d62971ec0fe76cdbce83985a00ee42372416abfc3a5584465005a7855ed + checksum: fe75c9d5c76a7a98d45495b91b2172fa3b7a09e0cc9370e5c8feb1c567b85c4288e2b3fded7cfdd7359ac28d6b3844feb8b82b8686842e93d23c827c417e83fc languageName: node linkType: hard -"chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": - version: 4.1.2 - resolution: "chalk@npm:4.1.2" +"chalk@npm:^4.0.0": + version: 4.1.1 + resolution: "chalk@npm:4.1.1" dependencies: ansi-styles: ^4.1.0 supports-color: ^7.1.0 - checksum: fe75c9d5c76a7a98d45495b91b2172fa3b7a09e0cc9370e5c8feb1c567b85c4288e2b3fded7cfdd7359ac28d6b3844feb8b82b8686842e93d23c827c417e83fc + checksum: 036e973e665ba1a32c975e291d5f3d549bceeb7b1b983320d4598fb75d70fe20c5db5d62971ec0fe76cdbce83985a00ee42372416abfc3a5584465005a7855ed languageName: node linkType: hard @@ -2752,6 +2860,13 @@ __metadata: languageName: node linkType: hard +"ci-info@npm:^2.0.0": + version: 2.0.0 + resolution: "ci-info@npm:2.0.0" + checksum: 3b374666a85ea3ca43fa49aa3a048d21c9b475c96eb13c133505d2324e7ae5efd6a454f41efe46a152269e9b6a00c9edbe63ec7fa1921957165aae16625acd67 + languageName: node + linkType: hard + "ci-info@npm:^3.3.1": version: 3.4.0 resolution: "ci-info@npm:3.4.0" @@ -2789,6 +2904,13 @@ __metadata: languageName: node linkType: hard +"cli-boxes@npm:^2.2.0": + version: 2.2.1 + resolution: "cli-boxes@npm:2.2.1" + checksum: be79f8ec23a558b49e01311b39a1ea01243ecee30539c880cf14bf518a12e223ef40c57ead0cb44f509bffdffc5c129c746cd50d863ab879385370112af4f585 + languageName: node + linkType: hard + "cli-cursor@npm:^2.1.0": version: 2.1.0 resolution: "cli-cursor@npm:2.1.0" @@ -2830,6 +2952,16 @@ __metadata: languageName: node linkType: hard +"cli-truncate@npm:^2.1.0": + version: 2.1.0 + resolution: "cli-truncate@npm:2.1.0" + dependencies: + slice-ansi: ^3.0.0 + string-width: ^4.2.0 + checksum: bf1e4e6195392dc718bf9cd71f317b6300dc4a9191d052f31046b8773230ece4fa09458813bf0e3455a5e68c0690d2ea2c197d14a8b85a7b5e01c97f4b5feb5d + languageName: node + linkType: hard + "cli-truncate@npm:^3.1.0": version: 3.1.0 resolution: "cli-truncate@npm:3.1.0" @@ -2890,6 +3022,15 @@ __metadata: languageName: node linkType: hard +"code-excerpt@npm:^3.0.0": + version: 3.0.0 + resolution: "code-excerpt@npm:3.0.0" + dependencies: + convert-to-spaces: ^1.0.1 + checksum: fa3a8ed15967076a43a4093b0c824cf0ada15d9aab12ea3c028851b72a69b56495aac1eadf18c3b6ae4baf0a95bb1e1faa9dbeeb0a2b2b5ae058da23328e9dd8 + languageName: node + linkType: hard + "code-excerpt@npm:^4.0.0": version: 4.0.0 resolution: "code-excerpt@npm:4.0.0" @@ -3147,6 +3288,13 @@ __metadata: languageName: node linkType: hard +"convert-to-spaces@npm:^1.0.1": + version: 1.0.2 + resolution: "convert-to-spaces@npm:1.0.2" + checksum: e73f2ae39eb2b184f0796138eaab9c088b03b94937377d31be5b2282aef6a6ccce6b46f51bd99b3b7dfc70f516e2a6b16c0dd911883bfadf8d1073f462480224 + languageName: node + linkType: hard + "convert-to-spaces@npm:^2.0.1": version: 2.0.1 resolution: "convert-to-spaces@npm:2.0.1" @@ -3271,6 +3419,13 @@ __metadata: languageName: node linkType: hard +"csstype@npm:^3.0.2": + version: 3.1.1 + resolution: "csstype@npm:3.1.1" + checksum: 1f7b4f5fdd955b7444b18ebdddf3f5c699159f13e9cf8ac9027ae4a60ae226aef9bbb14a6e12ca7dba3358b007cee6354b116e720262867c398de6c955ea451d + languageName: node + linkType: hard + "currently-unhandled@npm:^0.4.1": version: 0.4.1 resolution: "currently-unhandled@npm:0.4.1" @@ -3438,6 +3593,15 @@ __metadata: languageName: node linkType: hard +"define-property@npm:^1.0.0": + version: 1.0.0 + resolution: "define-property@npm:1.0.0" + dependencies: + is-descriptor: ^1.0.0 + checksum: 5fbed11dace44dd22914035ba9ae83ad06008532ca814d7936a53a09e897838acdad5b108dd0688cc8d2a7cf0681acbe00ee4136cf36743f680d10517379350a + languageName: node + linkType: hard + "del-cli@npm:^5.0.0": version: 5.0.0 resolution: "del-cli@npm:5.0.0" @@ -5062,6 +5226,69 @@ __metadata: languageName: node linkType: hard +"ink-big-text@npm:^1.2.0": + version: 1.2.0 + resolution: "ink-big-text@npm:1.2.0" + dependencies: + cfonts: ^2.8.6 + prop-types: ^15.7.2 + peerDependencies: + ink: ">=2.0.0" + react: ">=16.8.0" + checksum: b65481453cc13f726bdecf6378da30d55abb676e5eea5f6f38059d3cf7fa7e474c35ff10b760deecded246e9aabb68d22154c0e760860e2d05ca2d2ae60e5052 + languageName: node + linkType: hard + +"ink-text-input@npm:^4.0.3": + version: 4.0.3 + resolution: "ink-text-input@npm:4.0.3" + dependencies: + chalk: ^4.1.0 + type-fest: ^0.15.1 + peerDependencies: + ink: ^3.0.0-3 + react: ^16.5.2 || ^17.0.0 + checksum: 2d309ec8ca386010d467822e317389e3c60b764fd04091df063a45c31f43104fd9f4a4e71a928a2c3c3cca461a9b8a526e90439616760f0f3726507132abbac5 + languageName: node + linkType: hard + +"ink@npm:^3.2.0": + version: 3.2.0 + resolution: "ink@npm:3.2.0" + dependencies: + ansi-escapes: ^4.2.1 + auto-bind: 4.0.0 + chalk: ^4.1.0 + cli-boxes: ^2.2.0 + cli-cursor: ^3.1.0 + cli-truncate: ^2.1.0 + code-excerpt: ^3.0.0 + indent-string: ^4.0.0 + is-ci: ^2.0.0 + lodash: ^4.17.20 + patch-console: ^1.0.0 + react-devtools-core: ^4.19.1 + react-reconciler: ^0.26.2 + scheduler: ^0.20.2 + signal-exit: ^3.0.2 + slice-ansi: ^3.0.0 + stack-utils: ^2.0.2 + string-width: ^4.2.2 + type-fest: ^0.12.0 + widest-line: ^3.1.0 + wrap-ansi: ^6.2.0 + ws: ^7.5.5 + yoga-layout-prebuilt: ^1.9.6 + peerDependencies: + "@types/react": ">=16.8.0" + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 35f1b733b94bf12cc0bf7acb4d3fcba9d961ede15cee9c64a7325606b74cee78e1009eaffbac127f4d7d28e758d8259dea8d0850bfacb991b8d93632f41d3fa2 + languageName: node + linkType: hard + "inquirer@npm:6.5.2": version: 6.5.2 resolution: "inquirer@npm:6.5.2" @@ -5127,6 +5354,15 @@ __metadata: languageName: node linkType: hard +"is-accessor-descriptor@npm:^1.0.0": + version: 1.0.0 + resolution: "is-accessor-descriptor@npm:1.0.0" + dependencies: + kind-of: ^6.0.0 + checksum: 8e475968e9b22f9849343c25854fa24492dbe8ba0dea1a818978f9f1b887339190b022c9300d08c47fe36f1b913d70ce8cbaca00369c55a56705fdb7caed37fe + languageName: node + linkType: hard + "is-arrayish@npm:^0.2.1": version: 0.2.1 resolution: "is-arrayish@npm:0.2.1" @@ -5150,6 +5386,24 @@ __metadata: languageName: node linkType: hard +"is-buffer@npm:^1.1.5": + version: 1.1.6 + resolution: "is-buffer@npm:1.1.6" + checksum: 4a186d995d8bbf9153b4bd9ff9fd04ae75068fe695d29025d25e592d9488911eeece84eefbd8fa41b8ddcc0711058a71d4c466dcf6f1f6e1d83830052d8ca707 + languageName: node + linkType: hard + +"is-ci@npm:^2.0.0": + version: 2.0.0 + resolution: "is-ci@npm:2.0.0" + dependencies: + ci-info: ^2.0.0 + bin: + is-ci: bin.js + checksum: 77b869057510f3efa439bbb36e9be429d53b3f51abd4776eeea79ab3b221337fe1753d1e50058a9e2c650d38246108beffb15ccfd443929d77748d8c0cc90144 + languageName: node + linkType: hard + "is-core-module@npm:^2.1.0": version: 2.9.0 resolution: "is-core-module@npm:2.9.0" @@ -5186,6 +5440,26 @@ __metadata: languageName: node linkType: hard +"is-data-descriptor@npm:^1.0.0": + version: 1.0.0 + resolution: "is-data-descriptor@npm:1.0.0" + dependencies: + kind-of: ^6.0.0 + checksum: e705e6816241c013b05a65dc452244ee378d1c3e3842bd140beabe6e12c0d700ef23c91803f971aa7b091fb0573c5da8963af34a2b573337d87bc3e1f53a4e6d + languageName: node + linkType: hard + +"is-descriptor@npm:^1.0.0": + version: 1.0.2 + resolution: "is-descriptor@npm:1.0.2" + dependencies: + is-accessor-descriptor: ^1.0.0 + is-data-descriptor: ^1.0.0 + kind-of: ^6.0.2 + checksum: 2ed623560bee035fb67b23e32ce885700bef8abe3fbf8c909907d86507b91a2c89a9d3a4d835a4d7334dd5db0237a0aeae9ca109c1e4ef1c0e7b577c0846ab5a + languageName: node + linkType: hard + "is-docker@npm:^2.0.0": version: 2.2.1 resolution: "is-docker@npm:2.2.1" @@ -5271,6 +5545,15 @@ __metadata: languageName: node linkType: hard +"is-number@npm:^3.0.0": + version: 3.0.0 + resolution: "is-number@npm:3.0.0" + dependencies: + kind-of: ^3.0.2 + checksum: 0c62bf8e9d72c4dd203a74d8cfc751c746e75513380fef420cda8237e619a988ee43e678ddb23c87ac24d91ac0fe9f22e4ffb1301a50310c697e9d73ca3994e9 + languageName: node + linkType: hard + "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -5450,7 +5733,7 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^4.0.0": +"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" checksum: 8a95213a5a77deb6cbe94d86340e8d9ace2b93bc367790b260101d2f36a2eaf4e4e22d9fa9cf459b38af3a32fb4190e638024cf82ec95ef708680e405ea7cc78 @@ -5586,7 +5869,16 @@ __metadata: languageName: node linkType: hard -"kind-of@npm:^6.0.2, kind-of@npm:^6.0.3": +"kind-of@npm:^3.0.2": + version: 3.2.2 + resolution: "kind-of@npm:3.2.2" + dependencies: + is-buffer: ^1.1.5 + checksum: e898df8ca2f31038f27d24f0b8080da7be274f986bc6ed176f37c77c454d76627619e1681f6f9d2e8d2fd7557a18ecc419a6bb54e422abcbb8da8f1a75e4b386 + languageName: node + linkType: hard + +"kind-of@npm:^6.0.0, kind-of@npm:^6.0.2, kind-of@npm:^6.0.3": version: 6.0.3 resolution: "kind-of@npm:6.0.3" checksum: 3ab01e7b1d440b22fe4c31f23d8d38b4d9b91d9f291df683476576493d5dfd2e03848a8b05813dd0c3f0e835bc63f433007ddeceb71f05cb25c45ae1b19c6d3b @@ -5760,6 +6052,17 @@ __metadata: languageName: node linkType: hard +"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": + version: 1.4.0 + resolution: "loose-envify@npm:1.4.0" + dependencies: + js-tokens: ^3.0.0 || ^4.0.0 + bin: + loose-envify: cli.js + checksum: 6517e24e0cad87ec9888f500c5b5947032cdfe6ef65e1c1936a0c48a524b81e65542c9c3edc91c97d5bddc806ee2a985dbc79be89215d613b1de5db6d1cfe6f4 + languageName: node + linkType: hard + "lowercase-keys@npm:^3.0.0": version: 3.0.0 resolution: "lowercase-keys@npm:3.0.0" @@ -6423,7 +6726,7 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4.0.1, object-assign@npm:^4.1.0": +"object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f @@ -6712,6 +7015,13 @@ __metadata: languageName: node linkType: hard +"patch-console@npm:^1.0.0": + version: 1.0.0 + resolution: "patch-console@npm:1.0.0" + checksum: 8cd738aa470f2e9463fca35da6a19403384ac555004f698ddd3dfdb69135ab60fe9bd2edd1dbdd8c09d92c0a2190fd0f7337fe48123013baf8ffec8532885a3a + languageName: node + linkType: hard + "path-browserify@npm:^1.0.1": version: 1.0.1 resolution: "path-browserify@npm:1.0.1" @@ -6916,6 +7226,17 @@ __metadata: languageName: node linkType: hard +"prop-types@npm:^15.7.2": + version: 15.8.1 + resolution: "prop-types@npm:15.8.1" + dependencies: + loose-envify: ^1.4.0 + object-assign: ^4.1.1 + react-is: ^16.13.1 + checksum: c056d3f1c057cb7ff8344c645450e14f088a915d078dcda795041765047fa080d38e5d626560ccaac94a4e16e3aa15f3557c1a9a8d1174530955e992c675e459 + languageName: node + linkType: hard + "proper-lockfile@npm:^4.1.2": version: 4.1.2 resolution: "proper-lockfile@npm:4.1.2" @@ -6983,6 +7304,45 @@ __metadata: languageName: node linkType: hard +"react-devtools-core@npm:^4.19.1": + version: 4.27.2 + resolution: "react-devtools-core@npm:4.27.2" + dependencies: + shell-quote: ^1.6.1 + ws: ^7 + checksum: f52e2b05b8043c79fce6c0f9c93579f731a1850af79442ac7b8dfde5fb12e03f7d4f48dafc3c84e28c3675565f4af8a7002e49bcab862ece89c90dcef850a813 + languageName: node + linkType: hard + +"react-is@npm:^16.13.1": + version: 16.13.1 + resolution: "react-is@npm:16.13.1" + checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f + languageName: node + linkType: hard + +"react-reconciler@npm:^0.26.2": + version: 0.26.2 + resolution: "react-reconciler@npm:0.26.2" + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + scheduler: ^0.20.2 + peerDependencies: + react: ^17.0.2 + checksum: 2ebceace56f547f51eaf142becefef9cca980eae4f42d90ee5a966f54a375f5082d78b71b00c40bbd9bca69e0e0f698c7d4e81cc7373437caa19831fddc1d01b + languageName: node + linkType: hard + +"react@npm:^18.2.0": + version: 18.2.0 + resolution: "react@npm:18.2.0" + dependencies: + loose-envify: ^1.1.0 + checksum: 88e38092da8839b830cda6feef2e8505dec8ace60579e46aa5490fc3dc9bba0bd50336507dc166f43e3afc1c42939c09fe33b25fae889d6f402721dcd78fca1b + languageName: node + linkType: hard + "read-pkg-up@npm:^7.0.1": version: 7.0.1 resolution: "read-pkg-up@npm:7.0.1" @@ -7417,6 +7777,16 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.20.2": + version: 0.20.2 + resolution: "scheduler@npm:0.20.2" + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + checksum: c4b35cf967c8f0d3e65753252d0f260271f81a81e427241295c5a7b783abf4ea9e905f22f815ab66676f5313be0a25f47be582254db8f9241b259213e999b8fc + languageName: node + linkType: hard + "semver@npm:2 || 3 || 4 || 5, semver@npm:^5.5.0": version: 5.7.1 resolution: "semver@npm:5.7.1" @@ -7527,6 +7897,13 @@ __metadata: languageName: node linkType: hard +"shell-quote@npm:^1.6.1": + version: 1.8.0 + resolution: "shell-quote@npm:1.8.0" + checksum: 6ef7c5e308b9c77eedded882653a132214fa98b4a1512bb507588cf6cd2fc78bfee73e945d0c3211af028a1eabe09c6a19b96edd8977dc149810797e93809749 + languageName: node + linkType: hard + "shelljs@npm:^0.8.5": version: 0.8.5 resolution: "shelljs@npm:0.8.5" @@ -7591,6 +7968,17 @@ __metadata: languageName: node linkType: hard +"slice-ansi@npm:^3.0.0": + version: 3.0.0 + resolution: "slice-ansi@npm:3.0.0" + dependencies: + ansi-styles: ^4.0.0 + astral-regex: ^2.0.0 + is-fullwidth-code-point: ^3.0.0 + checksum: 5ec6d022d12e016347e9e3e98a7eb2a592213a43a65f1b61b74d2c78288da0aded781f665807a9f3876b9daa9ad94f64f77d7633a0458876c3a4fdc4eb223f24 + languageName: node + linkType: hard + "slice-ansi@npm:^5.0.0": version: 5.0.0 resolution: "slice-ansi@npm:5.0.0" @@ -7712,6 +8100,15 @@ __metadata: languageName: node linkType: hard +"stack-utils@npm:^2.0.2": + version: 2.0.6 + resolution: "stack-utils@npm:2.0.6" + dependencies: + escape-string-regexp: ^2.0.0 + checksum: 052bf4d25bbf5f78e06c1d5e67de2e088b06871fa04107ca8d3f0e9d9263326e2942c8bedee3545795fc77d787d443a538345eef74db2f8e35db3558c6f91ff7 + languageName: node + linkType: hard + "stack-utils@npm:^2.0.5": version: 2.0.5 resolution: "stack-utils@npm:2.0.5" @@ -7749,25 +8146,25 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^4.1.0, string-width@npm:^4.2.0": - version: 4.2.2 - resolution: "string-width@npm:4.2.2" +"string-width@npm:^4.0.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" dependencies: emoji-regex: ^8.0.0 is-fullwidth-code-point: ^3.0.0 - strip-ansi: ^6.0.0 - checksum: 343e089b0e66e0f72aab4ad1d9b6f2c9cc5255844b0c83fd9b53f2a3b3fd0421bdd6cb05be96a73117eb012db0887a6c1d64ca95aaa50c518e48980483fea0ab + strip-ansi: ^6.0.1 + checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb languageName: node linkType: hard -"string-width@npm:^4.2.3": - version: 4.2.3 - resolution: "string-width@npm:4.2.3" +"string-width@npm:^4.1.0, string-width@npm:^4.2.0": + version: 4.2.2 + resolution: "string-width@npm:4.2.2" dependencies: emoji-regex: ^8.0.0 is-fullwidth-code-point: ^3.0.0 - strip-ansi: ^6.0.1 - checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb + strip-ansi: ^6.0.0 + checksum: 343e089b0e66e0f72aab4ad1d9b6f2c9cc5255844b0c83fd9b53f2a3b3fd0421bdd6cb05be96a73117eb012db0887a6c1d64ca95aaa50c518e48980483fea0ab languageName: node linkType: hard @@ -8315,6 +8712,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.12.0": + version: 0.12.0 + resolution: "type-fest@npm:0.12.0" + checksum: 407d6c1a6fcc907f6124c37e977ba4966205014787a32a27579da6e47c3b1bd210b68cc1c7764d904c8aa55fb4efa6945586f9b4fae742c63ed026a4559da07d + languageName: node + linkType: hard + "type-fest@npm:^0.13.1": version: 0.13.1 resolution: "type-fest@npm:0.13.1" @@ -8322,6 +8726,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.15.1": + version: 0.15.1 + resolution: "type-fest@npm:0.15.1" + checksum: a1a0cdbd7f802d9784324f185df055739e97424ecb60914e9025574a4bc07e4a063c152e4510ebf5989de8a263220de1f6b5cf1b05f0b333dbd5b21d9b4a271b + languageName: node + linkType: hard + "type-fest@npm:^0.18.0": version: 0.18.1 resolution: "type-fest@npm:0.18.1" @@ -8596,6 +9007,27 @@ __metadata: languageName: node linkType: hard +"widest-line@npm:^3.1.0": + version: 3.1.0 + resolution: "widest-line@npm:3.1.0" + dependencies: + string-width: ^4.0.0 + checksum: 03db6c9d0af9329c37d74378ff1d91972b12553c7d72a6f4e8525fe61563fa7adb0b9d6e8d546b7e059688712ea874edd5ded475999abdeedf708de9849310e0 + languageName: node + linkType: hard + +"window-size@npm:^1.1.1": + version: 1.1.1 + resolution: "window-size@npm:1.1.1" + dependencies: + define-property: ^1.0.0 + is-number: ^3.0.0 + bin: + window-size: cli.js + checksum: 4ab5ea0d5f224d6859eb0747049cbebf43ed3ba9fe535a2cbd2024b1f9cfd3c9021db33a3b20ee42ef8b5c5a036ddca46271bf1fc471aef7fc4cb4a98bfb2e67 + languageName: node + linkType: hard + "winston-daily-rotate-file@npm:^4.7.1": version: 4.7.1 resolution: "winston-daily-rotate-file@npm:4.7.1" @@ -8647,6 +9079,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^6.2.0": + version: 6.2.0 + resolution: "wrap-ansi@npm:6.2.0" + dependencies: + ansi-styles: ^4.0.0 + string-width: ^4.1.0 + strip-ansi: ^6.0.0 + checksum: 6cd96a410161ff617b63581a08376f0cb9162375adeb7956e10c8cd397821f7eb2a6de24eb22a0b28401300bf228c86e50617cd568209b5f6775b93c97d2fe3a + languageName: node + linkType: hard + "wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -8675,6 +9118,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^7, ws@npm:^7.5.5": + version: 7.5.9 + resolution: "ws@npm:7.5.9" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: c3c100a181b731f40b7f2fddf004aa023f79d64f489706a28bc23ff88e87f6a64b3c6651fbec3a84a53960b75159574d7a7385709847a62ddb7ad6af76f49138 + languageName: node + linkType: hard + "xml2js@npm:^0.4.23": version: 0.4.23 resolution: "xml2js@npm:0.4.23" @@ -8815,6 +9273,15 @@ __metadata: languageName: node linkType: hard +"yoga-layout-prebuilt@npm:^1.9.6": + version: 1.10.0 + resolution: "yoga-layout-prebuilt@npm:1.10.0" + dependencies: + "@types/yoga-layout": 1.9.2 + checksum: 6954c7c7b04c585a1c974391bea4734611adb85702b5e9131549a1d3dc5b94e69bcfea34121cdaeb5e702663bf290fcce5374910128e54d1031503a57c062865 + languageName: node + linkType: hard + "z-schema@npm:~5.0.2": version: 5.0.3 resolution: "z-schema@npm:5.0.3" From 9d03726d47491874c3fb8894b10327fe7305650c Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 1 Mar 2023 15:17:28 +0100 Subject: [PATCH 02/27] feat: implement CLI for local testing --- .vscode/typescriptreact.code-snippets | 14 ++ ...ayout-prebuilt-npm-1.10.0-855b15449f.patch | 13 ++ package.json | 3 +- packages/cli/maintenance/watch.ts | 131 ++++++++++++ packages/cli/package.json | 13 +- packages/cli/src/cli.tsx | 109 ++++++++-- packages/cli/src/components/Frame.tsx | 193 ++++++++++++++++++ packages/cli/src/components/HotkeyLabel.tsx | 51 +++++ packages/cli/src/components/Logo.tsx | 16 ++ packages/cli/src/components/confirmExit.tsx | 28 +-- packages/cli/src/components/header.tsx | 35 ---- packages/cli/src/components/mainMenu.tsx | 32 --- packages/cli/src/components/setUSBPath.tsx | 4 + yarn.lock | 178 ++++------------ 14 files changed, 567 insertions(+), 253 deletions(-) create mode 100644 .vscode/typescriptreact.code-snippets create mode 100644 .yarn/patches/yoga-layout-prebuilt-npm-1.10.0-855b15449f.patch create mode 100644 packages/cli/maintenance/watch.ts create mode 100644 packages/cli/src/components/Frame.tsx create mode 100644 packages/cli/src/components/HotkeyLabel.tsx create mode 100644 packages/cli/src/components/Logo.tsx delete mode 100644 packages/cli/src/components/header.tsx delete mode 100644 packages/cli/src/components/mainMenu.tsx diff --git a/.vscode/typescriptreact.code-snippets b/.vscode/typescriptreact.code-snippets new file mode 100644 index 000000000000..562a43152834 --- /dev/null +++ b/.vscode/typescriptreact.code-snippets @@ -0,0 +1,14 @@ +{ + "React Functional Component": { + "prefix": "rfc", + "body": [ + "export interface ${0:$TM_FILENAME_BASE}Props {", + "\t// TODO:", + "}", + "", + "export const ${0:$TM_FILENAME_BASE}: React.FC<${0:$TM_FILENAME_BASE}Props> = (props) => {", + "\treturn
TODO
;", + "};" + ] + } +} diff --git a/.yarn/patches/yoga-layout-prebuilt-npm-1.10.0-855b15449f.patch b/.yarn/patches/yoga-layout-prebuilt-npm-1.10.0-855b15449f.patch new file mode 100644 index 000000000000..8c23c940deb2 --- /dev/null +++ b/.yarn/patches/yoga-layout-prebuilt-npm-1.10.0-855b15449f.patch @@ -0,0 +1,13 @@ +diff --git a/yoga-layout/build/Release/nbind.js b/yoga-layout/build/Release/nbind.js +index 0853309f9e0bd49d3e585db266442210d9538d1e..73e3fc4e416b3e3a177107f92a0fd10979177e16 100644 +--- a/yoga-layout/build/Release/nbind.js ++++ b/yoga-layout/build/Release/nbind.js +@@ -1143,7 +1143,7 @@ + } + }_nbind.addMethod = addMethod;function throwError(message) { + throw new Error(message); +- }_nbind.throwError = throwError;_nbind.bigEndian = false;_a = _typeModule(_typeModule), _nbind.Type = _a.Type, _nbind.makeType = _a.makeType, _nbind.getComplexType = _a.getComplexType, _nbind.structureList = _a.structureList;var BindType = function (_super) { ++ }_nbind.throwError = throwError;_nbind.bigEndian = false;var _a = _typeModule(_typeModule); _nbind.Type = _a.Type, _nbind.makeType = _a.makeType, _nbind.getComplexType = _a.getComplexType, _nbind.structureList = _a.structureList;var BindType = function (_super) { + __extends(BindType, _super);function BindType() { + var _this = _super !== null && _super.apply(this, arguments) || this;_this.heap = HEAPU32;_this.ptrSize = 4;return _this; + }BindType.prototype.needsWireRead = function (policyTbl) { diff --git a/package.json b/package.json index df095d776edc..7cecfa226728 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,8 @@ }, "resolutions": { "minimist": "^1.2.6", - "colors": "1.4.0" + "colors": "1.4.0", + "yoga-layout-prebuilt@^1.9.6": "patch:yoga-layout-prebuilt@npm%3A1.10.0#./.yarn/patches/yoga-layout-prebuilt-npm-1.10.0-855b15449f.patch" }, "scripts": { "w": "yarn ts maintenance/watch.ts", diff --git a/packages/cli/maintenance/watch.ts b/packages/cli/maintenance/watch.ts new file mode 100644 index 000000000000..0d7a6b682a2e --- /dev/null +++ b/packages/cli/maintenance/watch.ts @@ -0,0 +1,131 @@ +import PQueue from "@esm2cjs/p-queue"; +import { gray } from "ansi-colors"; +import cp from "child_process"; +import chokidar from "chokidar"; +import crypto from "crypto"; +import fs from "fs"; + +// @ts-expect-error This is available in Node 16, but we're targeting Node 14 +import stream from "stream/promises"; + +const ABORT_RUNNING = true; + +const task = process.argv[2]; +const taskArgs = process.argv.slice(3); + +const changes = new Set(); +const hashes = new Map>(); +let prevHash: Buffer | undefined; +const hashQueue = new PQueue({ concurrency: 10 }); + +let ready = false; +let child: cp.ChildProcess | undefined; + +const watcher = chokidar.watch(["build/**/*.js"], { + cwd: process.cwd(), + atomic: true, +}); + +async function exec(): Promise { + if (!ABORT_RUNNING && child) return; + + // before running the task, wait for all the hashes to be calculated + const entries = await Promise.all( + [...hashes.entries()].map( + async ([filename, hash]) => [filename, await hash] as const, + ), + ); + entries.sort(([fileA], [fileB]) => fileA.localeCompare(fileB)); + + if (changes.size > 0) { + // console.log("Files updated:"); + // for (const change of changes) { + // console.log(change); + // } + // console.log(); + changes.clear(); + } + + // Then compute a combined hash of all filenames and their hashes + const hasher = crypto.createHash("sha256"); + for (const [filename, hash] of entries) { + console.time("hashing " + filename); + hasher.update(filename); + hasher.update(hash); + console.timeEnd("hashing " + filename); + } + const totalHash = hasher.digest(); + + // And only run the task if the hash has changed + if (prevHash?.equals(totalHash)) { + // console.log("No changes detected, skipping task..."); + // console.log(); + return; + } + + if (child) child.kill("SIGTERM"); + + child = cp.spawn("yarn", [task, ...taskArgs], { + stdio: "inherit", + windowsHide: true, + }); + child.on("exit", (code) => { + child = undefined; + console.clear(); + console.log(gray(`Waiting for file changes... Press Ctrl+C to exit.`)); + }); +} + +const debouncedExec = debounce(exec, 250); + +watcher + .on("add", (filename) => { + // Whenever a file is added, hash it and call exec when done + hashes.set(filename, hashFile(filename)); + if (ready) { + changes.add("+ " + filename); + debouncedExec(); + } + }) + .on("change", (filename) => { + // Whenever a file is added, hash it and call exec when done + hashes.set(filename, hashFile(filename)); + if (ready) { + changes.add("~ " + filename); + debouncedExec(); + } + }) + .on("unlink", (filename) => { + // Whenever a file is removed, remove its hash and call exec + hashes.delete(filename); + if (ready) { + changes.add("- " + filename); + debouncedExec(); + } + }) + .on("ready", () => { + ready = true; + void exec(); + }); + +process.on("SIGINT", () => { + child?.kill("SIGTERM"); + void watcher.close(); +}); + +function hashFile(filename: string): Promise { + return hashQueue.add(async () => { + const reader = fs.createReadStream(filename); + const hasher = crypto.createHash("sha256"); + await stream.pipeline(reader, hasher); + return hasher.digest(); + }); +} + +function debounce(fn: () => void, timeout: number) { + let timeoutId: NodeJS.Timeout; + return () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(fn, timeout); + }; +} diff --git a/packages/cli/package.json b/packages/cli/package.json index d1b846142b5c..9c700dad929a 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -32,25 +32,32 @@ "node": ">=14.13.0" }, "scripts": { - "build": "tsc -b tsconfig.build.json", + "ts": "node -r esbuild-register", + "w": "yarn ts maintenance/watch.ts", + "build": "esbuild src/cli.tsx --outfile=build/cli.js --platform=node --target=node16 --format=cjs --bundle --external:zwave-js", "clean": "del-cli build/ \"*.tsbuildinfo\"", + "dev": "yarn w node build/cli.js", "lint:ts": "eslint --ext .ts --rule \"prettier/prettier: off\" \"src/**/*.ts\"", "lint:ts:fix": "yarn run lint:ts --fix", "lint:prettier": "prettier -c \"src/**/*.ts\"", "lint:prettier:fix": "yarn run lint:prettier -w" }, "dependencies": { + "@zwave-js/shared": "workspace:*", "zwave-js": "workspace:*" }, "devDependencies": { - "@types/ink-big-text": "^1.2.1", + "@esm2cjs/p-queue": "^7.3.0", "@types/ink-text-input": "^2.0.2", "@types/node": "^14.18.36", "@types/react": "^18", + "ansi-colors": "^4.1.3", "del-cli": "^5.0.0", + "esbuild": "0.15.7", + "esbuild-register": "^3.3.3", "ink": "^3.2.0", - "ink-big-text": "^1.2.0", "ink-text-input": "^4.0.3", + "ink-use-stdout-dimensions": "^1.0.5", "prettier": "^2.8.1", "react": "^18.2.0", "typescript": "4.9.4" diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 56ce6cfe2d99..22f3b6b31d6b 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,51 +1,118 @@ -import { Box, render, useApp } from "ink"; +import { Box, render, Text, useApp } from "ink"; +import useStdoutDimensions from "ink-use-stdout-dimensions"; import { useState } from "react"; +import { Driver, libVersion } from "zwave-js"; import { ConfirmExit } from "./components/confirmExit"; -import { Header } from "./components/header"; -import { MainMenu } from "./components/mainMenu"; +import { Frame } from "./components/Frame"; +import { HotkeyLabel } from "./components/HotkeyLabel"; +import { Logo } from "./components/Logo"; import { SetUSBPath } from "./components/setUSBPath"; enum CLIPage { + Idle, SetUSBPath, - MainMenu, StartingDriver, ConfirmExit, } const CLI: React.FC = () => { const { exit } = useApp(); + const [columns, rows] = useStdoutDimensions(); - const [cliPage, setCLIPage] = useState(CLIPage.MainMenu); - const [usbPath, setUSBPath] = useState(undefined); + const [cliPage, setCLIPage] = useState(CLIPage.Idle); + const [usbPath, setUSBPath] = useState(); + const [driver, setDriver] = useState(); + + const bottomMenu = + cliPage === CLIPage.Idle + ? { + left: [ + !!usbPath && ( + { + setCLIPage(CLIPage.StartingDriver); + }} + /> + ), + ], + right: [ + { + setCLIPage(CLIPage.SetUSBPath); + }} + />, + + setCLIPage(CLIPage.ConfirmExit)} + />, + ], + } + : undefined; return ( - -
+ + v{libVersion} + + ), + ], + right: [ + + USB Path: {usbPath || "(none)"} + , + ], + }} + bottomLabels={bottomMenu} + height={Math.min(30, rows)} + paddingY={1} + justifyContent="center" + > + {cliPage === CLIPage.Idle && ( + + + + + Select a USB path in the options, then start the driver. + + + )} + {cliPage === CLIPage.SetUSBPath && ( setCLIPage(CLIPage.MainMenu)} + onCancel={() => setCLIPage(CLIPage.Idle)} onSubmit={(path) => { setUSBPath(path); - setCLIPage(CLIPage.MainMenu); + setCLIPage(CLIPage.Idle); }} /> )} - {cliPage === CLIPage.MainMenu && ( - setCLIPage(CLIPage.ConfirmExit)} - onSetUSBPath={() => setCLIPage(CLIPage.SetUSBPath)} - /> - )} + {cliPage === CLIPage.ConfirmExit && ( setCLIPage(CLIPage.MainMenu)} - onExit={() => exit()} + onCancel={() => setCLIPage(CLIPage.Idle)} + onExit={exit} /> )} - + ); }; -const { waitUntilExit } = render(); -void waitUntilExit(); +// console.clear(); +render(); +// diff --git a/packages/cli/src/components/Frame.tsx b/packages/cli/src/components/Frame.tsx new file mode 100644 index 000000000000..c926b53426a5 --- /dev/null +++ b/packages/cli/src/components/Frame.tsx @@ -0,0 +1,193 @@ +import { pick } from "@zwave-js/shared/safe"; +import { Box, Text } from "ink"; +import type { ComponentPropsWithoutRef } from "react"; + +type BoxProps = ComponentPropsWithoutRef; + +// type BoxProps = { +// readonly position?: "absolute" | "relative" | undefined; +// readonly marginLeft?: number | undefined; +// readonly marginRight?: number | undefined; +// readonly marginTop?: number | undefined; +// readonly marginBottom?: number | undefined; +// readonly paddingLeft?: number | undefined; +// readonly paddingRight?: number | undefined; +// readonly paddingTop?: number | undefined; +// readonly paddingBottom?: number | undefined; +// readonly flexGrow?: number | undefined; +// readonly flexShrink?: number | undefined; +// readonly flexDirection?: "row" | "column" | "row-reverse" | "column-reverse" | undefined; +// readonly flexBasis?: string | number | undefined; +// readonly alignItems?: "flex-start" | "center" | "flex-end" | "stretch" | undefined; +// readonly alignSelf?: "flex-start" | "center" | "flex-end" | "auto" | undefined; +// readonly justifyContent?: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | undefined; +// readonly width?: string | number | undefined; +// readonly height?: string | number | undefined; +// readonly minWidth?: string | number | undefined; +// readonly minHeight?: string | number | undefined; +// readonly display?: "flex" | "none" | undefined; +// readonly borderStyle?: keyof cliBoxes.Boxes | undefined; +// readonly borderColor?: LiteralUnion | undefined; +// readonly margin?: number | undefined; +// readonly marginX?: number | undefined; +// readonly marginY?: number | undefined; +// readonly padding?: number | undefined; +// readonly paddingX?: number | undefined; +// readonly paddingY?: number | undefined; +// children?: React.ReactNode; +// key?: React.Key | null | undefined; +// } + +export interface FrameLabelGroupProps { + left?: (React.ReactNode | undefined | false)[]; + center?: (React.ReactNode | undefined | false)[]; + right?: (React.ReactNode | undefined | false)[]; +} + +const FrameLabels: React.FC<{ + labels: React.ReactNode[]; +}> = ({ labels }) => { + return ( + <> + {labels.map((label, i) => { + return ( + 0 ? 1 : 0} + > + {label} + + ); + })} + + ); +}; + +const FrameLabelGroup: React.FC = (props) => { + return ( + <> + + {props.left?.some(Boolean) && ( + + )} + + + {props.center?.some(Boolean) && ( + + )} + + + {props.right?.some(Boolean) && ( + + )} + + + ); +}; + +export interface FrameProps extends BoxProps { + topLabels?: FrameLabelGroupProps | false; + bottomLabels?: FrameLabelGroupProps | false; +} + +export const Frame: React.FC = (props) => { + const { topLabels, bottomLabels, ...boxProps } = props; + + const hasTopLabels = topLabels && Object.values(topLabels).some(Boolean); + const hasBottomLabels = + bottomLabels && Object.values(bottomLabels).some(Boolean); + + // Apply some defaults + boxProps.borderStyle ??= "round"; + boxProps.paddingX ??= 1; + + if (hasTopLabels || hasBottomLabels) { + const outerBoxProps = pick(boxProps, [ + "position", + "marginLeft", + "marginRight", + "marginTop", + "marginBottom", + "margin", + "marginX", + "marginY", + "flexGrow", + "flexShrink", + "flexBasis", + "alignSelf", + "width", + "height", + "minWidth", + "minHeight", + "borderStyle", + "borderColor", + "key", + ]); + + const innerBoxProps = pick(boxProps, [ + "paddingLeft", + "paddingRight", + "paddingTop", + "paddingBottom", + "flexDirection", + "alignItems", + "justifyContent", + "padding", + "paddingX", + "paddingY", + ]); + + return ( + + + + + + {boxProps.children} + + + + + + ); + } else { + return ; + } +}; diff --git a/packages/cli/src/components/HotkeyLabel.tsx b/packages/cli/src/components/HotkeyLabel.tsx new file mode 100644 index 000000000000..de054d0224db --- /dev/null +++ b/packages/cli/src/components/HotkeyLabel.tsx @@ -0,0 +1,51 @@ +import { Text, useInput } from "ink"; +import type { ComponentPropsWithoutRef } from "react"; + +type TextProps = ComponentPropsWithoutRef; + +export interface HotkeyLabelProps extends TextProps { + label: string; + hotkey?: string; + onPress?: () => void; +} + +export const HotkeyLabel: React.FC = (props) => { + const { label, hotkey, ...textProps } = props; + // Apply some default text props + textProps.color ??= "green"; + textProps.bold ??= true; + + const { color, children, ...rest } = textProps; + + useInput((input, key) => { + if (hotkey && input === hotkey) { + props.onPress?.(); + } + }); + + if (!hotkey) { + return {label}; + } else if (hotkey.length !== 1) { + throw new Error("Hotkey must be a single character"); + } + + const hotkeyIndex = label.toLowerCase().indexOf(hotkey.toLowerCase()); + + if (hotkeyIndex === -1) { + return ( + + {label} ({hotkey}) + + ); + } else { + return ( + + {label.slice(0, hotkeyIndex)} + + {label.slice(hotkeyIndex, hotkeyIndex + 1)} + + {label.slice(hotkeyIndex + 1)} + + ); + } +}; diff --git a/packages/cli/src/components/Logo.tsx b/packages/cli/src/components/Logo.tsx new file mode 100644 index 000000000000..1eed5b763114 --- /dev/null +++ b/packages/cli/src/components/Logo.tsx @@ -0,0 +1,16 @@ +import { Text } from "ink"; + +export const Logo: React.FC = () => { + return ( + + {` +███████╗ ██╗ ██╗ █████╗ ██╗ ██╗ ███████╗ ██╗ ███████╗ +╚══███╔╝ ██║ ██║ ██╔══██╗ ██║ ██║ ██╔════╝ ██║ ██╔════╝ + ███╔╝ ██║ █╗ ██║ ███████║ ██║ ██║ █████╗ █████╗ ██║ ███████╗ + ███╔╝ ██║███╗██║ ██╔══██║ ╚██╗ ██╔╝ ██╔══╝ ╚════╝ ██ ██║ ╚════██║ +███████╗ ╚███╔███╔╝ ██║ ██║ ╚████╔╝ ███████╗ ╚█████╔╝ ███████║ +╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝ ╚════╝ ╚══════╝ +`.trim()} + + ); +}; diff --git a/packages/cli/src/components/confirmExit.tsx b/packages/cli/src/components/confirmExit.tsx index 39f333422c90..713071d90734 100644 --- a/packages/cli/src/components/confirmExit.tsx +++ b/packages/cli/src/components/confirmExit.tsx @@ -1,5 +1,4 @@ -import { useInput } from "ink"; -import { Menu } from "./menu"; +import { Box, Text, useInput } from "ink"; export interface ConfirmExitProps { onExit: () => void; @@ -13,23 +12,12 @@ export const ConfirmExit: React.FC = (props) => { }); return ( - + + Are you sure you want to exit? + + Press RETURN to exit, or{" "} + ESCAPE to cancel. + + ); }; diff --git a/packages/cli/src/components/header.tsx b/packages/cli/src/components/header.tsx deleted file mode 100644 index 163633019298..000000000000 --- a/packages/cli/src/components/header.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Box, Text } from "ink"; -import BigText from "ink-big-text"; - -export interface HeaderProps { - usbPath?: string; -} - -export const Header: React.FC = (props) => { - return ( - - - - {" "} - USB Path:{" "} - {props.usbPath ? ( - {props.usbPath} - ) : ( - "(none)" - )} - - - ); -}; diff --git a/packages/cli/src/components/mainMenu.tsx b/packages/cli/src/components/mainMenu.tsx deleted file mode 100644 index 0ede02b9dc4c..000000000000 --- a/packages/cli/src/components/mainMenu.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Menu } from "./menu"; - -export interface MainMenuProps { - onSetUSBPath: () => void; - onExit: () => void; -} - -export const MainMenu: React.FC = (props) => { - return ( - - ); -}; diff --git a/packages/cli/src/components/setUSBPath.tsx b/packages/cli/src/components/setUSBPath.tsx index 1336f4097d42..6d34b3923852 100644 --- a/packages/cli/src/components/setUSBPath.tsx +++ b/packages/cli/src/components/setUSBPath.tsx @@ -21,6 +21,10 @@ export const SetUSBPath: React.FC = (props) => { onSubmit={props.onSubmit} > + + ENTER to confirm, ESCAPE to + cancel + ); }; diff --git a/yarn.lock b/yarn.lock index a2490818af7a..227f9cb59d53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1379,15 +1379,6 @@ __metadata: languageName: node linkType: hard -"@types/ink-big-text@npm:^1.2.1": - version: 1.2.1 - resolution: "@types/ink-big-text@npm:1.2.1" - dependencies: - "@types/react": "*" - checksum: 33f0974dab7f9988027db6e7e26610d9d3f007aa64df5fea5fbb7441541222e9d1292edb4772b710a0ab2cf40801cbac70246cedce1dde209314f7e367684b56 - languageName: node - linkType: hard - "@types/ink-text-input@npm:^2.0.2": version: 2.0.2 resolution: "@types/ink-text-input@npm:2.0.2" @@ -1521,7 +1512,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:*, @types/react@npm:^18": +"@types/react@npm:^18": version: 18.0.28 resolution: "@types/react@npm:18.0.28" dependencies: @@ -1794,14 +1785,18 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/cli@workspace:packages/cli" dependencies: - "@types/ink-big-text": ^1.2.1 + "@esm2cjs/p-queue": ^7.3.0 "@types/ink-text-input": ^2.0.2 "@types/node": ^14.18.36 "@types/react": ^18 + "@zwave-js/shared": "workspace:*" + ansi-colors: ^4.1.3 del-cli: ^5.0.0 + esbuild: 0.15.7 + esbuild-register: ^3.3.3 ink: ^3.2.0 - ink-big-text: ^1.2.0 ink-text-input: ^4.0.3 + ink-use-stdout-dimensions: ^1.0.5 prettier: ^2.8.1 react: ^18.2.0 typescript: 4.9.4 @@ -2770,18 +2765,6 @@ __metadata: languageName: node linkType: hard -"cfonts@npm:^2.8.6": - version: 2.10.1 - resolution: "cfonts@npm:2.10.1" - dependencies: - chalk: ^4 - window-size: ^1.1.1 - bin: - cfonts: bin/index.js - checksum: 2250d0878609c5cd39eced4212b708257a515f17fed83943f01423f6e11c091b77e30d56254b356f74bbb0e3f7d5f6782088d6e7e9985f3c9d8ec4bb34a15ad2 - languageName: node - linkType: hard - "chalk@npm:^2.0.0, chalk@npm:^2.4.1, chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -2793,23 +2776,23 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^4, chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": - version: 4.1.2 - resolution: "chalk@npm:4.1.2" +"chalk@npm:^4.0.0": + version: 4.1.1 + resolution: "chalk@npm:4.1.1" dependencies: ansi-styles: ^4.1.0 supports-color: ^7.1.0 - checksum: fe75c9d5c76a7a98d45495b91b2172fa3b7a09e0cc9370e5c8feb1c567b85c4288e2b3fded7cfdd7359ac28d6b3844feb8b82b8686842e93d23c827c417e83fc + checksum: 036e973e665ba1a32c975e291d5f3d549bceeb7b1b983320d4598fb75d70fe20c5db5d62971ec0fe76cdbce83985a00ee42372416abfc3a5584465005a7855ed languageName: node linkType: hard -"chalk@npm:^4.0.0": - version: 4.1.1 - resolution: "chalk@npm:4.1.1" +"chalk@npm:^4.1.0, chalk@npm:^4.1.1, chalk@npm:^4.1.2": + version: 4.1.2 + resolution: "chalk@npm:4.1.2" dependencies: ansi-styles: ^4.1.0 supports-color: ^7.1.0 - checksum: 036e973e665ba1a32c975e291d5f3d549bceeb7b1b983320d4598fb75d70fe20c5db5d62971ec0fe76cdbce83985a00ee42372416abfc3a5584465005a7855ed + checksum: fe75c9d5c76a7a98d45495b91b2172fa3b7a09e0cc9370e5c8feb1c567b85c4288e2b3fded7cfdd7359ac28d6b3844feb8b82b8686842e93d23c827c417e83fc languageName: node linkType: hard @@ -3593,15 +3576,6 @@ __metadata: languageName: node linkType: hard -"define-property@npm:^1.0.0": - version: 1.0.0 - resolution: "define-property@npm:1.0.0" - dependencies: - is-descriptor: ^1.0.0 - checksum: 5fbed11dace44dd22914035ba9ae83ad06008532ca814d7936a53a09e897838acdad5b108dd0688cc8d2a7cf0681acbe00ee4136cf36743f680d10517379350a - languageName: node - linkType: hard - "del-cli@npm:^5.0.0": version: 5.0.0 resolution: "del-cli@npm:5.0.0" @@ -5226,19 +5200,6 @@ __metadata: languageName: node linkType: hard -"ink-big-text@npm:^1.2.0": - version: 1.2.0 - resolution: "ink-big-text@npm:1.2.0" - dependencies: - cfonts: ^2.8.6 - prop-types: ^15.7.2 - peerDependencies: - ink: ">=2.0.0" - react: ">=16.8.0" - checksum: b65481453cc13f726bdecf6378da30d55abb676e5eea5f6f38059d3cf7fa7e474c35ff10b760deecded246e9aabb68d22154c0e760860e2d05ca2d2ae60e5052 - languageName: node - linkType: hard - "ink-text-input@npm:^4.0.3": version: 4.0.3 resolution: "ink-text-input@npm:4.0.3" @@ -5252,6 +5213,16 @@ __metadata: languageName: node linkType: hard +"ink-use-stdout-dimensions@npm:^1.0.5": + version: 1.0.5 + resolution: "ink-use-stdout-dimensions@npm:1.0.5" + peerDependencies: + ink: ">=2.0.0" + react: ">=16.0.0" + checksum: 71fb471ed6195460a5562f8cf0b046c888f837a29c022f48f180df3fd78b8b37dc736c1b8a383552930ae7a0c754b0b1f85fe234e8aa71b227c5b8c82de22472 + languageName: node + linkType: hard + "ink@npm:^3.2.0": version: 3.2.0 resolution: "ink@npm:3.2.0" @@ -5354,15 +5325,6 @@ __metadata: languageName: node linkType: hard -"is-accessor-descriptor@npm:^1.0.0": - version: 1.0.0 - resolution: "is-accessor-descriptor@npm:1.0.0" - dependencies: - kind-of: ^6.0.0 - checksum: 8e475968e9b22f9849343c25854fa24492dbe8ba0dea1a818978f9f1b887339190b022c9300d08c47fe36f1b913d70ce8cbaca00369c55a56705fdb7caed37fe - languageName: node - linkType: hard - "is-arrayish@npm:^0.2.1": version: 0.2.1 resolution: "is-arrayish@npm:0.2.1" @@ -5386,13 +5348,6 @@ __metadata: languageName: node linkType: hard -"is-buffer@npm:^1.1.5": - version: 1.1.6 - resolution: "is-buffer@npm:1.1.6" - checksum: 4a186d995d8bbf9153b4bd9ff9fd04ae75068fe695d29025d25e592d9488911eeece84eefbd8fa41b8ddcc0711058a71d4c466dcf6f1f6e1d83830052d8ca707 - languageName: node - linkType: hard - "is-ci@npm:^2.0.0": version: 2.0.0 resolution: "is-ci@npm:2.0.0" @@ -5440,26 +5395,6 @@ __metadata: languageName: node linkType: hard -"is-data-descriptor@npm:^1.0.0": - version: 1.0.0 - resolution: "is-data-descriptor@npm:1.0.0" - dependencies: - kind-of: ^6.0.0 - checksum: e705e6816241c013b05a65dc452244ee378d1c3e3842bd140beabe6e12c0d700ef23c91803f971aa7b091fb0573c5da8963af34a2b573337d87bc3e1f53a4e6d - languageName: node - linkType: hard - -"is-descriptor@npm:^1.0.0": - version: 1.0.2 - resolution: "is-descriptor@npm:1.0.2" - dependencies: - is-accessor-descriptor: ^1.0.0 - is-data-descriptor: ^1.0.0 - kind-of: ^6.0.2 - checksum: 2ed623560bee035fb67b23e32ce885700bef8abe3fbf8c909907d86507b91a2c89a9d3a4d835a4d7334dd5db0237a0aeae9ca109c1e4ef1c0e7b577c0846ab5a - languageName: node - linkType: hard - "is-docker@npm:^2.0.0": version: 2.2.1 resolution: "is-docker@npm:2.2.1" @@ -5545,15 +5480,6 @@ __metadata: languageName: node linkType: hard -"is-number@npm:^3.0.0": - version: 3.0.0 - resolution: "is-number@npm:3.0.0" - dependencies: - kind-of: ^3.0.2 - checksum: 0c62bf8e9d72c4dd203a74d8cfc751c746e75513380fef420cda8237e619a988ee43e678ddb23c87ac24d91ac0fe9f22e4ffb1301a50310c697e9d73ca3994e9 - languageName: node - linkType: hard - "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -5869,16 +5795,7 @@ __metadata: languageName: node linkType: hard -"kind-of@npm:^3.0.2": - version: 3.2.2 - resolution: "kind-of@npm:3.2.2" - dependencies: - is-buffer: ^1.1.5 - checksum: e898df8ca2f31038f27d24f0b8080da7be274f986bc6ed176f37c77c454d76627619e1681f6f9d2e8d2fd7557a18ecc419a6bb54e422abcbb8da8f1a75e4b386 - languageName: node - linkType: hard - -"kind-of@npm:^6.0.0, kind-of@npm:^6.0.2, kind-of@npm:^6.0.3": +"kind-of@npm:^6.0.2, kind-of@npm:^6.0.3": version: 6.0.3 resolution: "kind-of@npm:6.0.3" checksum: 3ab01e7b1d440b22fe4c31f23d8d38b4d9b91d9f291df683476576493d5dfd2e03848a8b05813dd0c3f0e835bc63f433007ddeceb71f05cb25c45ae1b19c6d3b @@ -6052,7 +5969,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.1.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -7226,17 +7143,6 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.7.2": - version: 15.8.1 - resolution: "prop-types@npm:15.8.1" - dependencies: - loose-envify: ^1.4.0 - object-assign: ^4.1.1 - react-is: ^16.13.1 - checksum: c056d3f1c057cb7ff8344c645450e14f088a915d078dcda795041765047fa080d38e5d626560ccaac94a4e16e3aa15f3557c1a9a8d1174530955e992c675e459 - languageName: node - linkType: hard - "proper-lockfile@npm:^4.1.2": version: 4.1.2 resolution: "proper-lockfile@npm:4.1.2" @@ -7314,13 +7220,6 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1": - version: 16.13.1 - resolution: "react-is@npm:16.13.1" - checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f - languageName: node - linkType: hard - "react-reconciler@npm:^0.26.2": version: 0.26.2 resolution: "react-reconciler@npm:0.26.2" @@ -9016,18 +8915,6 @@ __metadata: languageName: node linkType: hard -"window-size@npm:^1.1.1": - version: 1.1.1 - resolution: "window-size@npm:1.1.1" - dependencies: - define-property: ^1.0.0 - is-number: ^3.0.0 - bin: - window-size: cli.js - checksum: 4ab5ea0d5f224d6859eb0747049cbebf43ed3ba9fe535a2cbd2024b1f9cfd3c9021db33a3b20ee42ef8b5c5a036ddca46271bf1fc471aef7fc4cb4a98bfb2e67 - languageName: node - linkType: hard - "winston-daily-rotate-file@npm:^4.7.1": version: 4.7.1 resolution: "winston-daily-rotate-file@npm:4.7.1" @@ -9273,7 +9160,7 @@ __metadata: languageName: node linkType: hard -"yoga-layout-prebuilt@npm:^1.9.6": +"yoga-layout-prebuilt@npm:1.10.0": version: 1.10.0 resolution: "yoga-layout-prebuilt@npm:1.10.0" dependencies: @@ -9282,6 +9169,15 @@ __metadata: languageName: node linkType: hard +"yoga-layout-prebuilt@patch:yoga-layout-prebuilt@npm%3A1.10.0#./.yarn/patches/yoga-layout-prebuilt-npm-1.10.0-855b15449f.patch::locator=%40zwave-js%2Frepo%40workspace%3A.": + version: 1.10.0 + resolution: "yoga-layout-prebuilt@patch:yoga-layout-prebuilt@npm%3A1.10.0#./.yarn/patches/yoga-layout-prebuilt-npm-1.10.0-855b15449f.patch::version=1.10.0&hash=e91576&locator=%40zwave-js%2Frepo%40workspace%3A." + dependencies: + "@types/yoga-layout": 1.9.2 + checksum: 821f9b799915d25c9ad1086855e30312b1328d724baa377e2b2dffc994c54900a486b6a1bd2e41d5dc644000a1028b90ad530e2d60f56405423d3bfbc0c02a81 + languageName: node + linkType: hard + "z-schema@npm:~5.0.2": version: 5.0.3 resolution: "z-schema@npm:5.0.3" From 016843dadcf5f5a8a419e8bbf4ea3d9be8eed56b Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 1 Mar 2023 22:19:08 +0100 Subject: [PATCH 03/27] feat: better menu and logs --- .vscode/launch.json | 17 +- package.json | 1 + packages/cli/maintenance/watch.ts | 131 --------- packages/cli/package.json | 10 +- packages/cli/src/cli.tsx | 258 +++++++++++++----- packages/cli/src/components/Center.tsx | 15 + .../{confirmExit.tsx => ConfirmExit.tsx} | 0 packages/cli/src/components/Log.tsx | 51 ++++ packages/cli/src/components/Logo.tsx | 12 +- packages/cli/src/components/MainMenu.tsx | 9 + packages/cli/src/components/ModalMessage.tsx | 41 +++ .../cli/src/components/StartingDriver.tsx | 19 ++ packages/cli/src/components/VDivider.tsx | 27 ++ packages/cli/src/lib/driver.ts | 47 ++++ packages/cli/src/lib/logging.ts | 61 +++++ yarn.lock | 43 ++- 16 files changed, 527 insertions(+), 215 deletions(-) delete mode 100644 packages/cli/maintenance/watch.ts create mode 100644 packages/cli/src/components/Center.tsx rename packages/cli/src/components/{confirmExit.tsx => ConfirmExit.tsx} (100%) create mode 100644 packages/cli/src/components/Log.tsx create mode 100644 packages/cli/src/components/MainMenu.tsx create mode 100644 packages/cli/src/components/ModalMessage.tsx create mode 100644 packages/cli/src/components/StartingDriver.tsx create mode 100644 packages/cli/src/components/VDivider.tsx create mode 100644 packages/cli/src/lib/driver.ts create mode 100644 packages/cli/src/lib/logging.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index 43d5d3c64ed9..ca240fc5261f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,11 +4,25 @@ // Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Start CLI", + "request": "launch", + "runtimeExecutable": "yarn", + "runtimeArgs": [ + "node", + "--async-stack-traces", + "${workspaceFolder}/packages/cli/build/cli.js" + ], + "skipFiles": ["/**"], + "type": "node", + "console": "integratedTerminal", + "sourceMaps": true, + "preLaunchTask": "npm: build:cli" + }, { "type": "node", "request": "launch", "name": "Debug locally", - "port": 9229, "runtimeExecutable": "yarn", "runtimeArgs": [ "node", @@ -26,6 +40,7 @@ "sourceMaps": true, "preLaunchTask": "npm: build" }, + { "type": "node", "request": "launch", diff --git a/package.json b/package.json index 7cecfa226728..3b1937b15774 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "foreach": "yarn workspaces foreach -pvi --exclude @zwave-js/repo", "clean": "yarn turbo run clean", "build": "FORCE_COLOR=1 yarn turbo run build", + "build:cli": "yarn workspace @zwave-js/cli run build", "watch": "yarn w build", "test:ts": "FORCE_COLOR=1 yarn turbo run test:ts ${TURBO_FLAGS:-'--concurrency=1'} --", "test": "yarn w test:ts", diff --git a/packages/cli/maintenance/watch.ts b/packages/cli/maintenance/watch.ts deleted file mode 100644 index 0d7a6b682a2e..000000000000 --- a/packages/cli/maintenance/watch.ts +++ /dev/null @@ -1,131 +0,0 @@ -import PQueue from "@esm2cjs/p-queue"; -import { gray } from "ansi-colors"; -import cp from "child_process"; -import chokidar from "chokidar"; -import crypto from "crypto"; -import fs from "fs"; - -// @ts-expect-error This is available in Node 16, but we're targeting Node 14 -import stream from "stream/promises"; - -const ABORT_RUNNING = true; - -const task = process.argv[2]; -const taskArgs = process.argv.slice(3); - -const changes = new Set(); -const hashes = new Map>(); -let prevHash: Buffer | undefined; -const hashQueue = new PQueue({ concurrency: 10 }); - -let ready = false; -let child: cp.ChildProcess | undefined; - -const watcher = chokidar.watch(["build/**/*.js"], { - cwd: process.cwd(), - atomic: true, -}); - -async function exec(): Promise { - if (!ABORT_RUNNING && child) return; - - // before running the task, wait for all the hashes to be calculated - const entries = await Promise.all( - [...hashes.entries()].map( - async ([filename, hash]) => [filename, await hash] as const, - ), - ); - entries.sort(([fileA], [fileB]) => fileA.localeCompare(fileB)); - - if (changes.size > 0) { - // console.log("Files updated:"); - // for (const change of changes) { - // console.log(change); - // } - // console.log(); - changes.clear(); - } - - // Then compute a combined hash of all filenames and their hashes - const hasher = crypto.createHash("sha256"); - for (const [filename, hash] of entries) { - console.time("hashing " + filename); - hasher.update(filename); - hasher.update(hash); - console.timeEnd("hashing " + filename); - } - const totalHash = hasher.digest(); - - // And only run the task if the hash has changed - if (prevHash?.equals(totalHash)) { - // console.log("No changes detected, skipping task..."); - // console.log(); - return; - } - - if (child) child.kill("SIGTERM"); - - child = cp.spawn("yarn", [task, ...taskArgs], { - stdio: "inherit", - windowsHide: true, - }); - child.on("exit", (code) => { - child = undefined; - console.clear(); - console.log(gray(`Waiting for file changes... Press Ctrl+C to exit.`)); - }); -} - -const debouncedExec = debounce(exec, 250); - -watcher - .on("add", (filename) => { - // Whenever a file is added, hash it and call exec when done - hashes.set(filename, hashFile(filename)); - if (ready) { - changes.add("+ " + filename); - debouncedExec(); - } - }) - .on("change", (filename) => { - // Whenever a file is added, hash it and call exec when done - hashes.set(filename, hashFile(filename)); - if (ready) { - changes.add("~ " + filename); - debouncedExec(); - } - }) - .on("unlink", (filename) => { - // Whenever a file is removed, remove its hash and call exec - hashes.delete(filename); - if (ready) { - changes.add("- " + filename); - debouncedExec(); - } - }) - .on("ready", () => { - ready = true; - void exec(); - }); - -process.on("SIGINT", () => { - child?.kill("SIGTERM"); - void watcher.close(); -}); - -function hashFile(filename: string): Promise { - return hashQueue.add(async () => { - const reader = fs.createReadStream(filename); - const hasher = crypto.createHash("sha256"); - await stream.pipeline(reader, hasher); - return hasher.digest(); - }); -} - -function debounce(fn: () => void, timeout: number) { - let timeoutId: NodeJS.Timeout; - return () => { - clearTimeout(timeoutId); - timeoutId = setTimeout(fn, timeout); - }; -} diff --git a/packages/cli/package.json b/packages/cli/package.json index 9c700dad929a..124cefbebeea 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -32,34 +32,34 @@ "node": ">=14.13.0" }, "scripts": { - "ts": "node -r esbuild-register", - "w": "yarn ts maintenance/watch.ts", "build": "esbuild src/cli.tsx --outfile=build/cli.js --platform=node --target=node16 --format=cjs --bundle --external:zwave-js", "clean": "del-cli build/ \"*.tsbuildinfo\"", - "dev": "yarn w node build/cli.js", "lint:ts": "eslint --ext .ts --rule \"prettier/prettier: off\" \"src/**/*.ts\"", "lint:ts:fix": "yarn run lint:ts --fix", "lint:prettier": "prettier -c \"src/**/*.ts\"", "lint:prettier:fix": "yarn run lint:prettier -w" }, "dependencies": { + "@zwave-js/core": "workspace:*", "@zwave-js/shared": "workspace:*", "zwave-js": "workspace:*" }, "devDependencies": { "@esm2cjs/p-queue": "^7.3.0", + "@types/ink-spinner": "^3.0.1", "@types/ink-text-input": "^2.0.2", "@types/node": "^14.18.36", "@types/react": "^18", "ansi-colors": "^4.1.3", "del-cli": "^5.0.0", "esbuild": "0.15.7", - "esbuild-register": "^3.3.3", "ink": "^3.2.0", + "ink-spinner": "^4.0.3", "ink-text-input": "^4.0.3", "ink-use-stdout-dimensions": "^1.0.5", "prettier": "^2.8.1", "react": "^18.2.0", - "typescript": "4.9.4" + "typescript": "4.9.4", + "winston": "^3.8.2" } } diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 22f3b6b31d6b..33b30598d136 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,71 +1,148 @@ -import { Box, render, Text, useApp } from "ink"; +import { Box, render, Text, useApp, useInput } from "ink"; import useStdoutDimensions from "ink-use-stdout-dimensions"; -import { useState } from "react"; +import { useCallback, useState } from "react"; import { Driver, libVersion } from "zwave-js"; -import { ConfirmExit } from "./components/confirmExit"; +import { ConfirmExit } from "./components/ConfirmExit"; import { Frame } from "./components/Frame"; import { HotkeyLabel } from "./components/HotkeyLabel"; +import { Log } from "./components/Log"; import { Logo } from "./components/Logo"; +import { MainMenu } from "./components/MainMenu"; +import { ModalMessage, ModalMessageState } from "./components/ModalMessage"; import { SetUSBPath } from "./components/setUSBPath"; +import { StartingDriver } from "./components/StartingDriver"; +import { VDivider } from "./components/VDivider"; +import { startDriver } from "./lib/driver"; +import { createLogTransport, LinesBuffer } from "./lib/logging"; + +process.on("unhandledRejection", (err) => { + throw err; +}); enum CLIPage { - Idle, + Prepare, SetUSBPath, StartingDriver, + MainMenu, ConfirmExit, } +const MIN_ROWS = 30; + +const logBuffer = new LinesBuffer(10000); +const logTransport = createLogTransport(logBuffer.stream); + const CLI: React.FC = () => { const { exit } = useApp(); const [columns, rows] = useStdoutDimensions(); - const [cliPage, setCLIPage] = useState(CLIPage.Idle); - const [usbPath, setUSBPath] = useState(); + const [usbPath, setUSBPath] = useState("/dev/ttyACM0"); const [driver, setDriver] = useState(); + const [logEnabled, setLogEnabled] = useState(false); - const bottomMenu = - cliPage === CLIPage.Idle - ? { - left: [ - !!usbPath && ( - { - setCLIPage(CLIPage.StartingDriver); - }} - /> - ), - ], - right: [ + const [cliPage, setCLIPage] = useState(CLIPage.Prepare); + const [modalMessage, setModalMessage] = useState(); + const showError = useCallback( + (message: React.ReactNode) => { + setModalMessage({ + message: message, + color: "red", + }); + }, + [setModalMessage], + ); + + // Prevent the app from exiting automatically + useInput((input, key) => { + // nothing to do + }); + + const tryStartDriver = useCallback(async () => { + if (driver || !usbPath) return false; + + setCLIPage(CLIPage.StartingDriver); + try { + const driver = await startDriver(usbPath, logTransport); + setDriver(driver); + setCLIPage(CLIPage.MainMenu); + return true; + } catch (e: any) { + setCLIPage(CLIPage.Prepare); + showError(e.message); + return false; + } + }, [driver, usbPath]); + + if (rows < MIN_ROWS) { + return ( + + + Terminal is too small{" "} + + {columns}×{rows} + + . Please resize it to at least{" "} + + {columns}×{MIN_ROWS} + + . + + + ); + } + + const bottomMenu = modalMessage + ? undefined + : cliPage === CLIPage.Prepare || cliPage === CLIPage.MainMenu + ? { + left: [ + cliPage === CLIPage.Prepare && !!usbPath && ( { - setCLIPage(CLIPage.SetUSBPath); + tryStartDriver(); }} - />, + /> + ), + ], + center: [ + { + setLogEnabled((e) => !e); + }} + />, + ], + right: [ + { + setCLIPage(CLIPage.SetUSBPath); + }} + />, - setCLIPage(CLIPage.ConfirmExit)} - />, - ], - } - : undefined; + setCLIPage(CLIPage.ConfirmExit)} + />, + ], + } + : undefined; return ( - v{libVersion} - - ), + + v{libVersion} + , ], right: [ @@ -74,45 +151,86 @@ const CLI: React.FC = () => { ], }} bottomLabels={bottomMenu} - height={Math.min(30, rows)} + height={rows} paddingY={1} - justifyContent="center" + // align + // justifyContent="st" + flexDirection="row" + alignItems="stretch" + justifyContent="space-around" > - {cliPage === CLIPage.Idle && ( - setModalMessage(undefined)} + color={modalMessage.color} > - - - - Select a USB path in the options, then start the driver. - - - )} + {modalMessage.message} + + ) : ( + <> + + {cliPage === CLIPage.Prepare && ( + + + + {usbPath ? ( + Ready to start the driver. + ) : ( + + Select a USB path in the options, then + start the driver. + + )} + + )} - {cliPage === CLIPage.SetUSBPath && ( - setCLIPage(CLIPage.Idle)} - onSubmit={(path) => { - setUSBPath(path); - setCLIPage(CLIPage.Idle); - }} - /> - )} + {cliPage === CLIPage.SetUSBPath && ( + setCLIPage(CLIPage.Prepare)} + onSubmit={(path) => { + setUSBPath(path); + setCLIPage(CLIPage.Prepare); + }} + /> + )} - {cliPage === CLIPage.ConfirmExit && ( - setCLIPage(CLIPage.Idle)} - onExit={exit} - /> + {cliPage === CLIPage.StartingDriver && ( + + )} + + {cliPage === CLIPage.MainMenu && } + + {cliPage === CLIPage.ConfirmExit && ( + setCLIPage(CLIPage.Prepare)} + onExit={async () => { + if (driver) { + await driver.destroy(); + } + exit(); + }} + /> + )} + + {logEnabled && ( + + + + + )} + )} ); }; // console.clear(); -render(); -// +const { waitUntilExit } = render(); +waitUntilExit().then(() => { + console.clear(); +}); diff --git a/packages/cli/src/components/Center.tsx b/packages/cli/src/components/Center.tsx new file mode 100644 index 000000000000..b5fb94afa763 --- /dev/null +++ b/packages/cli/src/components/Center.tsx @@ -0,0 +1,15 @@ +import { Box } from "ink"; + +export interface CenterProps { + // empty +} + +export const Center: React.FC> = ( + props, +) => { + return ( + + {props.children} + + ); +}; diff --git a/packages/cli/src/components/confirmExit.tsx b/packages/cli/src/components/ConfirmExit.tsx similarity index 100% rename from packages/cli/src/components/confirmExit.tsx rename to packages/cli/src/components/ConfirmExit.tsx diff --git a/packages/cli/src/components/Log.tsx b/packages/cli/src/components/Log.tsx new file mode 100644 index 000000000000..03f79c05b2c0 --- /dev/null +++ b/packages/cli/src/components/Log.tsx @@ -0,0 +1,51 @@ +import { Box, DOMElement, measureElement, Text } from "ink"; +import { useCallback, useEffect, useRef, useState } from "react"; +import type { LinesBuffer } from "../lib/logging"; + +export interface LogProps { + buffer: LinesBuffer; +} + +export const Log: React.FC = (props) => { + const ref = useRef(null); + const [logHeight, setLogHeight] = useState(0); + const [scrollOffset, setScrollOffset] = useState(0); + const [log, setLog] = useState(""); + + const renderLog = useCallback(() => { + const lines = props.buffer.getView( + Math.max(props.buffer.size - logHeight, 0), + props.buffer.size, + ); + setLog(lines.join("\n")); + }, [props.buffer, logHeight]); + + useEffect(() => { + if (ref.current) { + setLogHeight(measureElement(ref.current).height); + } + }); + + // Update the log state whenever `buffer` emits a change event + useEffect(() => { + const listener = () => { + renderLog(); + }; + + props.buffer.on("change", listener); + return () => { + props.buffer.off("change", listener); + }; + }, [props.buffer, scrollOffset, logHeight]); + + // Render the log initially AND when the log height changes + useEffect(() => { + renderLog(); + }, [logHeight]); + + return ( + + {log} + + ); +}; diff --git a/packages/cli/src/components/Logo.tsx b/packages/cli/src/components/Logo.tsx index 1eed5b763114..13986df5bb82 100644 --- a/packages/cli/src/components/Logo.tsx +++ b/packages/cli/src/components/Logo.tsx @@ -4,12 +4,12 @@ export const Logo: React.FC = () => { return ( {` -███████╗ ██╗ ██╗ █████╗ ██╗ ██╗ ███████╗ ██╗ ███████╗ -╚══███╔╝ ██║ ██║ ██╔══██╗ ██║ ██║ ██╔════╝ ██║ ██╔════╝ - ███╔╝ ██║ █╗ ██║ ███████║ ██║ ██║ █████╗ █████╗ ██║ ███████╗ - ███╔╝ ██║███╗██║ ██╔══██║ ╚██╗ ██╔╝ ██╔══╝ ╚════╝ ██ ██║ ╚════██║ -███████╗ ╚███╔███╔╝ ██║ ██║ ╚████╔╝ ███████╗ ╚█████╔╝ ███████║ -╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝ ╚════╝ ╚══════╝ +███████╗ ██╗ ██╗ █████╗ ██╗ ██╗ ███████╗ ██╗ ███████╗ +╚══███╔╝ ██║ ██║ ██╔══██╗ ██║ ██║ ██╔════╝ ██║ ██╔════╝ + ███╔╝ █████╗ ██║ █╗ ██║ ███████║ ██║ ██║ █████╗ ██║ ███████╗ + ███╔╝ ╚════╝ ██║███╗██║ ██╔══██║ ╚██╗ ██╔╝ ██╔══╝ ██ ██║ ╚════██║ +███████╗ ╚███╔███╔╝ ██║ ██║ ╚████╔╝ ███████╗ ╚█████╔╝ ███████║ +╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝ ╚════╝ ╚══════╝ `.trim()} ); diff --git a/packages/cli/src/components/MainMenu.tsx b/packages/cli/src/components/MainMenu.tsx new file mode 100644 index 000000000000..ce87e9930d14 --- /dev/null +++ b/packages/cli/src/components/MainMenu.tsx @@ -0,0 +1,9 @@ +import { Text } from "ink"; + +export interface MainMenuProps { + // TODO: +} + +export const MainMenu: React.FC = (props) => { + return TODO; +}; diff --git a/packages/cli/src/components/ModalMessage.tsx b/packages/cli/src/components/ModalMessage.tsx new file mode 100644 index 000000000000..c4215d8d0c88 --- /dev/null +++ b/packages/cli/src/components/ModalMessage.tsx @@ -0,0 +1,41 @@ +import { Box, Text, TextProps, useInput } from "ink"; +import type { ReactNode } from "react"; +import { Center } from "./Center"; + +export interface ModalMessageState { + message: ReactNode; + color?: TextProps["color"]; +} + +export interface ModalMessageProps { + color?: TextProps["color"]; + onContinue: () => void; +} + +export const ModalMessage: React.FC< + React.PropsWithChildren +> = (props) => { + useInput((input, key) => { + if (key.return) { + props.onContinue(); + } + }); + + return ( +
+ + {props.children} + + + Press ENTER to continue... + + +
+ ); +}; diff --git a/packages/cli/src/components/StartingDriver.tsx b/packages/cli/src/components/StartingDriver.tsx new file mode 100644 index 000000000000..ea200048c4cc --- /dev/null +++ b/packages/cli/src/components/StartingDriver.tsx @@ -0,0 +1,19 @@ +import { Box, Text } from "ink"; +import Spinner from "ink-spinner"; + +export interface StartingDriverProps { + // TODO: +} + +export const StartingDriver: React.FC = (props) => { + return ( + + + + + + {" starting driver"} + + + ); +}; diff --git a/packages/cli/src/components/VDivider.tsx b/packages/cli/src/components/VDivider.tsx new file mode 100644 index 000000000000..fd76aeb9cf3a --- /dev/null +++ b/packages/cli/src/components/VDivider.tsx @@ -0,0 +1,27 @@ +import { Box, measureElement, Text, TextProps, type DOMElement } from "ink"; +import { useEffect, useRef, useState } from "react"; + +export interface VDividerProps extends TextProps { + character?: string; +} + +export const VDivider: React.FC = ({ + character = "│", + ...textProps +}) => { + const ref = useRef(null); + const [text, setText] = useState(character); + + useEffect(() => { + if (ref.current) { + const height = measureElement(ref.current).height; + setText(character.repeat(height)); + } + }); + + return ( + + {text} + + ); +}; diff --git a/packages/cli/src/lib/driver.ts b/packages/cli/src/lib/driver.ts new file mode 100644 index 000000000000..53382d5580f8 --- /dev/null +++ b/packages/cli/src/lib/driver.ts @@ -0,0 +1,47 @@ +import path from "path"; +import type winston from "winston"; +import { Driver } from "zwave-js"; + +export async function startDriver( + port: string, + logTransport: winston.transport, +): Promise { + const driver = new Driver(port, { + logConfig: { + // Do not log to console or file + enabled: false, + // But log to our own transport + transports: [logTransport], + }, + securityKeys: { + S0_Legacy: Buffer.from("0102030405060708090a0b0c0d0e0f10", "hex"), + S2_Unauthenticated: Buffer.from( + "5369389EFA18EE2A4894C7FB48347FEA", + "hex", + ), + S2_Authenticated: Buffer.from( + "656EF5C0F020F3C14238C04A1748B7E1", + "hex", + ), + S2_AccessControl: Buffer.from( + "31132050077310B6F7032F91C79C2EB8", + "hex", + ), + }, + storage: { + cacheDir: path.join(__dirname, "cache"), + lockDir: path.join(__dirname, "cache/locks"), + }, + allowBootloaderOnly: true, + }) + .on("error", console.error) + .once("driver ready", async () => { + // Test code goes here + }) + .once("bootloader ready", async () => { + // What to do when stuck in the bootloader + }); + await driver.start(); + + return driver; +} diff --git a/packages/cli/src/lib/logging.ts b/packages/cli/src/lib/logging.ts new file mode 100644 index 000000000000..e4fe0a4cf142 --- /dev/null +++ b/packages/cli/src/lib/logging.ts @@ -0,0 +1,61 @@ +import { createDefaultTransportFormat } from "@zwave-js/core"; +import { TypedEventEmitter } from "@zwave-js/shared"; +import { Writable } from "stream"; +import winston from "winston"; + +export function createLogTransport( + stream: Writable, +): winston.transports.StreamTransportInstance { + return new winston.transports.Stream({ + stream, + format: createDefaultTransportFormat(true, true), + }); +} + +export interface LinesBufferEvents { + change: () => void; +} + +export class LinesBuffer extends TypedEventEmitter { + constructor(public readonly maxSize: number) { + super(); + + const _lines: string[] = []; + let incomplete: string = ""; + + const that = this; + + this._stream = new Writable({ + write(chunk, encoding, callback) { + // TODO: maybe consider ansi codes here + const newLines = (chunk.toString() + incomplete).split("\n"); + // Remember the last line if it doesn't end with a newline + incomplete = newLines.pop()!; + + _lines.push(...newLines); + if (_lines.length > maxSize) { + _lines.splice(0, _lines.length - maxSize); + } + + that.emit("change"); + callback(); + }, + }); + this._lines = _lines; + } + + private _lines: string[]; + + private _stream: Writable; + public get stream(): Writable { + return this._stream; + } + + public get size(): number { + return this._lines.length; + } + + public getView(start: number, end: number): readonly string[] { + return this._lines.slice(start, end); + } +} diff --git a/yarn.lock b/yarn.lock index 227f9cb59d53..30ad30bfb9b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1347,6 +1347,13 @@ __metadata: languageName: node linkType: hard +"@types/cli-spinners@npm:*": + version: 1.3.0 + resolution: "@types/cli-spinners@npm:1.3.0" + checksum: 85ac09be36025e5266cc53ed2f450cc78e309fb505aae18cc9ac2cca88be7c0de3032c4f8edc3a0ada0e0321ecc039d3d9239f6a55fca8b6ff04b01b2959a73a + languageName: node + linkType: hard + "@types/clipboardy@npm:^2.0.1": version: 2.0.1 resolution: "@types/clipboardy@npm:2.0.1" @@ -1379,6 +1386,16 @@ __metadata: languageName: node linkType: hard +"@types/ink-spinner@npm:^3.0.1": + version: 3.0.1 + resolution: "@types/ink-spinner@npm:3.0.1" + dependencies: + "@types/cli-spinners": "*" + "@types/react": "*" + checksum: 97ae685754d20399d578679899c9231c9a303c072121154937a7fd32a3ef9e33ffb11a5d0f75d48954f121e85609ed103b421e783dc4911660cb794a59f14c19 + languageName: node + linkType: hard + "@types/ink-text-input@npm:^2.0.2": version: 2.0.2 resolution: "@types/ink-text-input@npm:2.0.2" @@ -1512,7 +1529,7 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:^18": +"@types/react@npm:*, @types/react@npm:^18": version: 18.0.28 resolution: "@types/react@npm:18.0.28" dependencies: @@ -1786,20 +1803,23 @@ __metadata: resolution: "@zwave-js/cli@workspace:packages/cli" dependencies: "@esm2cjs/p-queue": ^7.3.0 + "@types/ink-spinner": ^3.0.1 "@types/ink-text-input": ^2.0.2 "@types/node": ^14.18.36 "@types/react": ^18 + "@zwave-js/core": "workspace:*" "@zwave-js/shared": "workspace:*" ansi-colors: ^4.1.3 del-cli: ^5.0.0 esbuild: 0.15.7 - esbuild-register: ^3.3.3 ink: ^3.2.0 + ink-spinner: ^4.0.3 ink-text-input: ^4.0.3 ink-use-stdout-dimensions: ^1.0.5 prettier: ^2.8.1 react: ^18.2.0 typescript: 4.9.4 + winston: ^3.8.2 zwave-js: "workspace:*" bin: cli: bin/cli.js @@ -2928,6 +2948,13 @@ __metadata: languageName: node linkType: hard +"cli-spinners@npm:^2.3.0": + version: 2.7.0 + resolution: "cli-spinners@npm:2.7.0" + checksum: a9afaf73f58d1f951fb23742f503631b3cf513f43f4c7acb1b640100eb76bfa16efbcd1994d149ffc6603a6d75dd3d4a516a76f125f90dce437de9b16fd0ee6f + languageName: node + linkType: hard + "cli-spinners@npm:^2.5.0": version: 2.6.1 resolution: "cli-spinners@npm:2.6.1" @@ -5200,6 +5227,18 @@ __metadata: languageName: node linkType: hard +"ink-spinner@npm:^4.0.3": + version: 4.0.3 + resolution: "ink-spinner@npm:4.0.3" + dependencies: + cli-spinners: ^2.3.0 + peerDependencies: + ink: ">=3.0.5" + react: ">=16.8.2" + checksum: d3785d688dd1ba19fb7a850b7a2c1dd8b2d06e4c77e6a7cc6c5bbd366a5a721e9ea45d036447016a9028f7519994077ce603a60a43a495e17b7b443b8a513ddc + languageName: node + linkType: hard + "ink-text-input@npm:^4.0.3": version: 4.0.3 resolution: "ink-text-input@npm:4.0.3" From 8b0591352e9989f46b0f6cf2638cf20106118dc0 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 1 Mar 2023 22:31:28 +0100 Subject: [PATCH 04/27] chore: reduce bundle size --- packages/cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 124cefbebeea..81d955c7c95b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -32,7 +32,7 @@ "node": ">=14.13.0" }, "scripts": { - "build": "esbuild src/cli.tsx --outfile=build/cli.js --platform=node --target=node16 --format=cjs --bundle --external:zwave-js", + "build": "esbuild --define:process.env.NODE_ENV=\\\"production\\\" src/cli.tsx --outfile=build/cli.js --platform=node --target=node16 --format=cjs --bundle --minify --sourcemap --external:zwave-js", "clean": "del-cli build/ \"*.tsbuildinfo\"", "lint:ts": "eslint --ext .ts --rule \"prettier/prettier: off\" \"src/**/*.ts\"", "lint:ts:fix": "yarn run lint:ts --fix", From f363cafb99b35e53fabcdaff3a1d182fc9ac9b93 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Thu, 2 Mar 2023 01:21:35 +0100 Subject: [PATCH 05/27] refactor: dynamic menu --- package.json | 2 +- packages/cli/package.json | 1 + packages/cli/src/cli.tsx | 342 +++++++++--------- .../cli/src/components/StartingDriver.tsx | 41 ++- packages/cli/src/components/USBPathInfo.tsx | 11 + packages/cli/src/hooks/useActions.ts | 11 + packages/cli/src/hooks/useDialogs.ts | 10 + packages/cli/src/hooks/useDriver.ts | 17 + packages/cli/src/hooks/useGlobals.ts | 13 + packages/cli/src/hooks/useMenu.ts | 93 +++++ packages/cli/src/hooks/useNavigation.ts | 26 ++ packages/cli/src/lib/menu.tsx | 70 ++++ packages/cli/src/pages/Prepare.tsx | 64 ++++ 13 files changed, 532 insertions(+), 169 deletions(-) create mode 100644 packages/cli/src/components/USBPathInfo.tsx create mode 100644 packages/cli/src/hooks/useActions.ts create mode 100644 packages/cli/src/hooks/useDialogs.ts create mode 100644 packages/cli/src/hooks/useDriver.ts create mode 100644 packages/cli/src/hooks/useGlobals.ts create mode 100644 packages/cli/src/hooks/useMenu.ts create mode 100644 packages/cli/src/hooks/useNavigation.ts create mode 100644 packages/cli/src/lib/menu.tsx create mode 100644 packages/cli/src/pages/Prepare.tsx diff --git a/package.json b/package.json index 3b1937b15774..5b5297fb281d 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "foreach": "yarn workspaces foreach -pvi --exclude @zwave-js/repo", "clean": "yarn turbo run clean", "build": "FORCE_COLOR=1 yarn turbo run build", - "build:cli": "yarn workspace @zwave-js/cli run build", + "build:cli": "yarn workspace @zwave-js/cli run build:dev", "watch": "yarn w build", "test:ts": "FORCE_COLOR=1 yarn turbo run test:ts ${TURBO_FLAGS:-'--concurrency=1'} --", "test": "yarn w test:ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index 81d955c7c95b..1e7dd82a0a63 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -32,6 +32,7 @@ "node": ">=14.13.0" }, "scripts": { + "build:dev": "esbuild src/cli.tsx --outfile=build/cli.js --platform=node --target=node16 --format=cjs --bundle --sourcemap --external:zwave-js", "build": "esbuild --define:process.env.NODE_ENV=\\\"production\\\" src/cli.tsx --outfile=build/cli.js --platform=node --target=node16 --format=cjs --bundle --minify --sourcemap --external:zwave-js", "clean": "del-cli build/ \"*.tsbuildinfo\"", "lint:ts": "eslint --ext .ts --rule \"prettier/prettier: off\" \"src/**/*.ts\"", diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 33b30598d136..3f1debeb549f 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,37 +1,67 @@ import { Box, render, Text, useApp, useInput } from "ink"; import useStdoutDimensions from "ink-use-stdout-dimensions"; -import { useCallback, useState } from "react"; -import { Driver, libVersion } from "zwave-js"; +import { useCallback, useMemo, useState } from "react"; +import type { Driver } from "zwave-js"; import { ConfirmExit } from "./components/ConfirmExit"; -import { Frame } from "./components/Frame"; -import { HotkeyLabel } from "./components/HotkeyLabel"; +import { Frame, FrameLabelGroupProps } from "./components/Frame"; import { Log } from "./components/Log"; -import { Logo } from "./components/Logo"; import { MainMenu } from "./components/MainMenu"; import { ModalMessage, ModalMessageState } from "./components/ModalMessage"; import { SetUSBPath } from "./components/setUSBPath"; -import { StartingDriver } from "./components/StartingDriver"; +import { StartingDriverPage } from "./components/StartingDriver"; import { VDivider } from "./components/VDivider"; -import { startDriver } from "./lib/driver"; +import { Action, ActionsContext } from "./hooks/useActions"; +import { DialogsContext } from "./hooks/useDialogs"; +import { DriverContext } from "./hooks/useDriver"; +import { GlobalsContext } from "./hooks/useGlobals"; +import { MenuContext, MenuItem } from "./hooks/useMenu"; +import { CLIPage, NavigationContext } from "./hooks/useNavigation"; import { createLogTransport, LinesBuffer } from "./lib/logging"; +import { defaultMenuItems } from "./lib/menu"; +import { PreparePage } from "./pages/Prepare"; process.on("unhandledRejection", (err) => { throw err; }); -enum CLIPage { - Prepare, - SetUSBPath, - StartingDriver, - MainMenu, - ConfirmExit, -} - const MIN_ROWS = 30; const logBuffer = new LinesBuffer(10000); const logTransport = createLogTransport(logBuffer.stream); +const compareMenuItems = (a: MenuItem, b: MenuItem): number => { + if (a.compareTo) { + return a.compareTo(b); + } else if (b.compareTo) { + return -b.compareTo(a); + } else { + return 0; + } +}; + +const normalizeMenuItems = (items: MenuItem[]): FrameLabelGroupProps => { + const leftItems = items.filter((i) => i.location.endsWith("Left")); + const centerItems = items.filter((i) => i.location.endsWith("Center")); + const rightItems = items.filter((i) => i.location.endsWith("Right")); + return { + left: leftItems + .filter((i) => i.visible !== false) + .sort(compareMenuItems) + .map((i) => i.item) + .filter(Boolean), + center: centerItems + .filter((i) => i.visible !== false) + .sort(compareMenuItems) + .map((i) => i.item) + .filter(Boolean), + right: rightItems + .filter((i) => i.visible !== false) + .sort(compareMenuItems) + .map((i) => i.item) + .filter(Boolean), + }; +}; + const CLI: React.FC = () => { const { exit } = useApp(); const [columns, rows] = useStdoutDimensions(); @@ -41,6 +71,7 @@ const CLI: React.FC = () => { const [logEnabled, setLogEnabled] = useState(false); const [cliPage, setCLIPage] = useState(CLIPage.Prepare); + const [modalMessage, setModalMessage] = useState(); const showError = useCallback( (message: React.ReactNode) => { @@ -52,26 +83,42 @@ const CLI: React.FC = () => { [setModalMessage], ); + const [menuItems, setMenuItems] = useState(defaultMenuItems); + const updateMenuItems = useCallback( + (added: MenuItem[], changed: MenuItem[], removed: MenuItem[]) => { + setMenuItems((current) => { + const ret = [ + ...current.filter((i) => !removed.includes(i)), + ...added, + ]; + return ret; + }); + }, + [setMenuItems], + ); + // Prevent the app from exiting automatically useInput((input, key) => { // nothing to do }); - const tryStartDriver = useCallback(async () => { - if (driver || !usbPath) return false; - - setCLIPage(CLIPage.StartingDriver); - try { - const driver = await startDriver(usbPath, logTransport); - setDriver(driver); - setCLIPage(CLIPage.MainMenu); - return true; - } catch (e: any) { - setCLIPage(CLIPage.Prepare); - showError(e.message); - return false; - } - }, [driver, usbPath]); + const performAction = useCallback(async (action: Action) => { + // if (action.type === "navigate") { + // setCLIPage(action.to); + // } + }, []); + + const topMenu = useMemo(() => { + const topItems = menuItems.filter((i) => i.location.startsWith("top")); + return normalizeMenuItems(topItems); + }, [menuItems]); + + const bottomMenu = useMemo(() => { + const bottomItems = menuItems.filter((i) => + i.location.startsWith("bottom"), + ); + return normalizeMenuItems(bottomItems); + }, [menuItems]); if (rows < MIN_ROWS) { return ( @@ -91,146 +138,113 @@ const CLI: React.FC = () => { ); } - const bottomMenu = modalMessage - ? undefined - : cliPage === CLIPage.Prepare || cliPage === CLIPage.MainMenu - ? { - left: [ - cliPage === CLIPage.Prepare && !!usbPath && ( - { - tryStartDriver(); - }} - /> - ), - ], - center: [ - { - setLogEnabled((e) => !e); - }} - />, - ], - right: [ - { - setCLIPage(CLIPage.SetUSBPath); - }} - />, - - setCLIPage(CLIPage.ConfirmExit)} - />, - ], - } - : undefined; - return ( - - v{libVersion} -
, - ], - right: [ - - USB Path: {usbPath || "(none)"} - , - ], - }} - bottomLabels={bottomMenu} - height={rows} - paddingY={1} - // align - // justifyContent="st" - flexDirection="row" - alignItems="stretch" - justifyContent="space-around" - > - {modalMessage ? ( - setModalMessage(undefined)} - color={modalMessage.color} + + + - {modalMessage.message} - - ) : ( - <> - - {cliPage === CLIPage.Prepare && ( - - - - {usbPath ? ( - Ready to start the driver. - ) : ( - - Select a USB path in the options, then - start the driver. - - )} - - )} - - {cliPage === CLIPage.SetUSBPath && ( - setCLIPage(CLIPage.Prepare)} - onSubmit={(path) => { - setUSBPath(path); - setCLIPage(CLIPage.Prepare); - }} - /> - )} - - {cliPage === CLIPage.StartingDriver && ( - - )} - - {cliPage === CLIPage.MainMenu && } - - {cliPage === CLIPage.ConfirmExit && ( - setCLIPage(CLIPage.Prepare)} - onExit={async () => { - if (driver) { - await driver.destroy(); - } - exit(); - }} - /> - )} - - {logEnabled && ( - - - - - )} - - )} - + + + + + {modalMessage ? ( + + setModalMessage(undefined) + } + color={modalMessage.color} + > + {modalMessage.message} + + ) : ( + <> + + {cliPage === + CLIPage.Prepare && ( + + )} + + {cliPage === + CLIPage.SetUSBPath && ( + + setCLIPage( + CLIPage.Prepare, + ) + } + onSubmit={(path) => { + setUSBPath(path); + setCLIPage( + CLIPage.Prepare, + ); + }} + /> + )} + + {cliPage === + CLIPage.StartingDriver && ( + + )} + + {cliPage === + CLIPage.MainMenu && ( + + )} + + {cliPage === + CLIPage.ConfirmExit && ( + + setCLIPage( + CLIPage.Prepare, + ) + } + onExit={async () => { + if (driver) { + await driver.destroy(); + } + exit(); + }} + /> + )} + + {logEnabled && ( + + + + + )} + + )} + + + + + + + ); }; // console.clear(); const { waitUntilExit } = render(); waitUntilExit().then(() => { - console.clear(); + // console.clear(); }); diff --git a/packages/cli/src/components/StartingDriver.tsx b/packages/cli/src/components/StartingDriver.tsx index ea200048c4cc..96385f52d1a8 100644 --- a/packages/cli/src/components/StartingDriver.tsx +++ b/packages/cli/src/components/StartingDriver.tsx @@ -1,11 +1,44 @@ import { Box, Text } from "ink"; import Spinner from "ink-spinner"; +import { useEffect } from "react"; +import { useDialogs } from "../hooks/useDialogs"; +import { useDriver } from "../hooks/useDriver"; +import { useGlobals } from "../hooks/useGlobals"; +import { useMenu } from "../hooks/useMenu"; +import { CLIPage, useNavigation } from "../hooks/useNavigation"; +import { startDriver } from "../lib/driver"; +import { toggleLogMenuItem } from "../lib/menu"; -export interface StartingDriverProps { - // TODO: -} +export const StartingDriverPage: React.FC = () => { + useMenu([toggleLogMenuItem]); + + const { usbPath, logTransport } = useGlobals(); + const [driver, setDriver] = useDriver(); + const [navigate] = useNavigation(); + const { showError } = useDialogs(); + + // When opening this page, try to start the driver + useEffect(() => { + if (driver) { + navigate(CLIPage.MainMenu); + return; + } else if (!usbPath) { + navigate(CLIPage.Prepare); + return; + } + + (async () => { + try { + const driver = await startDriver(usbPath, logTransport); + setDriver(driver); + navigate(CLIPage.MainMenu); + } catch (e: any) { + navigate(CLIPage.Prepare); + showError(e.message); + } + })(); + }, []); -export const StartingDriver: React.FC = (props) => { return ( diff --git a/packages/cli/src/components/USBPathInfo.tsx b/packages/cli/src/components/USBPathInfo.tsx new file mode 100644 index 000000000000..35326e4d5cb3 --- /dev/null +++ b/packages/cli/src/components/USBPathInfo.tsx @@ -0,0 +1,11 @@ +import { Text } from "ink"; +import { useGlobals } from "../hooks/useGlobals"; + +export const USBPathInfo: React.FC = () => { + const { usbPath } = useGlobals(); + return ( + + USB Path: {usbPath || "(none)"} + + ); +}; diff --git a/packages/cli/src/hooks/useActions.ts b/packages/cli/src/hooks/useActions.ts new file mode 100644 index 000000000000..65503c3fa099 --- /dev/null +++ b/packages/cli/src/hooks/useActions.ts @@ -0,0 +1,11 @@ +import React from "react"; + +export type Action = void; + +interface IActionsContext { + do: (what: Action) => void; +} + +export const ActionsContext = React.createContext({} as any); + +export const useActions = () => React.useContext(ActionsContext); diff --git a/packages/cli/src/hooks/useDialogs.ts b/packages/cli/src/hooks/useDialogs.ts new file mode 100644 index 000000000000..6adc1c841c2a --- /dev/null +++ b/packages/cli/src/hooks/useDialogs.ts @@ -0,0 +1,10 @@ +import React from "react"; + +export type IDialogsContext = { + showError: (message: React.ReactNode) => void; +}; + +// Context that stores references to the methods that show Notifications and Modals +export const DialogsContext = React.createContext({} as any); + +export const useDialogs = () => React.useContext(DialogsContext); diff --git a/packages/cli/src/hooks/useDriver.ts b/packages/cli/src/hooks/useDriver.ts new file mode 100644 index 000000000000..83a1a67755f5 --- /dev/null +++ b/packages/cli/src/hooks/useDriver.ts @@ -0,0 +1,17 @@ +import React from "react"; +import type { Driver } from "zwave-js"; + +interface IDriverContext { + driver: Driver; + setDriver: (driver: Driver) => void; +} + +export const DriverContext = React.createContext({} as any); + +export const useDriver = (): readonly [ + driver: Driver, + setDriver: (driver: Driver) => void, +] => { + const { driver, setDriver } = React.useContext(DriverContext); + return [driver, setDriver]; +}; diff --git a/packages/cli/src/hooks/useGlobals.ts b/packages/cli/src/hooks/useGlobals.ts new file mode 100644 index 000000000000..0f8abd5026bc --- /dev/null +++ b/packages/cli/src/hooks/useGlobals.ts @@ -0,0 +1,13 @@ +import React from "react"; +import type winston from "winston"; + +interface IGlobalsContext { + usbPath: string; + logTransport: winston.transport; + logEnabled: boolean; + setLogEnabled: React.Dispatch>; +} + +export const GlobalsContext = React.createContext({} as any); + +export const useGlobals = () => React.useContext(GlobalsContext); diff --git a/packages/cli/src/hooks/useMenu.ts b/packages/cli/src/hooks/useMenu.ts new file mode 100644 index 000000000000..d2ea7fc20343 --- /dev/null +++ b/packages/cli/src/hooks/useMenu.ts @@ -0,0 +1,93 @@ +import React from "react"; + +interface IMenuContext { + updateItems: ( + added: MenuItem[], + changed: MenuItem[], + removed: MenuItem[], + ) => void; +} + +export interface MenuItem { + location: + | "topLeft" + | "topCenter" + | "topRight" + | "topCenter" + | "bottomLeft" + | "bottomCenter" + | "bottomRight"; + item: React.ReactNode; + visible?: boolean; + compareTo?: (item: MenuItem) => -1 | 0 | 1; +} + +export type MaybeMenuItem = MenuItem | null | undefined | false; + +export const MenuContext = React.createContext({} as any); + +function hashItem(item: MaybeMenuItem): string { + if (!item) return ""; + return JSON.stringify({ + location: item.location, + visible: item.visible ?? true, + }); +} + +function hashItems(items: MaybeMenuItem[]): string { + return items.map(hashItem).join(";"); +} + +export function useMenu(items: MaybeMenuItem[]): () => void { + const itemsRef = React.useRef(new Map()); + const context = React.useContext(MenuContext); + + const setMenuItems = React.useCallback( + (_items: MaybeMenuItem[]) => { + const items = _items.filter((item) => !!item) as MenuItem[]; + const newItems = items.filter( + (item) => !itemsRef.current.has(item), + ); + const removedItems = [...itemsRef.current.keys()].filter( + (item) => !items.includes(item), + ); + const changedItems = items.filter( + (item) => + itemsRef.current.has(item) && + itemsRef.current.get(item) !== hashItem(item), + ); + + for (const item of removedItems) { + itemsRef.current.delete(item); + } + for (const item of newItems) { + itemsRef.current.set(item, hashItem(item)); + } + for (const item of changedItems) { + itemsRef.current.set(item, hashItem(item)); + } + + if (newItems.length || changedItems.length || removedItems.length) { + context.updateItems(newItems, changedItems, removedItems); + } + }, + [context, itemsRef], + ); + + React.useEffect(() => { + setMenuItems(items); + + // Remove all registered menu items when unmounting + return () => { + context.updateItems([], [], Array.from(itemsRef.current.keys())); + }; + }, []); + + // Update menu items when the items array hash changes + React.useEffect(() => { + setMenuItems(items); + }, [hashItems(items)]); + + // Return a hook to force-update if we cannot detec the changes + return () => setMenuItems(items); +} diff --git a/packages/cli/src/hooks/useNavigation.ts b/packages/cli/src/hooks/useNavigation.ts new file mode 100644 index 000000000000..88956334e9e9 --- /dev/null +++ b/packages/cli/src/hooks/useNavigation.ts @@ -0,0 +1,26 @@ +import React from "react"; + +export enum CLIPage { + Prepare, + SetUSBPath, + StartingDriver, + MainMenu, + ConfirmExit, +} + +interface INavigationContext { + currentPage: CLIPage; + navigate: (page: CLIPage) => void; +} + +export const NavigationContext = React.createContext( + {} as any, +); + +export const useNavigation = (): readonly [ + navigate: (page: CLIPage) => void, + currentPage: CLIPage, +] => { + const { currentPage, navigate } = React.useContext(NavigationContext); + return [navigate, currentPage]; +}; diff --git a/packages/cli/src/lib/menu.tsx b/packages/cli/src/lib/menu.tsx new file mode 100644 index 000000000000..240adbea5fe2 --- /dev/null +++ b/packages/cli/src/lib/menu.tsx @@ -0,0 +1,70 @@ +import { Text } from "ink"; +import { libVersion } from "zwave-js"; +import { HotkeyLabel } from "../components/HotkeyLabel"; +import { USBPathInfo } from "../components/USBPathInfo"; +import { useGlobals } from "../hooks/useGlobals"; +import type { MenuItem } from "../hooks/useMenu"; +import { CLIPage, useNavigation } from "../hooks/useNavigation"; + +const ToggleLogMenuItem: React.FC = () => { + const { logEnabled, setLogEnabled } = useGlobals(); + return ( + { + setLogEnabled((e) => !e); + }} + /> + ); +}; + +export const toggleLogMenuItem: MenuItem = { + location: "bottomCenter", + item: , +}; + +// ===================================================================== + +const ExitMenuItem: React.FC = () => { + const [navigate] = useNavigation(); + + return ( + navigate(CLIPage.ConfirmExit)} + /> + ); +}; + +export const exitMenuItem: MenuItem = { + location: "bottomRight", + item: , + // always put this at the end + compareTo: () => 1, +}; + +// ===================================================================== + +export const defaultMenuItems: MenuItem[] = [ + { + location: "topLeft", + item: "Z-Wave JS", + }, + { + location: "topLeft", + item: ( + + v{libVersion} + + ), + }, + { + location: "topRight", + item: , + }, + exitMenuItem, +]; diff --git a/packages/cli/src/pages/Prepare.tsx b/packages/cli/src/pages/Prepare.tsx new file mode 100644 index 000000000000..7896ce8a6aad --- /dev/null +++ b/packages/cli/src/pages/Prepare.tsx @@ -0,0 +1,64 @@ +import { Box, Text } from "ink"; +import { useState } from "react"; +import { HotkeyLabel } from "../components/HotkeyLabel"; +import { Logo } from "../components/Logo"; +import { useGlobals } from "../hooks/useGlobals"; +import { MenuItem, useMenu } from "../hooks/useMenu"; +import { CLIPage, useNavigation } from "../hooks/useNavigation"; +import { toggleLogMenuItem } from "../lib/menu"; + +export interface PreparePageProps { + // TODO: +} + +export const PreparePage: React.FC = (props) => { + const { usbPath } = useGlobals(); + const [navigate] = useNavigation(); + + const [visible, setVisible] = useState(false); + + const startDriverMenuItem: MenuItem = { + location: "bottomLeft", + item: ( + { + navigate(CLIPage.StartingDriver); + }} + /> + ), + visible: !!usbPath, + }; + + useMenu([ + startDriverMenuItem, + toggleLogMenuItem, + { + location: "bottomRight", + item: ( + { + navigate(CLIPage.SetUSBPath); + }} + /> + ), + }, + ]); + + return ( + + + + {usbPath ? ( + Ready to start the driver. + ) : ( + + Select a USB path in the options, then start the driver. + + )} + + ); +}; From e04d48286b934cbc0ed779a3fbcd995a8eb0ae55 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Thu, 2 Mar 2023 10:31:11 +0100 Subject: [PATCH 06/27] refactor: move menu item creation to hook --- packages/cli/src/cli.tsx | 69 +++------------------------ packages/cli/src/hooks/useMenu.ts | 78 +++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 63 deletions(-) diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 3f1debeb549f..6e232b9fb788 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,9 +1,9 @@ import { Box, render, Text, useApp, useInput } from "ink"; import useStdoutDimensions from "ink-use-stdout-dimensions"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useState } from "react"; import type { Driver } from "zwave-js"; import { ConfirmExit } from "./components/ConfirmExit"; -import { Frame, FrameLabelGroupProps } from "./components/Frame"; +import { Frame } from "./components/Frame"; import { Log } from "./components/Log"; import { MainMenu } from "./components/MainMenu"; import { ModalMessage, ModalMessageState } from "./components/ModalMessage"; @@ -14,7 +14,7 @@ import { Action, ActionsContext } from "./hooks/useActions"; import { DialogsContext } from "./hooks/useDialogs"; import { DriverContext } from "./hooks/useDriver"; import { GlobalsContext } from "./hooks/useGlobals"; -import { MenuContext, MenuItem } from "./hooks/useMenu"; +import { MenuContext, useMenuItemSlots } from "./hooks/useMenu"; import { CLIPage, NavigationContext } from "./hooks/useNavigation"; import { createLogTransport, LinesBuffer } from "./lib/logging"; import { defaultMenuItems } from "./lib/menu"; @@ -29,39 +29,6 @@ const MIN_ROWS = 30; const logBuffer = new LinesBuffer(10000); const logTransport = createLogTransport(logBuffer.stream); -const compareMenuItems = (a: MenuItem, b: MenuItem): number => { - if (a.compareTo) { - return a.compareTo(b); - } else if (b.compareTo) { - return -b.compareTo(a); - } else { - return 0; - } -}; - -const normalizeMenuItems = (items: MenuItem[]): FrameLabelGroupProps => { - const leftItems = items.filter((i) => i.location.endsWith("Left")); - const centerItems = items.filter((i) => i.location.endsWith("Center")); - const rightItems = items.filter((i) => i.location.endsWith("Right")); - return { - left: leftItems - .filter((i) => i.visible !== false) - .sort(compareMenuItems) - .map((i) => i.item) - .filter(Boolean), - center: centerItems - .filter((i) => i.visible !== false) - .sort(compareMenuItems) - .map((i) => i.item) - .filter(Boolean), - right: rightItems - .filter((i) => i.visible !== false) - .sort(compareMenuItems) - .map((i) => i.item) - .filter(Boolean), - }; -}; - const CLI: React.FC = () => { const { exit } = useApp(); const [columns, rows] = useStdoutDimensions(); @@ -83,19 +50,7 @@ const CLI: React.FC = () => { [setModalMessage], ); - const [menuItems, setMenuItems] = useState(defaultMenuItems); - const updateMenuItems = useCallback( - (added: MenuItem[], changed: MenuItem[], removed: MenuItem[]) => { - setMenuItems((current) => { - const ret = [ - ...current.filter((i) => !removed.includes(i)), - ...added, - ]; - return ret; - }); - }, - [setMenuItems], - ); + const [menuItemSlots, updateMenuItems] = useMenuItemSlots(defaultMenuItems); // Prevent the app from exiting automatically useInput((input, key) => { @@ -108,18 +63,6 @@ const CLI: React.FC = () => { // } }, []); - const topMenu = useMemo(() => { - const topItems = menuItems.filter((i) => i.location.startsWith("top")); - return normalizeMenuItems(topItems); - }, [menuItems]); - - const bottomMenu = useMemo(() => { - const bottomItems = menuItems.filter((i) => - i.location.startsWith("bottom"), - ); - return normalizeMenuItems(bottomItems); - }, [menuItems]); - if (rows < MIN_ROWS) { return ( @@ -152,8 +95,8 @@ const CLI: React.FC = () => { > void { // Return a hook to force-update if we cannot detec the changes return () => setMenuItems(items); } + +// ^ for components +// ===================================================================== +// v for the CLI + +export type MenuItemSlots = Record< + "top" | "bottom", + Record<"left" | "center" | "right", React.ReactNode[]> +>; + +const compareMenuItems = (a: MenuItem, b: MenuItem): number => { + if (a.compareTo) { + return a.compareTo(b); + } else if (b.compareTo) { + return -b.compareTo(a); + } else { + return 0; + } +}; + +const visibleItemsAsNodes = (items: MenuItem[]): React.ReactNode[] => { + return items + .filter((i) => i.visible !== false) + .sort(compareMenuItems) + .map((i) => i.item) + .filter(Boolean); +}; + +const normalizeMenuItems = (items: MenuItem[]): MenuItemSlots["top"] => { + const leftItems = items.filter((i) => i.location.endsWith("Left")); + const centerItems = items.filter((i) => i.location.endsWith("Center")); + const rightItems = items.filter((i) => i.location.endsWith("Right")); + return { + left: visibleItemsAsNodes(leftItems), + center: visibleItemsAsNodes(centerItems), + right: visibleItemsAsNodes(rightItems), + }; +}; + +export function useMenuItemSlots( + mergeWith: MenuItem[], +): readonly [ + slots: MenuItemSlots, + updateItems: ( + added: MenuItem[], + changed: MenuItem[], + removed: MenuItem[], + ) => void, +] { + const [menuItems, setMenuItems] = React.useState(mergeWith); + + const updateMenuItems = React.useCallback( + (added: MenuItem[], changed: MenuItem[], removed: MenuItem[]) => { + setMenuItems((current) => { + const ret = [ + ...current.filter((i) => !removed.includes(i)), + ...added, + ]; + return ret; + }); + }, + [setMenuItems], + ); + + const slots = React.useMemo(() => { + const topItems = menuItems.filter((i) => i.location.startsWith("top")); + const bottomItems = menuItems.filter((i) => + i.location.startsWith("bottom"), + ); + + return { + top: normalizeMenuItems(topItems), + bottom: normalizeMenuItems(bottomItems), + }; + }, [menuItems]); + + return [slots, updateMenuItems]; +} From a5b488e94594585014ee794595cc4e45fce46066 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Thu, 2 Mar 2023 17:18:26 +0100 Subject: [PATCH 07/27] fix: stuff --- .vscode/launch.json | 3 + packages/cli/src/cli.tsx | 175 ++++++++++--------- packages/cli/src/components/Center.tsx | 15 +- packages/cli/src/components/Frame.tsx | 80 +-------- packages/cli/src/components/HotkeyLabel.tsx | 5 +- packages/cli/src/components/ModalMessage.tsx | 50 +++++- packages/cli/src/components/VDivider.tsx | 8 +- packages/cli/src/components/ZStack.tsx | 67 +++++++ packages/cli/src/components/menu.tsx | 9 +- packages/cli/src/lib/boxProps.ts | 76 ++++++++ 10 files changed, 320 insertions(+), 168 deletions(-) create mode 100644 packages/cli/src/components/ZStack.tsx create mode 100644 packages/cli/src/lib/boxProps.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index ca240fc5261f..db85cd21070d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,6 +13,9 @@ "--async-stack-traces", "${workspaceFolder}/packages/cli/build/cli.js" ], + "env": { + // "DEV": "true" + }, "skipFiles": ["/**"], "type": "node", "console": "integratedTerminal", diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 6e232b9fb788..3bb0e6d91186 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -10,6 +10,7 @@ import { ModalMessage, ModalMessageState } from "./components/ModalMessage"; import { SetUSBPath } from "./components/setUSBPath"; import { StartingDriverPage } from "./components/StartingDriver"; import { VDivider } from "./components/VDivider"; +import { Layer, ZStack } from "./components/ZStack"; import { Action, ActionsContext } from "./hooks/useActions"; import { DialogsContext } from "./hooks/useDialogs"; import { DriverContext } from "./hooks/useDriver"; @@ -94,89 +95,107 @@ const CLI: React.FC = () => { value={{ driver: driver!, setDriver }} > - - {modalMessage ? ( - - setModalMessage(undefined) + + + - {modalMessage.message} - - ) : ( - <> - - {cliPage === - CLIPage.Prepare && ( - - )} - - {cliPage === - CLIPage.SetUSBPath && ( - - setCLIPage( - CLIPage.Prepare, - ) - } - onSubmit={(path) => { - setUSBPath(path); - setCLIPage( - CLIPage.Prepare, - ); - }} - /> - )} - - {cliPage === - CLIPage.StartingDriver && ( - - )} - - {cliPage === - CLIPage.MainMenu && ( - - )} - - {cliPage === - CLIPage.ConfirmExit && ( - - setCLIPage( - CLIPage.Prepare, - ) - } - onExit={async () => { - if (driver) { - await driver.destroy(); + <> + + {cliPage === + CLIPage.Prepare && ( + + )} + + {cliPage === + CLIPage.SetUSBPath && ( + + setCLIPage( + CLIPage.Prepare, + ) } - exit(); - }} - /> - )} - - {logEnabled && ( - - - + onSubmit={( + path, + ) => { + setUSBPath( + path, + ); + setCLIPage( + CLIPage.Prepare, + ); + }} + /> + )} + + {cliPage === + CLIPage.StartingDriver && ( + + )} + + {cliPage === + CLIPage.MainMenu && ( + + )} + + {cliPage === + CLIPage.ConfirmExit && ( + + setCLIPage( + CLIPage.Prepare, + ) + } + onExit={async () => { + if (driver) { + await driver.destroy(); + } + exit(); + }} + /> + )} - )} - + {logEnabled && ( + + + + + )} + + + + {modalMessage && ( + + + setModalMessage(undefined) + } + color={modalMessage.color} + > + {modalMessage.message} + + )} - + diff --git a/packages/cli/src/components/Center.tsx b/packages/cli/src/components/Center.tsx index b5fb94afa763..e4d30718fce3 100644 --- a/packages/cli/src/components/Center.tsx +++ b/packages/cli/src/components/Center.tsx @@ -1,15 +1,24 @@ import { Box } from "ink"; +import type { OuterBoxProps } from "../lib/boxProps"; -export interface CenterProps { +export interface CenterProps + extends Omit { // empty } export const Center: React.FC> = ( props, ) => { + const { children, ...boxProps } = props; return ( - - {props.children} + + {children} ); }; diff --git a/packages/cli/src/components/Frame.tsx b/packages/cli/src/components/Frame.tsx index c926b53426a5..15cb6d0818b3 100644 --- a/packages/cli/src/components/Frame.tsx +++ b/packages/cli/src/components/Frame.tsx @@ -1,42 +1,6 @@ -import { pick } from "@zwave-js/shared/safe"; -import { Box, Text } from "ink"; -import type { ComponentPropsWithoutRef } from "react"; - -type BoxProps = ComponentPropsWithoutRef; - -// type BoxProps = { -// readonly position?: "absolute" | "relative" | undefined; -// readonly marginLeft?: number | undefined; -// readonly marginRight?: number | undefined; -// readonly marginTop?: number | undefined; -// readonly marginBottom?: number | undefined; -// readonly paddingLeft?: number | undefined; -// readonly paddingRight?: number | undefined; -// readonly paddingTop?: number | undefined; -// readonly paddingBottom?: number | undefined; -// readonly flexGrow?: number | undefined; -// readonly flexShrink?: number | undefined; -// readonly flexDirection?: "row" | "column" | "row-reverse" | "column-reverse" | undefined; -// readonly flexBasis?: string | number | undefined; -// readonly alignItems?: "flex-start" | "center" | "flex-end" | "stretch" | undefined; -// readonly alignSelf?: "flex-start" | "center" | "flex-end" | "auto" | undefined; -// readonly justifyContent?: "flex-start" | "center" | "flex-end" | "space-between" | "space-around" | undefined; -// readonly width?: string | number | undefined; -// readonly height?: string | number | undefined; -// readonly minWidth?: string | number | undefined; -// readonly minHeight?: string | number | undefined; -// readonly display?: "flex" | "none" | undefined; -// readonly borderStyle?: keyof cliBoxes.Boxes | undefined; -// readonly borderColor?: LiteralUnion | undefined; -// readonly margin?: number | undefined; -// readonly marginX?: number | undefined; -// readonly marginY?: number | undefined; -// readonly padding?: number | undefined; -// readonly paddingX?: number | undefined; -// readonly paddingY?: number | undefined; -// children?: React.ReactNode; -// key?: React.Key | null | undefined; -// } +import { Box, BoxProps, Text } from "ink"; +import type React from "react"; +import { getInnerBoxProps, getOuterBoxProps } from "../lib/boxProps"; export interface FrameLabelGroupProps { left?: (React.ReactNode | undefined | false)[]; @@ -91,7 +55,7 @@ export interface FrameProps extends BoxProps { bottomLabels?: FrameLabelGroupProps | false; } -export const Frame: React.FC = (props) => { +export const Frame: React.FC> = (props) => { const { topLabels, bottomLabels, ...boxProps } = props; const hasTopLabels = topLabels && Object.values(topLabels).some(Boolean); @@ -103,40 +67,8 @@ export const Frame: React.FC = (props) => { boxProps.paddingX ??= 1; if (hasTopLabels || hasBottomLabels) { - const outerBoxProps = pick(boxProps, [ - "position", - "marginLeft", - "marginRight", - "marginTop", - "marginBottom", - "margin", - "marginX", - "marginY", - "flexGrow", - "flexShrink", - "flexBasis", - "alignSelf", - "width", - "height", - "minWidth", - "minHeight", - "borderStyle", - "borderColor", - "key", - ]); - - const innerBoxProps = pick(boxProps, [ - "paddingLeft", - "paddingRight", - "paddingTop", - "paddingBottom", - "flexDirection", - "alignItems", - "justifyContent", - "padding", - "paddingX", - "paddingY", - ]); + const outerBoxProps = getOuterBoxProps(boxProps); + const innerBoxProps = getInnerBoxProps(boxProps); return ( diff --git a/packages/cli/src/components/HotkeyLabel.tsx b/packages/cli/src/components/HotkeyLabel.tsx index de054d0224db..04ef4bf9fdb5 100644 --- a/packages/cli/src/components/HotkeyLabel.tsx +++ b/packages/cli/src/components/HotkeyLabel.tsx @@ -1,7 +1,4 @@ -import { Text, useInput } from "ink"; -import type { ComponentPropsWithoutRef } from "react"; - -type TextProps = ComponentPropsWithoutRef; +import { Text, TextProps, useInput } from "ink"; export interface HotkeyLabelProps extends TextProps { label: string; diff --git a/packages/cli/src/components/ModalMessage.tsx b/packages/cli/src/components/ModalMessage.tsx index c4215d8d0c88..dc74eb749b1b 100644 --- a/packages/cli/src/components/ModalMessage.tsx +++ b/packages/cli/src/components/ModalMessage.tsx @@ -1,5 +1,19 @@ -import { Box, Text, TextProps, useInput } from "ink"; -import type { ReactNode } from "react"; +import { + Box, + DOMElement, + measureElement, + Text, + TextProps, + useInput, +} from "ink"; +import { + ReactNode, + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; import { Center } from "./Center"; export interface ModalMessageState { @@ -21,14 +35,46 @@ export const ModalMessage: React.FC< } }); + const ref = useRef(null); + const [height, setHeight] = useState(0); + const [text, setText] = useState(""); + + const updateSize = useCallback(() => { + if (ref.current) { + const { width, height } = measureElement(ref.current); + setHeight(height); + if ( + height === 0 || + width === 0 || + Number.isNaN(height) || + Number.isNaN(width) + ) { + setText(""); + } else { + const text = new Array(height) + .fill(" ".repeat(width)) + .join("\n"); + setText(text); + } + } + }, [ref.current]); + + useEffect(updateSize); + // useEffect(updateSize, [ref.current]); + useLayoutEffect(updateSize); + return (
+ + {text} + {props.children} diff --git a/packages/cli/src/components/VDivider.tsx b/packages/cli/src/components/VDivider.tsx index fd76aeb9cf3a..69c332713735 100644 --- a/packages/cli/src/components/VDivider.tsx +++ b/packages/cli/src/components/VDivider.tsx @@ -14,8 +14,12 @@ export const VDivider: React.FC = ({ useEffect(() => { if (ref.current) { - const height = measureElement(ref.current).height; - setText(character.repeat(height)); + const height = Math.max(1, measureElement(ref.current).height); + if (Number.isNaN(height)) { + setText(character); + } else { + setText(new Array(height).fill(character).join("\n")); + } } }); diff --git a/packages/cli/src/components/ZStack.tsx b/packages/cli/src/components/ZStack.tsx new file mode 100644 index 000000000000..6e4041fbe70d --- /dev/null +++ b/packages/cli/src/components/ZStack.tsx @@ -0,0 +1,67 @@ +import { Box, DOMElement, measureElement } from "ink"; +import { + Children, + PropsWithChildren, + useEffect, + useRef, + useState, +} from "react"; +import { getOuterBoxProps, OuterBoxProps } from "../lib/boxProps"; + +export interface LayerProps { + zIndex?: number; +} + +export const Layer: React.FC> = (props) => { + return <>{props.children}; +}; + +export interface ZStackProps extends OuterBoxProps {} + +export const ZStack: React.FC> = (props) => { + const { ...boxProps } = props; + const ref = useRef(null); + const [width, setWidth] = useState(0); + const [height, setHeight] = useState(0); + + // Always know the size of what we're rendering + useEffect(() => { + if (ref.current) { + const { width, height } = measureElement(ref.current); + setWidth(width); + setHeight(height); + } + }); + + const outerBoxProps = getOuterBoxProps(boxProps); + + const arrayChildren = Children.toArray(props.children); + for (const child of arrayChildren) { + if (typeof child !== "object" || !child || !("props" in child)) { + throw new Error("ZStack children must be elements"); + } + } + + const sortedChildren = (arrayChildren as React.ReactElement[]).sort( + (a, b) => (a.props.zIndex ?? 0) - (b.props.zIndex ?? 0), + ); + + return ( + + + {sortedChildren?.map((child, i) => ( + + {child.props.children} + + ))} + + + ); + + return
TODO
; +}; diff --git a/packages/cli/src/components/menu.tsx b/packages/cli/src/components/menu.tsx index d010c0c34e00..f034fd0b5863 100644 --- a/packages/cli/src/components/menu.tsx +++ b/packages/cli/src/components/menu.tsx @@ -1,14 +1,13 @@ -import { Box, Text, useInput } from "ink"; -import type { ComponentPropsWithoutRef } from "react"; +import { Box, BoxProps, Text, TextProps, useInput } from "ink"; export interface MenuProps { label?: string; - layoutProps?: ComponentPropsWithoutRef; - textProps?: ComponentPropsWithoutRef; + layoutProps?: BoxProps; + textProps?: TextProps; options: { input: string; label: string; - textProps?: ComponentPropsWithoutRef; + textProps?: TextProps; onSelect: () => void; }[]; } diff --git a/packages/cli/src/lib/boxProps.ts b/packages/cli/src/lib/boxProps.ts new file mode 100644 index 000000000000..e33e02569bc4 --- /dev/null +++ b/packages/cli/src/lib/boxProps.ts @@ -0,0 +1,76 @@ +import { pick } from "@zwave-js/shared/safe"; +import type { BoxProps } from "ink"; + +export type OuterBoxProps = Pick< + BoxProps, + | "position" + | "marginLeft" + | "marginRight" + | "marginTop" + | "marginBottom" + | "margin" + | "marginX" + | "marginY" + | "flexGrow" + | "flexShrink" + | "flexBasis" + | "alignSelf" + | "width" + | "height" + | "minWidth" + | "minHeight" + | "borderStyle" + | "borderColor" +>; + +export type InnerBoxProps = Pick< + BoxProps, + | "paddingLeft" + | "paddingRight" + | "paddingTop" + | "paddingBottom" + | "flexDirection" + | "alignItems" + | "justifyContent" + | "padding" + | "paddingX" + | "paddingY" +>; + +export function getOuterBoxProps(props: BoxProps): OuterBoxProps { + return pick(props, [ + "position", + "marginLeft", + "marginRight", + "marginTop", + "marginBottom", + "margin", + "marginX", + "marginY", + "flexGrow", + "flexShrink", + "flexBasis", + "alignSelf", + "width", + "height", + "minWidth", + "minHeight", + "borderStyle", + "borderColor", + ]); +} + +export function getInnerBoxProps(props: BoxProps): InnerBoxProps { + return pick(props, [ + "paddingLeft", + "paddingRight", + "paddingTop", + "paddingBottom", + "flexDirection", + "alignItems", + "justifyContent", + "padding", + "paddingX", + "paddingY", + ]); +} From c0d2b45c9dcd0cba1d9eb01b6799bd05f08d4d2d Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Thu, 2 Mar 2023 20:54:35 +0100 Subject: [PATCH 08/27] chore: upgrade ink to 4.0 (WIP) --- ...-devtools-core-npm-4.27.2-7a013e485e.patch | 9 + package.json | 5 +- packages/cli/build.sh | 33 ++ packages/cli/package.json | 16 +- packages/cli/src/cli.tsx | 38 +- packages/cli/src/components/Center.tsx | 2 +- packages/cli/src/components/Frame.tsx | 2 +- packages/cli/src/components/Log.tsx | 2 +- packages/cli/src/components/ModalMessage.tsx | 2 +- .../cli/src/components/StartingDriver.tsx | 14 +- packages/cli/src/components/USBPathInfo.tsx | 2 +- packages/cli/src/components/ZStack.tsx | 2 +- packages/cli/src/hooks/useStdoutDimensions.ts | 21 + packages/cli/src/lib/menu.tsx | 10 +- packages/cli/src/pages/Prepare.tsx | 12 +- packages/cli/tsconfig.build.json | 4 +- packages/cli/tsconfig.json | 4 +- packages/config/package.json | 2 +- packages/core/package.json | 2 +- packages/maintenance/package.json | 2 +- packages/nvmedit/package.json | 2 +- packages/serial/package.json | 2 +- packages/shared/package.json | 2 +- packages/testing/package.json | 2 +- packages/zwave-js/package.json | 2 +- yarn.lock | 395 ++++++++++-------- 26 files changed, 344 insertions(+), 245 deletions(-) create mode 100644 .yarn/patches/react-devtools-core-npm-4.27.2-7a013e485e.patch create mode 100755 packages/cli/build.sh create mode 100644 packages/cli/src/hooks/useStdoutDimensions.ts diff --git a/.yarn/patches/react-devtools-core-npm-4.27.2-7a013e485e.patch b/.yarn/patches/react-devtools-core-npm-4.27.2-7a013e485e.patch new file mode 100644 index 000000000000..72b6cd624642 --- /dev/null +++ b/.yarn/patches/react-devtools-core-npm-4.27.2-7a013e485e.patch @@ -0,0 +1,9 @@ +diff --git a/dist/backend.js b/dist/backend.js +index 3eecd3041e688f887bb491b793ba653e3d74a23a..9ad2645340f0dc4fe1e95802839980d9eac7c328 100644 +--- a/dist/backend.js ++++ b/dist/backend.js +@@ -1,3 +1,4 @@ ++var window = globalThis; + (function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); diff --git a/package.json b/package.json index 5b5297fb281d..b4ac11d512f4 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "cz-conventional-changelog": "^3.3.0", "del-cli": "^5.0.0", "esbuild": "0.15.7", - "esbuild-register": "^3.3.3", + "esbuild-register": "^3.4.2", "eslint": "^8.31.0", "eslint-config-prettier": "^8.6.0", "eslint-plugin-prettier": "^4.2.1", @@ -86,7 +86,8 @@ "resolutions": { "minimist": "^1.2.6", "colors": "1.4.0", - "yoga-layout-prebuilt@^1.9.6": "patch:yoga-layout-prebuilt@npm%3A1.10.0#./.yarn/patches/yoga-layout-prebuilt-npm-1.10.0-855b15449f.patch" + "yoga-layout-prebuilt@^1.9.6": "patch:yoga-layout-prebuilt@npm%3A1.10.0#./.yarn/patches/yoga-layout-prebuilt-npm-1.10.0-855b15449f.patch", + "react-devtools-core@^4.27.2": "patch:react-devtools-core@npm%3A4.27.2#./.yarn/patches/react-devtools-core-npm-4.27.2-7a013e485e.patch" }, "scripts": { "w": "yarn ts maintenance/watch.ts", diff --git a/packages/cli/build.sh b/packages/cli/build.sh new file mode 100755 index 000000000000..451186c58d47 --- /dev/null +++ b/packages/cli/build.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +DEFINE_NODEENV="--define:process.env.NODE_ENV=\"production\"" +MINIFY="--minify" + +while [[ $# -gt 0 ]]; do + case $1 in + --dev) + DEFINE_NODEENV="" + MINIFY="" + shift # past argument + ;; + -*|--*) + echo "Unknown option $1" + exit 1 + ;; + esac +done + +esbuild src/cli.tsx \ + $DEFINE_NODEENV \ + --outfile=build/cli.js \ + --platform=node \ + --target=node16 \ + --format=esm \ + --bundle \ + $MINIFY \ + --sourcemap \ + --external:zwave-js \ + --external:react-devtools-core \ + # Fix esbuild not being able to do dynamic require() in ESM mode + --banner:js="import { createRequire } from 'module'; const require = createRequire(import.meta.url);" + diff --git a/packages/cli/package.json b/packages/cli/package.json index 1e7dd82a0a63..e6ef8e79680b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -6,7 +6,8 @@ "publishConfig": { "access": "public" }, - "main": "build/cli.js", + "type": "module", + "module": "build/cli.js", "files": [ "bin/", "build/**/*.{js,d.ts,map}" @@ -32,8 +33,9 @@ "node": ">=14.13.0" }, "scripts": { - "build:dev": "esbuild src/cli.tsx --outfile=build/cli.js --platform=node --target=node16 --format=cjs --bundle --sourcemap --external:zwave-js", - "build": "esbuild --define:process.env.NODE_ENV=\\\"production\\\" src/cli.tsx --outfile=build/cli.js --platform=node --target=node16 --format=cjs --bundle --minify --sourcemap --external:zwave-js", + "ts": "yarn node --loader esbuild-register/loader -r esbuild-register", + "build": "./build.sh", + "build:dev": "./build.sh --dev", "clean": "del-cli build/ \"*.tsbuildinfo\"", "lint:ts": "eslint --ext .ts --rule \"prettier/prettier: off\" \"src/**/*.ts\"", "lint:ts:fix": "yarn run lint:ts --fix", @@ -54,12 +56,14 @@ "ansi-colors": "^4.1.3", "del-cli": "^5.0.0", "esbuild": "0.15.7", - "ink": "^3.2.0", - "ink-spinner": "^4.0.3", - "ink-text-input": "^4.0.3", + "esbuild-register": "^3.4.2", + "ink": "^4.0.0", + "ink-spinner": "^5.0.0", + "ink-text-input": "^5.0.0", "ink-use-stdout-dimensions": "^1.0.5", "prettier": "^2.8.1", "react": "^18.2.0", + "react-devtools-core": "^4.27.2", "typescript": "4.9.4", "winston": "^3.8.2" } diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 3bb0e6d91186..a5e7a1770544 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,25 +1,25 @@ import { Box, render, Text, useApp, useInput } from "ink"; -import useStdoutDimensions from "ink-use-stdout-dimensions"; import { useCallback, useState } from "react"; import type { Driver } from "zwave-js"; -import { ConfirmExit } from "./components/ConfirmExit"; -import { Frame } from "./components/Frame"; -import { Log } from "./components/Log"; -import { MainMenu } from "./components/MainMenu"; -import { ModalMessage, ModalMessageState } from "./components/ModalMessage"; -import { SetUSBPath } from "./components/setUSBPath"; -import { StartingDriverPage } from "./components/StartingDriver"; -import { VDivider } from "./components/VDivider"; -import { Layer, ZStack } from "./components/ZStack"; -import { Action, ActionsContext } from "./hooks/useActions"; -import { DialogsContext } from "./hooks/useDialogs"; -import { DriverContext } from "./hooks/useDriver"; -import { GlobalsContext } from "./hooks/useGlobals"; -import { MenuContext, useMenuItemSlots } from "./hooks/useMenu"; -import { CLIPage, NavigationContext } from "./hooks/useNavigation"; -import { createLogTransport, LinesBuffer } from "./lib/logging"; -import { defaultMenuItems } from "./lib/menu"; -import { PreparePage } from "./pages/Prepare"; +import { ConfirmExit } from "./components/ConfirmExit.js"; +import { Frame } from "./components/Frame.js"; +import { Log } from "./components/Log.js"; +import { MainMenu } from "./components/MainMenu.js"; +import { ModalMessage, ModalMessageState } from "./components/ModalMessage.js"; +import { SetUSBPath } from "./components/setUSBPath.js"; +import { StartingDriverPage } from "./components/StartingDriver.js"; +import { VDivider } from "./components/VDivider.js"; +import { Layer, ZStack } from "./components/ZStack.js"; +import { Action, ActionsContext } from "./hooks/useActions.js"; +import { DialogsContext } from "./hooks/useDialogs.js"; +import { DriverContext } from "./hooks/useDriver.js"; +import { GlobalsContext } from "./hooks/useGlobals.js"; +import { MenuContext, useMenuItemSlots } from "./hooks/useMenu.js"; +import { CLIPage, NavigationContext } from "./hooks/useNavigation.js"; +import { useStdoutDimensions } from "./hooks/useStdoutDimensions.js"; +import { createLogTransport, LinesBuffer } from "./lib/logging.js"; +import { defaultMenuItems } from "./lib/menu.js"; +import { PreparePage } from "./pages/Prepare.js"; process.on("unhandledRejection", (err) => { throw err; diff --git a/packages/cli/src/components/Center.tsx b/packages/cli/src/components/Center.tsx index e4d30718fce3..76937bf8c539 100644 --- a/packages/cli/src/components/Center.tsx +++ b/packages/cli/src/components/Center.tsx @@ -1,5 +1,5 @@ import { Box } from "ink"; -import type { OuterBoxProps } from "../lib/boxProps"; +import type { OuterBoxProps } from "../lib/boxProps.js"; export interface CenterProps extends Omit { diff --git a/packages/cli/src/components/Frame.tsx b/packages/cli/src/components/Frame.tsx index 15cb6d0818b3..65f546127ca4 100644 --- a/packages/cli/src/components/Frame.tsx +++ b/packages/cli/src/components/Frame.tsx @@ -1,6 +1,6 @@ import { Box, BoxProps, Text } from "ink"; import type React from "react"; -import { getInnerBoxProps, getOuterBoxProps } from "../lib/boxProps"; +import { getInnerBoxProps, getOuterBoxProps } from "../lib/boxProps.js"; export interface FrameLabelGroupProps { left?: (React.ReactNode | undefined | false)[]; diff --git a/packages/cli/src/components/Log.tsx b/packages/cli/src/components/Log.tsx index 03f79c05b2c0..29ae141b45d7 100644 --- a/packages/cli/src/components/Log.tsx +++ b/packages/cli/src/components/Log.tsx @@ -1,6 +1,6 @@ import { Box, DOMElement, measureElement, Text } from "ink"; import { useCallback, useEffect, useRef, useState } from "react"; -import type { LinesBuffer } from "../lib/logging"; +import type { LinesBuffer } from "../lib/logging.js"; export interface LogProps { buffer: LinesBuffer; diff --git a/packages/cli/src/components/ModalMessage.tsx b/packages/cli/src/components/ModalMessage.tsx index dc74eb749b1b..6c7683b4ffb1 100644 --- a/packages/cli/src/components/ModalMessage.tsx +++ b/packages/cli/src/components/ModalMessage.tsx @@ -14,7 +14,7 @@ import { useRef, useState, } from "react"; -import { Center } from "./Center"; +import { Center } from "./Center.js"; export interface ModalMessageState { message: ReactNode; diff --git a/packages/cli/src/components/StartingDriver.tsx b/packages/cli/src/components/StartingDriver.tsx index 96385f52d1a8..75905326097a 100644 --- a/packages/cli/src/components/StartingDriver.tsx +++ b/packages/cli/src/components/StartingDriver.tsx @@ -1,13 +1,13 @@ import { Box, Text } from "ink"; import Spinner from "ink-spinner"; import { useEffect } from "react"; -import { useDialogs } from "../hooks/useDialogs"; -import { useDriver } from "../hooks/useDriver"; -import { useGlobals } from "../hooks/useGlobals"; -import { useMenu } from "../hooks/useMenu"; -import { CLIPage, useNavigation } from "../hooks/useNavigation"; -import { startDriver } from "../lib/driver"; -import { toggleLogMenuItem } from "../lib/menu"; +import { useDialogs } from "../hooks/useDialogs.js"; +import { useDriver } from "../hooks/useDriver.js"; +import { useGlobals } from "../hooks/useGlobals.js"; +import { useMenu } from "../hooks/useMenu.js"; +import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; +import { startDriver } from "../lib/driver.js"; +import { toggleLogMenuItem } from "../lib/menu.js"; export const StartingDriverPage: React.FC = () => { useMenu([toggleLogMenuItem]); diff --git a/packages/cli/src/components/USBPathInfo.tsx b/packages/cli/src/components/USBPathInfo.tsx index 35326e4d5cb3..f541bb4f82b0 100644 --- a/packages/cli/src/components/USBPathInfo.tsx +++ b/packages/cli/src/components/USBPathInfo.tsx @@ -1,5 +1,5 @@ import { Text } from "ink"; -import { useGlobals } from "../hooks/useGlobals"; +import { useGlobals } from "../hooks/useGlobals.js"; export const USBPathInfo: React.FC = () => { const { usbPath } = useGlobals(); diff --git a/packages/cli/src/components/ZStack.tsx b/packages/cli/src/components/ZStack.tsx index 6e4041fbe70d..beaea76f014f 100644 --- a/packages/cli/src/components/ZStack.tsx +++ b/packages/cli/src/components/ZStack.tsx @@ -6,7 +6,7 @@ import { useRef, useState, } from "react"; -import { getOuterBoxProps, OuterBoxProps } from "../lib/boxProps"; +import { getOuterBoxProps, OuterBoxProps } from "../lib/boxProps.js"; export interface LayerProps { zIndex?: number; diff --git a/packages/cli/src/hooks/useStdoutDimensions.ts b/packages/cli/src/hooks/useStdoutDimensions.ts new file mode 100644 index 000000000000..c03c9f9b6b00 --- /dev/null +++ b/packages/cli/src/hooks/useStdoutDimensions.ts @@ -0,0 +1,21 @@ +// copied from https://github.com/cameronhunter/ink-monorepo which isn't compatible with ink@4 +import { useStdout } from "ink"; +import { useEffect, useState } from "react"; + +export function useStdoutDimensions(): [number, number] { + const { stdout } = useStdout(); + const [dimensions, setDimensions] = useState<[number, number]>([ + stdout.columns, + stdout.rows, + ]); + + useEffect(() => { + const handler = () => setDimensions([stdout.columns, stdout.rows]); + stdout.on("resize", handler); + return () => { + stdout.off("resize", handler); + }; + }, [stdout]); + + return dimensions; +} diff --git a/packages/cli/src/lib/menu.tsx b/packages/cli/src/lib/menu.tsx index 240adbea5fe2..5cc09fc4f76c 100644 --- a/packages/cli/src/lib/menu.tsx +++ b/packages/cli/src/lib/menu.tsx @@ -1,10 +1,10 @@ import { Text } from "ink"; import { libVersion } from "zwave-js"; -import { HotkeyLabel } from "../components/HotkeyLabel"; -import { USBPathInfo } from "../components/USBPathInfo"; -import { useGlobals } from "../hooks/useGlobals"; -import type { MenuItem } from "../hooks/useMenu"; -import { CLIPage, useNavigation } from "../hooks/useNavigation"; +import { HotkeyLabel } from "../components/HotkeyLabel.js"; +import { USBPathInfo } from "../components/USBPathInfo.js"; +import { useGlobals } from "../hooks/useGlobals.js"; +import type { MenuItem } from "../hooks/useMenu.js"; +import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; const ToggleLogMenuItem: React.FC = () => { const { logEnabled, setLogEnabled } = useGlobals(); diff --git a/packages/cli/src/pages/Prepare.tsx b/packages/cli/src/pages/Prepare.tsx index 7896ce8a6aad..3baa109e2e9f 100644 --- a/packages/cli/src/pages/Prepare.tsx +++ b/packages/cli/src/pages/Prepare.tsx @@ -1,11 +1,11 @@ import { Box, Text } from "ink"; import { useState } from "react"; -import { HotkeyLabel } from "../components/HotkeyLabel"; -import { Logo } from "../components/Logo"; -import { useGlobals } from "../hooks/useGlobals"; -import { MenuItem, useMenu } from "../hooks/useMenu"; -import { CLIPage, useNavigation } from "../hooks/useNavigation"; -import { toggleLogMenuItem } from "../lib/menu"; +import { HotkeyLabel } from "../components/HotkeyLabel.js"; +import { Logo } from "../components/Logo.js"; +import { useGlobals } from "../hooks/useGlobals.js"; +import { MenuItem, useMenu } from "../hooks/useMenu.js"; +import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; +import { toggleLogMenuItem } from "../lib/menu.js"; export interface PreparePageProps { // TODO: diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json index 01333ab04dbc..d98d198db002 100644 --- a/packages/cli/tsconfig.build.json +++ b/packages/cli/tsconfig.build.json @@ -4,7 +4,9 @@ "compilerOptions": { "rootDir": "src", "outDir": "build", - "jsx": "react-jsx" + "jsx": "react-jsx", + "module": "Node16", + "moduleResolution": "node16" }, "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["src/**/*.test.ts"] diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 0b343ce64925..534f810ff30a 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -2,7 +2,9 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "jsx": "react-jsx" + "jsx": "react-jsx", + "module": "Node16", + "moduleResolution": "node16" }, "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["build/**", "node_modules/**"] diff --git a/packages/config/package.json b/packages/config/package.json index 4f703351a068..8b99c1d16774 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -93,7 +93,7 @@ "comment-json": "^4.2.3", "del-cli": "^5.0.0", "esbuild": "0.15.7", - "esbuild-register": "^3.3.3", + "esbuild-register": "^3.4.2", "js-levenshtein": "^1.1.6", "pegjs": "^0.10.0", "prettier": "^2.8.1", diff --git a/packages/core/package.json b/packages/core/package.json index 5b367eda8d3a..fb6ef5609170 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -97,7 +97,7 @@ "ava": "^4.3.3", "del-cli": "^5.0.0", "esbuild": "0.15.7", - "esbuild-register": "^3.3.3", + "esbuild-register": "^3.4.2", "prettier": "^2.8.1", "sinon": "^14.0.0", "typescript": "4.9.4" diff --git a/packages/maintenance/package.json b/packages/maintenance/package.json index bfe0e4637f3e..10bf4d3884ce 100644 --- a/packages/maintenance/package.json +++ b/packages/maintenance/package.json @@ -52,7 +52,7 @@ "clipboardy": "^2.3.0", "del-cli": "^5.0.0", "esbuild": "0.15.7", - "esbuild-register": "^3.3.3", + "esbuild-register": "^3.4.2", "execa": "^5.1.1", "fs-extra": "^10.1.0", "globrex": "^0.1.2", diff --git a/packages/nvmedit/package.json b/packages/nvmedit/package.json index 10f67bc643bb..4cd471254727 100644 --- a/packages/nvmedit/package.json +++ b/packages/nvmedit/package.json @@ -81,7 +81,7 @@ "ava": "^4.3.3", "del-cli": "^5.0.0", "esbuild": "0.15.7", - "esbuild-register": "^3.3.3", + "esbuild-register": "^3.4.2", "prettier": "^2.8.1", "typescript": "4.9.4" } diff --git a/packages/serial/package.json b/packages/serial/package.json index 3876c21683c6..437c8b76975f 100644 --- a/packages/serial/package.json +++ b/packages/serial/package.json @@ -88,7 +88,7 @@ "ava": "^4.3.3", "del-cli": "^5.0.0", "esbuild": "0.15.7", - "esbuild-register": "^3.3.3", + "esbuild-register": "^3.4.2", "prettier": "^2.8.1", "sinon": "^14.0.0", "typescript": "4.9.4" diff --git a/packages/shared/package.json b/packages/shared/package.json index 261151f21621..2801a08e58a0 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -73,7 +73,7 @@ "ava": "^4.3.3", "del-cli": "^5.0.0", "esbuild": "0.15.7", - "esbuild-register": "^3.3.3", + "esbuild-register": "^3.4.2", "prettier": "^2.8.1", "sinon": "^14.0.0", "typescript": "4.9.4" diff --git a/packages/testing/package.json b/packages/testing/package.json index b255aa1c2b03..ae577b0fc09a 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -53,7 +53,7 @@ "@types/triple-beam": "^1.3.2", "del-cli": "^5.0.0", "esbuild": "0.15.7", - "esbuild-register": "^3.3.3", + "esbuild-register": "^3.4.2", "prettier": "^2.8.1", "triple-beam": "*", "typescript": "4.9.4", diff --git a/packages/zwave-js/package.json b/packages/zwave-js/package.json index 73bce2727be4..acd1cf214fd4 100644 --- a/packages/zwave-js/package.json +++ b/packages/zwave-js/package.json @@ -151,7 +151,7 @@ "ava": "^4.3.3", "del-cli": "^5.0.0", "esbuild": "0.15.7", - "esbuild-register": "^3.3.3", + "esbuild-register": "^3.4.2", "mockdate": "^3.0.5", "prettier": "^2.8.1", "proxyquire": "^2.1.3", diff --git a/yarn.lock b/yarn.lock index 30ad30bfb9b0..ec0b23c5d3b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1812,12 +1812,14 @@ __metadata: ansi-colors: ^4.1.3 del-cli: ^5.0.0 esbuild: 0.15.7 - ink: ^3.2.0 - ink-spinner: ^4.0.3 - ink-text-input: ^4.0.3 + esbuild-register: ^3.4.2 + ink: ^4.0.0 + ink-spinner: ^5.0.0 + ink-text-input: ^5.0.0 ink-use-stdout-dimensions: ^1.0.5 prettier: ^2.8.1 react: ^18.2.0 + react-devtools-core: ^4.27.2 typescript: 4.9.4 winston: ^3.8.2 zwave-js: "workspace:*" @@ -1850,7 +1852,7 @@ __metadata: comment-json: ^4.2.3 del-cli: ^5.0.0 esbuild: 0.15.7 - esbuild-register: ^3.3.3 + esbuild-register: ^3.4.2 fs-extra: ^10.1.0 js-levenshtein: ^1.1.6 json-logic-js: ^2.0.2 @@ -1883,7 +1885,7 @@ __metadata: dayjs: ^1.11.5 del-cli: ^5.0.0 esbuild: 0.15.7 - esbuild-register: ^3.3.3 + esbuild-register: ^3.4.2 logform: ^2.4.2 nrf-intel-hex: ^1.3.0 prettier: ^2.8.1 @@ -1949,7 +1951,7 @@ __metadata: clipboardy: ^2.3.0 del-cli: ^5.0.0 esbuild: 0.15.7 - esbuild-register: ^3.3.3 + esbuild-register: ^3.4.2 execa: ^5.1.1 fs-extra: ^10.1.0 globrex: ^0.1.2 @@ -1977,7 +1979,7 @@ __metadata: ava: ^4.3.3 del-cli: ^5.0.0 esbuild: 0.15.7 - esbuild-register: ^3.3.3 + esbuild-register: ^3.4.2 fs-extra: ^10.1.0 prettier: ^2.8.1 reflect-metadata: ^0.1.13 @@ -2030,7 +2032,7 @@ __metadata: cz-conventional-changelog: ^3.3.0 del-cli: ^5.0.0 esbuild: 0.15.7 - esbuild-register: ^3.3.3 + esbuild-register: ^3.4.2 eslint: ^8.31.0 eslint-config-prettier: ^8.6.0 eslint-plugin-prettier: ^4.2.1 @@ -2069,7 +2071,7 @@ __metadata: ava: ^4.3.3 del-cli: ^5.0.0 esbuild: 0.15.7 - esbuild-register: ^3.3.3 + esbuild-register: ^3.4.2 prettier: ^2.8.1 serialport: ^10.4.0 sinon: ^14.0.0 @@ -2090,7 +2092,7 @@ __metadata: ava: ^4.3.3 del-cli: ^5.0.0 esbuild: 0.15.7 - esbuild-register: ^3.3.3 + esbuild-register: ^3.4.2 fs-extra: ^10.1.0 prettier: ^2.8.1 sinon: ^14.0.0 @@ -2112,7 +2114,7 @@ __metadata: ansi-colors: ^4.1.3 del-cli: ^5.0.0 esbuild: 0.15.7 - esbuild-register: ^3.3.3 + esbuild-register: ^3.4.2 prettier: ^2.8.1 triple-beam: "*" typescript: 4.9.4 @@ -2308,6 +2310,15 @@ __metadata: languageName: node linkType: hard +"ansi-escapes@npm:^6.0.0": + version: 6.0.0 + resolution: "ansi-escapes@npm:6.0.0" + dependencies: + type-fest: ^3.0.0 + checksum: 1ddc0b27b1d040c3c703c9cd80ee0a103817e2f9fa8f1adf0c66e970b57543ec60effdb0bd1a396ed7182bca3b1a0d8fda60ec61fee862d353db81b1c3650a78 + languageName: node + linkType: hard + "ansi-regex@npm:^2.0.0": version: 2.1.1 resolution: "ansi-regex@npm:2.1.1" @@ -2481,13 +2492,6 @@ __metadata: languageName: node linkType: hard -"astral-regex@npm:^2.0.0": - version: 2.0.0 - resolution: "astral-regex@npm:2.0.0" - checksum: 876231688c66400473ba505731df37ea436e574dd524520294cc3bbc54ea40334865e01fa0d074d74d036ee874ee7e62f486ea38bc421ee8e6a871c06f011766 - languageName: node - linkType: hard - "async@npm:^3.2.3": version: 3.2.3 resolution: "async@npm:3.2.3" @@ -2509,10 +2513,10 @@ __metadata: languageName: node linkType: hard -"auto-bind@npm:4.0.0": - version: 4.0.0 - resolution: "auto-bind@npm:4.0.0" - checksum: 00cad71cce5742faccb7dd65c1b55ebc4f45add4b0c9a1547b10b05bab22813230133b0c892c67ba3eb969a4524710c5e43cc45c72898ec84e56f3a596e7a04f +"auto-bind@npm:^5.0.1": + version: 5.0.1 + resolution: "auto-bind@npm:5.0.1" + checksum: 44a6d8d040c4382e761922f8fa1b044e18ddefbc855fecee0c76ec6b4e6fc74adda21026bc86e190833e05f52b4b6615372c2a83a734858f8395b1e2a98b253a languageName: node linkType: hard @@ -2823,6 +2827,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^5.2.0": + version: 5.2.0 + resolution: "chalk@npm:5.2.0" + checksum: 03d8060277de6cf2fd567dc25fcf770593eb5bb85f460ce443e49255a30ff1242edd0c90a06a03803b0466ff0687a939b41db1757bec987113e83de89a003caa + languageName: node + linkType: hard + "chardet@npm:^0.7.0": version: 0.7.0 resolution: "chardet@npm:0.7.0" @@ -2863,10 +2874,10 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^2.0.0": - version: 2.0.0 - resolution: "ci-info@npm:2.0.0" - checksum: 3b374666a85ea3ca43fa49aa3a048d21c9b475c96eb13c133505d2324e7ae5efd6a454f41efe46a152269e9b6a00c9edbe63ec7fa1921957165aae16625acd67 +"ci-info@npm:^3.2.0": + version: 3.8.0 + resolution: "ci-info@npm:3.8.0" + checksum: d0a4d3160497cae54294974a7246202244fff031b0a6ea20dd57b10ec510aa17399c41a1b0982142c105f3255aff2173e5c0dd7302ee1b2f28ba3debda375098 languageName: node linkType: hard @@ -2907,10 +2918,10 @@ __metadata: languageName: node linkType: hard -"cli-boxes@npm:^2.2.0": - version: 2.2.1 - resolution: "cli-boxes@npm:2.2.1" - checksum: be79f8ec23a558b49e01311b39a1ea01243ecee30539c880cf14bf518a12e223ef40c57ead0cb44f509bffdffc5c129c746cd50d863ab879385370112af4f585 +"cli-boxes@npm:^3.0.0": + version: 3.0.0 + resolution: "cli-boxes@npm:3.0.0" + checksum: 637d84419d293a9eac40a1c8c96a2859e7d98b24a1a317788e13c8f441be052fc899480c6acab3acc82eaf1bccda6b7542d7cdcf5c9c3cc39227175dc098d5b2 languageName: node linkType: hard @@ -2932,6 +2943,15 @@ __metadata: languageName: node linkType: hard +"cli-cursor@npm:^4.0.0": + version: 4.0.0 + resolution: "cli-cursor@npm:4.0.0" + dependencies: + restore-cursor: ^4.0.0 + checksum: ab3f3ea2076e2176a1da29f9d64f72ec3efad51c0960898b56c8a17671365c26e67b735920530eaf7328d61f8bd41c27f46b9cf6e4e10fe2fa44b5e8c0e392cc + languageName: node + linkType: hard + "cli-highlight@npm:^2.1.11": version: 2.1.11 resolution: "cli-highlight@npm:2.1.11" @@ -2948,13 +2968,6 @@ __metadata: languageName: node linkType: hard -"cli-spinners@npm:^2.3.0": - version: 2.7.0 - resolution: "cli-spinners@npm:2.7.0" - checksum: a9afaf73f58d1f951fb23742f503631b3cf513f43f4c7acb1b640100eb76bfa16efbcd1994d149ffc6603a6d75dd3d4a516a76f125f90dce437de9b16fd0ee6f - languageName: node - linkType: hard - "cli-spinners@npm:^2.5.0": version: 2.6.1 resolution: "cli-spinners@npm:2.6.1" @@ -2962,13 +2975,10 @@ __metadata: languageName: node linkType: hard -"cli-truncate@npm:^2.1.0": - version: 2.1.0 - resolution: "cli-truncate@npm:2.1.0" - dependencies: - slice-ansi: ^3.0.0 - string-width: ^4.2.0 - checksum: bf1e4e6195392dc718bf9cd71f317b6300dc4a9191d052f31046b8773230ece4fa09458813bf0e3455a5e68c0690d2ea2c197d14a8b85a7b5e01c97f4b5feb5d +"cli-spinners@npm:^2.7.0": + version: 2.7.0 + resolution: "cli-spinners@npm:2.7.0" + checksum: a9afaf73f58d1f951fb23742f503631b3cf513f43f4c7acb1b640100eb76bfa16efbcd1994d149ffc6603a6d75dd3d4a516a76f125f90dce437de9b16fd0ee6f languageName: node linkType: hard @@ -3032,15 +3042,6 @@ __metadata: languageName: node linkType: hard -"code-excerpt@npm:^3.0.0": - version: 3.0.0 - resolution: "code-excerpt@npm:3.0.0" - dependencies: - convert-to-spaces: ^1.0.1 - checksum: fa3a8ed15967076a43a4093b0c824cf0ada15d9aab12ea3c028851b72a69b56495aac1eadf18c3b6ae4baf0a95bb1e1faa9dbeeb0a2b2b5ae058da23328e9dd8 - languageName: node - linkType: hard - "code-excerpt@npm:^4.0.0": version: 4.0.0 resolution: "code-excerpt@npm:4.0.0" @@ -3298,13 +3299,6 @@ __metadata: languageName: node linkType: hard -"convert-to-spaces@npm:^1.0.1": - version: 1.0.2 - resolution: "convert-to-spaces@npm:1.0.2" - checksum: e73f2ae39eb2b184f0796138eaab9c088b03b94937377d31be5b2282aef6a6ccce6b46f51bd99b3b7dfc70f516e2a6b16c0dd911883bfadf8d1073f462480224 - languageName: node - linkType: hard - "convert-to-spaces@npm:^2.0.1": version: 2.0.1 resolution: "convert-to-spaces@npm:2.0.1" @@ -3935,12 +3929,14 @@ __metadata: languageName: node linkType: hard -"esbuild-register@npm:^3.3.3": - version: 3.3.3 - resolution: "esbuild-register@npm:3.3.3" +"esbuild-register@npm:^3.4.2": + version: 3.4.2 + resolution: "esbuild-register@npm:3.4.2" + dependencies: + debug: ^4.3.4 peerDependencies: esbuild: ">=0.12 <1" - checksum: f43fecb9f5c48fcf859a0b3681368af8987c299c822da9a996b90f453b9b2226b9a90c7dbaec0968a86b53ee524af0d8b4ed828866c29bfede5c176834bc0f2b + checksum: f65d1ccb58b1ccbba376efb1fc023abe22731d9b79eead1b0120e57d4413318f063696257a5af637b527fa1d3f009095aa6edb1bf6ff69d637a9ab281fb727b3 languageName: node linkType: hard @@ -5227,28 +5223,28 @@ __metadata: languageName: node linkType: hard -"ink-spinner@npm:^4.0.3": - version: 4.0.3 - resolution: "ink-spinner@npm:4.0.3" +"ink-spinner@npm:^5.0.0": + version: 5.0.0 + resolution: "ink-spinner@npm:5.0.0" dependencies: - cli-spinners: ^2.3.0 + cli-spinners: ^2.7.0 peerDependencies: - ink: ">=3.0.5" - react: ">=16.8.2" - checksum: d3785d688dd1ba19fb7a850b7a2c1dd8b2d06e4c77e6a7cc6c5bbd366a5a721e9ea45d036447016a9028f7519994077ce603a60a43a495e17b7b443b8a513ddc + ink: ">=4.0.0" + react: ">=18.0.0" + checksum: 88e547ff56ac8ee31239daef43b03ca2797eb20cc338ad25aba8e8fbe2cb322ea212494f8c545f327d345051be50542e1a27fdee3758a32a1b4a5db5308cad63 languageName: node linkType: hard -"ink-text-input@npm:^4.0.3": - version: 4.0.3 - resolution: "ink-text-input@npm:4.0.3" +"ink-text-input@npm:^5.0.0": + version: 5.0.0 + resolution: "ink-text-input@npm:5.0.0" dependencies: - chalk: ^4.1.0 - type-fest: ^0.15.1 + chalk: ^5.2.0 + type-fest: ^3.6.1 peerDependencies: - ink: ^3.0.0-3 - react: ^16.5.2 || ^17.0.0 - checksum: 2d309ec8ca386010d467822e317389e3c60b764fd04091df063a45c31f43104fd9f4a4e71a928a2c3c3cca461a9b8a526e90439616760f0f3726507132abbac5 + ink: ^4.0.0 + react: ^18.0.0 + checksum: e7b68bfa8fcbd616111ceb034525e968cad72ec5a8126bcaec78c09ddde17a3986ac83fe53fd765cca5666e02cf11b7dd6209ab45b50edb26ca9e047abec5ef4 languageName: node linkType: hard @@ -5262,40 +5258,42 @@ __metadata: languageName: node linkType: hard -"ink@npm:^3.2.0": - version: 3.2.0 - resolution: "ink@npm:3.2.0" +"ink@npm:^4.0.0": + version: 4.0.0 + resolution: "ink@npm:4.0.0" dependencies: - ansi-escapes: ^4.2.1 - auto-bind: 4.0.0 - chalk: ^4.1.0 - cli-boxes: ^2.2.0 - cli-cursor: ^3.1.0 - cli-truncate: ^2.1.0 - code-excerpt: ^3.0.0 - indent-string: ^4.0.0 - is-ci: ^2.0.0 - lodash: ^4.17.20 - patch-console: ^1.0.0 - react-devtools-core: ^4.19.1 - react-reconciler: ^0.26.2 - scheduler: ^0.20.2 - signal-exit: ^3.0.2 - slice-ansi: ^3.0.0 - stack-utils: ^2.0.2 - string-width: ^4.2.2 + ansi-escapes: ^6.0.0 + auto-bind: ^5.0.1 + chalk: ^5.2.0 + cli-boxes: ^3.0.0 + cli-cursor: ^4.0.0 + cli-truncate: ^3.1.0 + code-excerpt: ^4.0.0 + indent-string: ^5.0.0 + is-ci: ^3.0.1 + lodash-es: ^4.17.21 + patch-console: ^2.0.0 + react-reconciler: ^0.29.0 + scheduler: ^0.23.0 + signal-exit: ^3.0.7 + slice-ansi: ^5.0.0 + stack-utils: ^2.0.6 + string-width: ^5.1.2 type-fest: ^0.12.0 - widest-line: ^3.1.0 - wrap-ansi: ^6.2.0 - ws: ^7.5.5 + widest-line: ^4.0.1 + wrap-ansi: ^8.1.0 + ws: ^8.12.0 yoga-layout-prebuilt: ^1.9.6 peerDependencies: - "@types/react": ">=16.8.0" - react: ">=16.8.0" + "@types/react": ">=18.0.0" + react: ">=18.0.0" + react-devtools-core: ^4.19.1 peerDependenciesMeta: "@types/react": optional: true - checksum: 35f1b733b94bf12cc0bf7acb4d3fcba9d961ede15cee9c64a7325606b74cee78e1009eaffbac127f4d7d28e758d8259dea8d0850bfacb991b8d93632f41d3fa2 + react-devtools-core: + optional: true + checksum: 7cd120ed6589e17ea4f8c0abcfeac37e3afb3d6c5b82342a7996f0a15a1a4d1ba933f926ae8e5539dd8adea3e6cfbcf5d4025d82c1d917fbcf8fc56f92213f63 languageName: node linkType: hard @@ -5387,14 +5385,14 @@ __metadata: languageName: node linkType: hard -"is-ci@npm:^2.0.0": - version: 2.0.0 - resolution: "is-ci@npm:2.0.0" +"is-ci@npm:^3.0.1": + version: 3.0.1 + resolution: "is-ci@npm:3.0.1" dependencies: - ci-info: ^2.0.0 + ci-info: ^3.2.0 bin: is-ci: bin.js - checksum: 77b869057510f3efa439bbb36e9be429d53b3f51abd4776eeea79ab3b221337fe1753d1e50058a9e2c650d38246108beffb15ccfd443929d77748d8c0cc90144 + checksum: 192c66dc7826d58f803ecae624860dccf1899fc1f3ac5505284c0a5cf5f889046ffeb958fa651e5725d5705c5bcb14f055b79150ea5fcad7456a9569de60260e languageName: node linkType: hard @@ -5917,6 +5915,13 @@ __metadata: languageName: node linkType: hard +"lodash-es@npm:^4.17.21": + version: 4.17.21 + resolution: "lodash-es@npm:4.17.21" + checksum: 05cbffad6e2adbb331a4e16fbd826e7faee403a1a04873b82b42c0f22090f280839f85b95393f487c1303c8a3d2a010048bf06151a6cbe03eee4d388fb0a12d2 + languageName: node + linkType: hard + "lodash.get@npm:^4.4.2": version: 4.4.2 resolution: "lodash.get@npm:4.4.2" @@ -6682,7 +6687,7 @@ __metadata: languageName: node linkType: hard -"object-assign@npm:^4.0.1, object-assign@npm:^4.1.0, object-assign@npm:^4.1.1": +"object-assign@npm:^4.0.1, object-assign@npm:^4.1.0": version: 4.1.1 resolution: "object-assign@npm:4.1.1" checksum: fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f @@ -6971,10 +6976,10 @@ __metadata: languageName: node linkType: hard -"patch-console@npm:^1.0.0": - version: 1.0.0 - resolution: "patch-console@npm:1.0.0" - checksum: 8cd738aa470f2e9463fca35da6a19403384ac555004f698ddd3dfdb69135ab60fe9bd2edd1dbdd8c09d92c0a2190fd0f7337fe48123013baf8ffec8532885a3a +"patch-console@npm:^2.0.0": + version: 2.0.0 + resolution: "patch-console@npm:2.0.0" + checksum: 10e7d382cc1cf930a2114a822cdc816109a1147bcbc4881ca4fa2ad0228a60cf14d53f815fce3164f25851fea71db4026ae8271e4026b42b0a6e92ddc074d4c2 languageName: node linkType: hard @@ -7249,7 +7254,7 @@ __metadata: languageName: node linkType: hard -"react-devtools-core@npm:^4.19.1": +"react-devtools-core@npm:4.27.2": version: 4.27.2 resolution: "react-devtools-core@npm:4.27.2" dependencies: @@ -7259,16 +7264,25 @@ __metadata: languageName: node linkType: hard -"react-reconciler@npm:^0.26.2": - version: 0.26.2 - resolution: "react-reconciler@npm:0.26.2" +"react-devtools-core@patch:react-devtools-core@npm%3A4.27.2#./.yarn/patches/react-devtools-core-npm-4.27.2-7a013e485e.patch::locator=%40zwave-js%2Frepo%40workspace%3A.": + version: 4.27.2 + resolution: "react-devtools-core@patch:react-devtools-core@npm%3A4.27.2#./.yarn/patches/react-devtools-core-npm-4.27.2-7a013e485e.patch::version=4.27.2&hash=d25436&locator=%40zwave-js%2Frepo%40workspace%3A." + dependencies: + shell-quote: ^1.6.1 + ws: ^7 + checksum: 82c07bcad9c4eeae57db301a276515bc2bbaafe8fa766ed3686547601ab34070375199aab551a1a0cde04663d1a3bddb9ab906f4fc0efc34f5f4a68474697c97 + languageName: node + linkType: hard + +"react-reconciler@npm:^0.29.0": + version: 0.29.0 + resolution: "react-reconciler@npm:0.29.0" dependencies: loose-envify: ^1.1.0 - object-assign: ^4.1.1 - scheduler: ^0.20.2 + scheduler: ^0.23.0 peerDependencies: - react: ^17.0.2 - checksum: 2ebceace56f547f51eaf142becefef9cca980eae4f42d90ee5a966f54a375f5082d78b71b00c40bbd9bca69e0e0f698c7d4e81cc7373437caa19831fddc1d01b + react: ^18.2.0 + checksum: 730db9cf451fe6b5102042fda32a029c73ef970f758707d8a0f07e11e9cc262bc973987464d920b519da99f7e20bb1fef1d1c6b05ff993ad12d59d63b004a2ab languageName: node linkType: hard @@ -7614,6 +7628,16 @@ __metadata: languageName: node linkType: hard +"restore-cursor@npm:^4.0.0": + version: 4.0.0 + resolution: "restore-cursor@npm:4.0.0" + dependencies: + onetime: ^5.1.0 + signal-exit: ^3.0.2 + checksum: 5b675c5a59763bf26e604289eab35711525f11388d77f409453904e1e69c0d37ae5889295706b2c81d23bd780165084d040f9b68fffc32cc921519031c4fa4af + languageName: node + linkType: hard + "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -7715,13 +7739,12 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.20.2": - version: 0.20.2 - resolution: "scheduler@npm:0.20.2" +"scheduler@npm:^0.23.0": + version: 0.23.0 + resolution: "scheduler@npm:0.23.0" dependencies: loose-envify: ^1.1.0 - object-assign: ^4.1.1 - checksum: c4b35cf967c8f0d3e65753252d0f260271f81a81e427241295c5a7b783abf4ea9e905f22f815ab66676f5313be0a25f47be582254db8f9241b259213e999b8fc + checksum: d79192eeaa12abef860c195ea45d37cbf2bbf5f66e3c4dcd16f54a7da53b17788a70d109ee3d3dde1a0fd50e6a8fc171f4300356c5aee4fc0171de526bf35f8a languageName: node linkType: hard @@ -7906,17 +7929,6 @@ __metadata: languageName: node linkType: hard -"slice-ansi@npm:^3.0.0": - version: 3.0.0 - resolution: "slice-ansi@npm:3.0.0" - dependencies: - ansi-styles: ^4.0.0 - astral-regex: ^2.0.0 - is-fullwidth-code-point: ^3.0.0 - checksum: 5ec6d022d12e016347e9e3e98a7eb2a592213a43a65f1b61b74d2c78288da0aded781f665807a9f3876b9daa9ad94f64f77d7633a0458876c3a4fdc4eb223f24 - languageName: node - linkType: hard - "slice-ansi@npm:^5.0.0": version: 5.0.0 resolution: "slice-ansi@npm:5.0.0" @@ -8038,21 +8050,21 @@ __metadata: languageName: node linkType: hard -"stack-utils@npm:^2.0.2": - version: 2.0.6 - resolution: "stack-utils@npm:2.0.6" +"stack-utils@npm:^2.0.5": + version: 2.0.5 + resolution: "stack-utils@npm:2.0.5" dependencies: escape-string-regexp: ^2.0.0 - checksum: 052bf4d25bbf5f78e06c1d5e67de2e088b06871fa04107ca8d3f0e9d9263326e2942c8bedee3545795fc77d787d443a538345eef74db2f8e35db3558c6f91ff7 + checksum: 76b69da0f5b48a34a0f93c98ee2a96544d2c4ca2557f7eef5ddb961d3bdc33870b46f498a84a7c4f4ffb781df639840e7ebf6639164ed4da5e1aeb659615b9c7 languageName: node linkType: hard -"stack-utils@npm:^2.0.5": - version: 2.0.5 - resolution: "stack-utils@npm:2.0.5" +"stack-utils@npm:^2.0.6": + version: 2.0.6 + resolution: "stack-utils@npm:2.0.6" dependencies: escape-string-regexp: ^2.0.0 - checksum: 76b69da0f5b48a34a0f93c98ee2a96544d2c4ca2557f7eef5ddb961d3bdc33870b46f498a84a7c4f4ffb781df639840e7ebf6639164ed4da5e1aeb659615b9c7 + checksum: 052bf4d25bbf5f78e06c1d5e67de2e088b06871fa04107ca8d3f0e9d9263326e2942c8bedee3545795fc77d787d443a538345eef74db2f8e35db3558c6f91ff7 languageName: node linkType: hard @@ -8084,29 +8096,29 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^4.0.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": - version: 4.2.3 - resolution: "string-width@npm:4.2.3" +"string-width@npm:^4.1.0, string-width@npm:^4.2.0": + version: 4.2.2 + resolution: "string-width@npm:4.2.2" dependencies: emoji-regex: ^8.0.0 is-fullwidth-code-point: ^3.0.0 - strip-ansi: ^6.0.1 - checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb + strip-ansi: ^6.0.0 + checksum: 343e089b0e66e0f72aab4ad1d9b6f2c9cc5255844b0c83fd9b53f2a3b3fd0421bdd6cb05be96a73117eb012db0887a6c1d64ca95aaa50c518e48980483fea0ab languageName: node linkType: hard -"string-width@npm:^4.1.0, string-width@npm:^4.2.0": - version: 4.2.2 - resolution: "string-width@npm:4.2.2" +"string-width@npm:^4.2.3": + version: 4.2.3 + resolution: "string-width@npm:4.2.3" dependencies: emoji-regex: ^8.0.0 is-fullwidth-code-point: ^3.0.0 - strip-ansi: ^6.0.0 - checksum: 343e089b0e66e0f72aab4ad1d9b6f2c9cc5255844b0c83fd9b53f2a3b3fd0421bdd6cb05be96a73117eb012db0887a6c1d64ca95aaa50c518e48980483fea0ab + strip-ansi: ^6.0.1 + checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb languageName: node linkType: hard -"string-width@npm:^5.0.0": +"string-width@npm:^5.0.0, string-width@npm:^5.0.1, string-width@npm:^5.1.2": version: 5.1.2 resolution: "string-width@npm:5.1.2" dependencies: @@ -8664,13 +8676,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^0.15.1": - version: 0.15.1 - resolution: "type-fest@npm:0.15.1" - checksum: a1a0cdbd7f802d9784324f185df055739e97424ecb60914e9025574a4bc07e4a063c152e4510ebf5989de8a263220de1f6b5cf1b05f0b333dbd5b21d9b4a271b - languageName: node - linkType: hard - "type-fest@npm:^0.18.0": version: 0.18.1 resolution: "type-fest@npm:0.18.1" @@ -8713,6 +8718,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^3.0.0, type-fest@npm:^3.6.1": + version: 3.6.1 + resolution: "type-fest@npm:3.6.1" + checksum: f7e39bf6b74a883661ec8642707f49c33cfcdc6221e1ba36b1d329c1cf301d87351b3ca0839b894cbfe47dc62140c0ce47e69c88f76800b678e0b67b7fe826e6 + languageName: node + linkType: hard + "typescript@npm:4.9.4": version: 4.9.4 resolution: "typescript@npm:4.9.4" @@ -8945,12 +8957,12 @@ __metadata: languageName: node linkType: hard -"widest-line@npm:^3.1.0": - version: 3.1.0 - resolution: "widest-line@npm:3.1.0" +"widest-line@npm:^4.0.1": + version: 4.0.1 + resolution: "widest-line@npm:4.0.1" dependencies: - string-width: ^4.0.0 - checksum: 03db6c9d0af9329c37d74378ff1d91972b12553c7d72a6f4e8525fe61563fa7adb0b9d6e8d546b7e059688712ea874edd5ded475999abdeedf708de9849310e0 + string-width: ^5.0.1 + checksum: 64c48cf27171221be5f86fc54b94dd29879165bdff1a7aa92dde723d9a8c99fb108312768a5d62c8c2b80b701fa27bbd36a1ddc58367585cd45c0db7920a0cba languageName: node linkType: hard @@ -9005,17 +9017,6 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^6.2.0": - version: 6.2.0 - resolution: "wrap-ansi@npm:6.2.0" - dependencies: - ansi-styles: ^4.0.0 - string-width: ^4.1.0 - strip-ansi: ^6.0.0 - checksum: 6cd96a410161ff617b63581a08376f0cb9162375adeb7956e10c8cd397821f7eb2a6de24eb22a0b28401300bf228c86e50617cd568209b5f6775b93c97d2fe3a - languageName: node - linkType: hard - "wrap-ansi@npm:^7.0.0": version: 7.0.0 resolution: "wrap-ansi@npm:7.0.0" @@ -9027,6 +9028,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^8.1.0": + version: 8.1.0 + resolution: "wrap-ansi@npm:8.1.0" + dependencies: + ansi-styles: ^6.1.0 + string-width: ^5.0.1 + strip-ansi: ^7.0.1 + checksum: 371733296dc2d616900ce15a0049dca0ef67597d6394c57347ba334393599e800bab03c41d4d45221b6bc967b8c453ec3ae4749eff3894202d16800fdfe0e238 + languageName: node + linkType: hard + "wrappy@npm:1": version: 1.0.2 resolution: "wrappy@npm:1.0.2" @@ -9044,7 +9056,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7, ws@npm:^7.5.5": +"ws@npm:^7": version: 7.5.9 resolution: "ws@npm:7.5.9" peerDependencies: @@ -9059,6 +9071,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.12.0": + version: 8.12.1 + resolution: "ws@npm:8.12.1" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 97301c1c4d838fc81bd413f370f75c12aabe44527b31323b761eab3043a9ecb7e32ffd668548382c9a6a5ad3a1c3a9249608e8338e6b939f2f9540f1e21970b5 + languageName: node + linkType: hard + "xml2js@npm:^0.4.23": version: 0.4.23 resolution: "xml2js@npm:0.4.23" @@ -9268,7 +9295,7 @@ __metadata: ava: ^4.3.3 del-cli: ^5.0.0 esbuild: 0.15.7 - esbuild-register: ^3.3.3 + esbuild-register: ^3.4.2 execa: ^5.1.1 fs-extra: ^10.1.0 mockdate: ^3.0.5 From 9891d8bac7f48beb939ec157ef2e73342a4360da Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Thu, 2 Mar 2023 21:06:21 +0100 Subject: [PATCH 09/27] chore: wtf ESM --- packages/cli/build.sh | 3 +-- packages/cli/src/lib/driver.ts | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/cli/build.sh b/packages/cli/build.sh index 451186c58d47..f115a319a6e3 100755 --- a/packages/cli/build.sh +++ b/packages/cli/build.sh @@ -28,6 +28,5 @@ esbuild src/cli.tsx \ --sourcemap \ --external:zwave-js \ --external:react-devtools-core \ + --banner:js="import { createRequire } from 'module'; var require = require || createRequire(import.meta.url);" # Fix esbuild not being able to do dynamic require() in ESM mode - --banner:js="import { createRequire } from 'module'; const require = createRequire(import.meta.url);" - diff --git a/packages/cli/src/lib/driver.ts b/packages/cli/src/lib/driver.ts index 53382d5580f8..d6dc3141343e 100644 --- a/packages/cli/src/lib/driver.ts +++ b/packages/cli/src/lib/driver.ts @@ -1,11 +1,13 @@ import path from "path"; import type winston from "winston"; -import { Driver } from "zwave-js"; + +// This line is the boundary between ESM and CommonJS. We need it because zwave-js uses __dirname and __filename +const { Driver } = require("zwave-js") as typeof import("zwave-js"); export async function startDriver( port: string, logTransport: winston.transport, -): Promise { +): Promise { const driver = new Driver(port, { logConfig: { // Do not log to console or file From 6a7021defd4a3135609bfe4926c896822788e4f8 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Thu, 2 Mar 2023 23:03:44 +0100 Subject: [PATCH 10/27] feat: stuff --- packages/cli/src/cli.tsx | 267 ++++++++++-------- packages/cli/src/components/ConfirmExit.tsx | 23 -- packages/cli/src/components/Frame.tsx | 2 +- packages/cli/src/components/HDivider.tsx | 31 ++ packages/cli/src/components/HotkeyLabel.tsx | 80 +++++- packages/cli/src/components/Log.tsx | 18 +- packages/cli/src/components/MainMenu.tsx | 9 - packages/cli/src/components/ModalMessage.tsx | 50 +--- .../cli/src/components/StartingDriver.tsx | 42 ++- packages/cli/src/components/USBPathInfo.tsx | 6 +- packages/cli/src/hooks/useDriver.ts | 15 +- packages/cli/src/hooks/useGlobals.ts | 5 +- packages/cli/src/hooks/useNavigation.ts | 10 +- packages/cli/src/lib/driver.ts | 26 +- packages/cli/src/lib/logging.ts | 5 + packages/cli/src/lib/menu.tsx | 39 ++- packages/cli/src/pages/ConfirmExit.tsx | 28 ++ packages/cli/src/pages/MainMenu.tsx | 17 ++ packages/cli/src/pages/Prepare.tsx | 48 ++-- 19 files changed, 449 insertions(+), 272 deletions(-) delete mode 100644 packages/cli/src/components/ConfirmExit.tsx create mode 100644 packages/cli/src/components/HDivider.tsx delete mode 100644 packages/cli/src/components/MainMenu.tsx create mode 100644 packages/cli/src/pages/ConfirmExit.tsx create mode 100644 packages/cli/src/pages/MainMenu.tsx diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 3bb0e6d91186..14b54a94ff79 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,17 +1,15 @@ -import { Box, render, Text, useApp, useInput } from "ink"; +import { Box, render, Text, useInput } from "ink"; import useStdoutDimensions from "ink-use-stdout-dimensions"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import type { Driver } from "zwave-js"; -import { ConfirmExit } from "./components/ConfirmExit"; import { Frame } from "./components/Frame"; +import { HDivider } from "./components/HDivider"; import { Log } from "./components/Log"; -import { MainMenu } from "./components/MainMenu"; import { ModalMessage, ModalMessageState } from "./components/ModalMessage"; import { SetUSBPath } from "./components/setUSBPath"; import { StartingDriverPage } from "./components/StartingDriver"; import { VDivider } from "./components/VDivider"; -import { Layer, ZStack } from "./components/ZStack"; -import { Action, ActionsContext } from "./hooks/useActions"; +import { ActionsContext } from "./hooks/useActions"; import { DialogsContext } from "./hooks/useDialogs"; import { DriverContext } from "./hooks/useDriver"; import { GlobalsContext } from "./hooks/useGlobals"; @@ -19,6 +17,8 @@ import { MenuContext, useMenuItemSlots } from "./hooks/useMenu"; import { CLIPage, NavigationContext } from "./hooks/useNavigation"; import { createLogTransport, LinesBuffer } from "./lib/logging"; import { defaultMenuItems } from "./lib/menu"; +import { ConfirmExitPage } from "./pages/ConfirmExit"; +import { MainMenuPage } from "./pages/MainMenu"; import { PreparePage } from "./pages/Prepare"; process.on("unhandledRejection", (err) => { @@ -30,15 +30,48 @@ const MIN_ROWS = 30; const logBuffer = new LinesBuffer(10000); const logTransport = createLogTransport(logBuffer.stream); +const clearLog = () => logBuffer.clear(); + const CLI: React.FC = () => { - const { exit } = useApp(); const [columns, rows] = useStdoutDimensions(); + // Switch between horizontal and vertical layout + const [layout, setLayout] = useState<"horizontal" | "vertical">( + columns >= 180 ? "horizontal" : "vertical", + ); + useEffect(() => { + setLayout(columns >= 180 ? "horizontal" : "vertical"); + }, [columns, setLayout]); + const [usbPath, setUSBPath] = useState("/dev/ttyACM0"); const [driver, setDriver] = useState(); - const [logEnabled, setLogEnabled] = useState(false); + const destroyDriver = useCallback(async () => { + if (driver) { + await driver.destroy(); + } + }, [driver]); + + const [logVisible, setLogVisible] = useState(false); const [cliPage, setCLIPage] = useState(CLIPage.Prepare); + const [prevCliPage, setPrevCLIPage] = useState(); + + const navigate = useCallback( + (to: CLIPage) => { + setPrevCLIPage(cliPage); + setCLIPage(to); + }, + [cliPage, setCLIPage, setPrevCLIPage], + ); + + const back = useCallback(() => { + if (prevCliPage) { + setCLIPage(prevCliPage); + setPrevCLIPage(undefined); + return true; + } + return false; + }, [prevCliPage, setCLIPage, setPrevCLIPage]); const [modalMessage, setModalMessage] = useState(); const showError = useCallback( @@ -54,11 +87,11 @@ const CLI: React.FC = () => { const [menuItemSlots, updateMenuItems] = useMenuItemSlots(defaultMenuItems); // Prevent the app from exiting automatically - useInput((input, key) => { + useInput(() => { // nothing to do }); - const performAction = useCallback(async (action: Action) => { + const performAction = useCallback(async () => { // if (action.type === "navigate") { // setCLIPage(action.to); // } @@ -85,117 +118,133 @@ const CLI: React.FC = () => { return ( - - - - <> - - {cliPage === - CLIPage.Prepare && ( - - )} + + {!modalMessage && ( + <> + + {cliPage === + CLIPage.Prepare && ( + + )} - {cliPage === - CLIPage.SetUSBPath && ( - - setCLIPage( - CLIPage.Prepare, - ) - } - onSubmit={( - path, - ) => { - setUSBPath( - path, - ); - setCLIPage( - CLIPage.Prepare, - ); - }} - /> - )} + {cliPage === + CLIPage.SetUSBPath && ( + + setCLIPage( + CLIPage.Prepare, + ) + } + onSubmit={(path) => { + setUSBPath(path); + setCLIPage( + CLIPage.Prepare, + ); + }} + /> + )} - {cliPage === - CLIPage.StartingDriver && ( - - )} + {cliPage === + CLIPage.StartingDriver && ( + + )} - {cliPage === - CLIPage.MainMenu && ( - - )} + {cliPage === + CLIPage.MainMenu && ( + + )} - {cliPage === - CLIPage.ConfirmExit && ( - - setCLIPage( - CLIPage.Prepare, - ) - } - onExit={async () => { - if (driver) { - await driver.destroy(); - } - exit(); - }} - /> + {cliPage === + CLIPage.ConfirmExit && ( + + )} + + {logVisible && ( + + {layout === "horizontal" ? ( + + ) : ( + )} + - {logEnabled && ( - - - - - )} - - - + )} + + )} {modalMessage && ( - - - setModalMessage(undefined) - } - color={modalMessage.color} - > - {modalMessage.message} - - + + setModalMessage(undefined) + } + color={modalMessage.color} + > + {modalMessage.message} + )} - + @@ -205,8 +254,4 @@ const CLI: React.FC = () => { ); }; -// console.clear(); -const { waitUntilExit } = render(); -waitUntilExit().then(() => { - // console.clear(); -}); +render(); diff --git a/packages/cli/src/components/ConfirmExit.tsx b/packages/cli/src/components/ConfirmExit.tsx deleted file mode 100644 index 713071d90734..000000000000 --- a/packages/cli/src/components/ConfirmExit.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Box, Text, useInput } from "ink"; - -export interface ConfirmExitProps { - onExit: () => void; - onCancel: () => void; -} - -export const ConfirmExit: React.FC = (props) => { - useInput((input, key) => { - if (key.return) props.onExit(); - else if (key.escape) props.onCancel(); - }); - - return ( - - Are you sure you want to exit? - - Press RETURN to exit, or{" "} - ESCAPE to cancel. - - - ); -}; diff --git a/packages/cli/src/components/Frame.tsx b/packages/cli/src/components/Frame.tsx index 15cb6d0818b3..e1954918938a 100644 --- a/packages/cli/src/components/Frame.tsx +++ b/packages/cli/src/components/Frame.tsx @@ -18,7 +18,7 @@ const FrameLabels: React.FC<{ 0 ? 1 : 0} + marginLeft={i > 0 ? 2 : 0} > {label} diff --git a/packages/cli/src/components/HDivider.tsx b/packages/cli/src/components/HDivider.tsx new file mode 100644 index 000000000000..503aeed72d98 --- /dev/null +++ b/packages/cli/src/components/HDivider.tsx @@ -0,0 +1,31 @@ +import { Box, measureElement, Text, TextProps, type DOMElement } from "ink"; +import React, { useEffect, useRef, useState } from "react"; + +export interface HDividerProps extends TextProps { + character?: string; +} + +export const HDivider: React.FC = ({ + character = "─", + ...textProps +}) => { + const ref = useRef(null); + const [text, setText] = useState(character); + + useEffect(() => { + if (ref.current) { + const width = Math.max(1, measureElement(ref.current).width); + if (Number.isNaN(width)) { + setText(character); + } else { + setText(character.repeat(width)); + } + } + }); + + return ( + + {text} + + ); +}; diff --git a/packages/cli/src/components/HotkeyLabel.tsx b/packages/cli/src/components/HotkeyLabel.tsx index 04ef4bf9fdb5..4f18583d2d36 100644 --- a/packages/cli/src/components/HotkeyLabel.tsx +++ b/packages/cli/src/components/HotkeyLabel.tsx @@ -1,40 +1,83 @@ import { Text, TextProps, useInput } from "ink"; export interface HotkeyLabelProps extends TextProps { - label: string; + label?: string; hotkey?: string; + modifiers?: Modifiers; onPress?: () => void; } +type Modifiers = ("ctrl" | "shift")[]; + +const specialKeys: Record = { + upArrow: "↑", + downArrow: "↓", + leftArrow: "←", + rightArrow: "→", + pageDown: "PgDn", + pageUp: "PgUp", + return: "↲", + escape: "", + tab: "↹", + backspace: "⤆", + delete: "Del", +}; + +function modifiersToString(modifiers: Modifiers | undefined = []): string { + let ret = ""; + if (modifiers.includes("ctrl")) ret += "Ctrl+"; + if (modifiers.includes("shift")) ret += "⇧+"; + return ret; +} + +function renderHotkey(hotkey: string, modifiers: Modifiers | undefined) { + if (hotkey in specialKeys) { + return modifiersToString(modifiers) + specialKeys[hotkey]; + } else { + return modifiersToString(modifiers) + hotkey.toUpperCase(); + } +} + export const HotkeyLabel: React.FC = (props) => { - const { label, hotkey, ...textProps } = props; + const { hotkey, label, modifiers, ...textProps } = props; // Apply some default text props - textProps.color ??= "green"; + textProps.color ??= "red"; textProps.bold ??= true; const { color, children, ...rest } = textProps; useInput((input, key) => { - if (hotkey && input === hotkey) { + if ( + hotkey && + (input === hotkey || + (hotkey in specialKeys && (key as any)[hotkey])) && + (!modifiers || modifiers.every((mod) => key[mod])) + ) { props.onPress?.(); } }); + if (!label) { + if (hotkey) { + return ( + {renderHotkey(hotkey, modifiers)} + ); + } else { + throw new Error("At least one of label or hotkey must be provided"); + } + } + if (!hotkey) { return {label}; - } else if (hotkey.length !== 1) { - throw new Error("Hotkey must be a single character"); + } else if (hotkey.length !== 1 && !(hotkey in specialKeys)) { + throw new Error("Hotkey must be a single character or a special key"); } - const hotkeyIndex = label.toLowerCase().indexOf(hotkey.toLowerCase()); - - if (hotkeyIndex === -1) { - return ( - - {label} ({hotkey}) - - ); - } else { + const hotkeyIndex = + hotkey.length === 1 && !modifiers?.length + ? label.toLowerCase().indexOf(hotkey.toLowerCase()) + : -1; + if (hotkeyIndex >= 0) { return ( {label.slice(0, hotkeyIndex)} @@ -44,5 +87,12 @@ export const HotkeyLabel: React.FC = (props) => { {label.slice(hotkeyIndex + 1)} ); + } else { + return ( + + {label}{" "} + ({renderHotkey(hotkey, modifiers)}) + + ); } }; diff --git a/packages/cli/src/components/Log.tsx b/packages/cli/src/components/Log.tsx index 03f79c05b2c0..bc1285cd604f 100644 --- a/packages/cli/src/components/Log.tsx +++ b/packages/cli/src/components/Log.tsx @@ -1,6 +1,7 @@ -import { Box, DOMElement, measureElement, Text } from "ink"; +import { Box, DOMElement, measureElement, Spacer, Text } from "ink"; import { useCallback, useEffect, useRef, useState } from "react"; import type { LinesBuffer } from "../lib/logging"; +import { HotkeyLabel } from "./HotkeyLabel"; export interface LogProps { buffer: LinesBuffer; @@ -44,8 +45,21 @@ export const Log: React.FC = (props) => { }, [logHeight]); return ( - + {log} + + + + + + + ); }; diff --git a/packages/cli/src/components/MainMenu.tsx b/packages/cli/src/components/MainMenu.tsx deleted file mode 100644 index ce87e9930d14..000000000000 --- a/packages/cli/src/components/MainMenu.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Text } from "ink"; - -export interface MainMenuProps { - // TODO: -} - -export const MainMenu: React.FC = (props) => { - return TODO; -}; diff --git a/packages/cli/src/components/ModalMessage.tsx b/packages/cli/src/components/ModalMessage.tsx index dc74eb749b1b..c4215d8d0c88 100644 --- a/packages/cli/src/components/ModalMessage.tsx +++ b/packages/cli/src/components/ModalMessage.tsx @@ -1,19 +1,5 @@ -import { - Box, - DOMElement, - measureElement, - Text, - TextProps, - useInput, -} from "ink"; -import { - ReactNode, - useCallback, - useEffect, - useLayoutEffect, - useRef, - useState, -} from "react"; +import { Box, Text, TextProps, useInput } from "ink"; +import type { ReactNode } from "react"; import { Center } from "./Center"; export interface ModalMessageState { @@ -35,46 +21,14 @@ export const ModalMessage: React.FC< } }); - const ref = useRef(null); - const [height, setHeight] = useState(0); - const [text, setText] = useState(""); - - const updateSize = useCallback(() => { - if (ref.current) { - const { width, height } = measureElement(ref.current); - setHeight(height); - if ( - height === 0 || - width === 0 || - Number.isNaN(height) || - Number.isNaN(width) - ) { - setText(""); - } else { - const text = new Array(height) - .fill(" ".repeat(width)) - .join("\n"); - setText(text); - } - } - }, [ref.current]); - - useEffect(updateSize); - // useEffect(updateSize, [ref.current]); - useLayoutEffect(updateSize); - return (
- - {text} - {props.children} diff --git a/packages/cli/src/components/StartingDriver.tsx b/packages/cli/src/components/StartingDriver.tsx index 96385f52d1a8..bcc91a36510f 100644 --- a/packages/cli/src/components/StartingDriver.tsx +++ b/packages/cli/src/components/StartingDriver.tsx @@ -1,21 +1,37 @@ import { Box, Text } from "ink"; import Spinner from "ink-spinner"; -import { useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; +import type { Driver } from "zwave-js"; import { useDialogs } from "../hooks/useDialogs"; import { useDriver } from "../hooks/useDriver"; import { useGlobals } from "../hooks/useGlobals"; import { useMenu } from "../hooks/useMenu"; import { CLIPage, useNavigation } from "../hooks/useNavigation"; import { startDriver } from "../lib/driver"; -import { toggleLogMenuItem } from "../lib/menu"; +import { exitMenuItem, toggleLogMenuItem } from "../lib/menu"; export const StartingDriverPage: React.FC = () => { - useMenu([toggleLogMenuItem]); + useMenu([toggleLogMenuItem, exitMenuItem]); - const { usbPath, logTransport } = useGlobals(); - const [driver, setDriver] = useDriver(); - const [navigate] = useNavigation(); + const { usbPath, logTransport, clearLog } = useGlobals(); + const { driver, setDriver } = useDriver(); + const { navigate } = useNavigation(); const { showError } = useDialogs(); + const [message, setMessage] = useState("starting driver"); + + const onError = useCallback( + (e: Error) => { + showError(e.message); + }, + [showError], + ); + const onDriverReady = useCallback((driver: Driver) => { + navigate(CLIPage.MainMenu); + }, []); + const onBootloaderReady = useCallback((driver: Driver) => { + setMessage("driver stuck in bootloader mode"); + // TODO + }, []); // When opening this page, try to start the driver useEffect(() => { @@ -29,9 +45,15 @@ export const StartingDriverPage: React.FC = () => { (async () => { try { - const driver = await startDriver(usbPath, logTransport); + clearLog(); + const driver = await startDriver(usbPath, { + logTransport, + onError, + onDriverReady, + onBootloaderReady, + }); setDriver(driver); - navigate(CLIPage.MainMenu); + setMessage("initializing"); } catch (e: any) { navigate(CLIPage.Prepare); showError(e.message); @@ -44,8 +66,8 @@ export const StartingDriverPage: React.FC = () => { - - {" starting driver"} + {" "} + {message} ); diff --git a/packages/cli/src/components/USBPathInfo.tsx b/packages/cli/src/components/USBPathInfo.tsx index 35326e4d5cb3..74a047f58175 100644 --- a/packages/cli/src/components/USBPathInfo.tsx +++ b/packages/cli/src/components/USBPathInfo.tsx @@ -3,9 +3,9 @@ import { useGlobals } from "../hooks/useGlobals"; export const USBPathInfo: React.FC = () => { const { usbPath } = useGlobals(); - return ( + return usbPath ? ( - USB Path: {usbPath || "(none)"} + {usbPath} - ); + ) : null; }; diff --git a/packages/cli/src/hooks/useDriver.ts b/packages/cli/src/hooks/useDriver.ts index 83a1a67755f5..be5bf9810a6d 100644 --- a/packages/cli/src/hooks/useDriver.ts +++ b/packages/cli/src/hooks/useDriver.ts @@ -4,14 +4,19 @@ import type { Driver } from "zwave-js"; interface IDriverContext { driver: Driver; setDriver: (driver: Driver) => void; + destroyDriver: () => Promise; } export const DriverContext = React.createContext({} as any); -export const useDriver = (): readonly [ - driver: Driver, - setDriver: (driver: Driver) => void, -] => { +export const useDriver = () => { const { driver, setDriver } = React.useContext(DriverContext); - return [driver, setDriver]; + const destroyDriver = React.useCallback(async () => { + if (!driver) return; + await driver.destroy(); + // @ts-expect-error + setDriver(undefined); + }, [driver]); + + return { driver, setDriver, destroyDriver }; }; diff --git a/packages/cli/src/hooks/useGlobals.ts b/packages/cli/src/hooks/useGlobals.ts index 0f8abd5026bc..d8747f810121 100644 --- a/packages/cli/src/hooks/useGlobals.ts +++ b/packages/cli/src/hooks/useGlobals.ts @@ -4,8 +4,9 @@ import type winston from "winston"; interface IGlobalsContext { usbPath: string; logTransport: winston.transport; - logEnabled: boolean; - setLogEnabled: React.Dispatch>; + logVisible: boolean; + setLogVisible: React.Dispatch>; + clearLog: () => void; } export const GlobalsContext = React.createContext({} as any); diff --git a/packages/cli/src/hooks/useNavigation.ts b/packages/cli/src/hooks/useNavigation.ts index 88956334e9e9..247fa199848e 100644 --- a/packages/cli/src/hooks/useNavigation.ts +++ b/packages/cli/src/hooks/useNavigation.ts @@ -9,18 +9,14 @@ export enum CLIPage { } interface INavigationContext { + previousPage?: CLIPage; currentPage: CLIPage; navigate: (page: CLIPage) => void; + back: () => boolean; } export const NavigationContext = React.createContext( {} as any, ); -export const useNavigation = (): readonly [ - navigate: (page: CLIPage) => void, - currentPage: CLIPage, -] => { - const { currentPage, navigate } = React.useContext(NavigationContext); - return [navigate, currentPage]; -}; +export const useNavigation = () => React.useContext(NavigationContext); diff --git a/packages/cli/src/lib/driver.ts b/packages/cli/src/lib/driver.ts index 53382d5580f8..1f485deeacb6 100644 --- a/packages/cli/src/lib/driver.ts +++ b/packages/cli/src/lib/driver.ts @@ -2,16 +2,23 @@ import path from "path"; import type winston from "winston"; import { Driver } from "zwave-js"; +export interface StartDriverOptions { + logTransport: winston.transport; + onDriverReady: (driver: Driver) => void; + onBootloaderReady: (driver: Driver) => void; + onError: (error: Error) => void; +} + export async function startDriver( port: string, - logTransport: winston.transport, + options: StartDriverOptions, ): Promise { const driver = new Driver(port, { logConfig: { // Do not log to console or file enabled: false, // But log to our own transport - transports: [logTransport], + transports: [options.logTransport], }, securityKeys: { S0_Legacy: Buffer.from("0102030405060708090a0b0c0d0e0f10", "hex"), @@ -33,14 +40,13 @@ export async function startDriver( lockDir: path.join(__dirname, "cache/locks"), }, allowBootloaderOnly: true, - }) - .on("error", console.error) - .once("driver ready", async () => { - // Test code goes here - }) - .once("bootloader ready", async () => { - // What to do when stuck in the bootloader - }); + }); + + driver + .on("error", options.onError) + .once("driver ready", () => options.onDriverReady(driver)) + .once("bootloader ready", () => options.onBootloaderReady(driver)); + await driver.start(); return driver; diff --git a/packages/cli/src/lib/logging.ts b/packages/cli/src/lib/logging.ts index e4fe0a4cf142..39360c74484e 100644 --- a/packages/cli/src/lib/logging.ts +++ b/packages/cli/src/lib/logging.ts @@ -58,4 +58,9 @@ export class LinesBuffer extends TypedEventEmitter { public getView(start: number, end: number): readonly string[] { return this._lines.slice(start, end); } + + public clear(): void { + this._lines.splice(0, this._lines.length); + this.emit("change"); + } } diff --git a/packages/cli/src/lib/menu.tsx b/packages/cli/src/lib/menu.tsx index 240adbea5fe2..7649e9e4f12e 100644 --- a/packages/cli/src/lib/menu.tsx +++ b/packages/cli/src/lib/menu.tsx @@ -2,19 +2,20 @@ import { Text } from "ink"; import { libVersion } from "zwave-js"; import { HotkeyLabel } from "../components/HotkeyLabel"; import { USBPathInfo } from "../components/USBPathInfo"; +import { useDriver } from "../hooks/useDriver"; import { useGlobals } from "../hooks/useGlobals"; import type { MenuItem } from "../hooks/useMenu"; import { CLIPage, useNavigation } from "../hooks/useNavigation"; const ToggleLogMenuItem: React.FC = () => { - const { logEnabled, setLogEnabled } = useGlobals(); + const { logVisible, setLogVisible } = useGlobals(); return ( { - setLogEnabled((e) => !e); + setLogVisible((e) => !e); }} /> ); @@ -28,13 +29,13 @@ export const toggleLogMenuItem: MenuItem = { // ===================================================================== const ExitMenuItem: React.FC = () => { - const [navigate] = useNavigation(); + const { navigate } = useNavigation(); return ( navigate(CLIPage.ConfirmExit)} /> ); @@ -49,6 +50,29 @@ export const exitMenuItem: MenuItem = { // ===================================================================== +const DestroyDriverMenuItem: React.FC = () => { + const { navigate } = useNavigation(); + const { driver, destroyDriver } = useDriver(); + + return ( + { + await destroyDriver(); + navigate(CLIPage.Prepare); + }} + /> + ); +}; + +export const destroyDriverMenuItem: MenuItem = { + location: "bottomRight", + item: , +}; + +// ===================================================================== + export const defaultMenuItems: MenuItem[] = [ { location: "topLeft", @@ -63,8 +87,7 @@ export const defaultMenuItems: MenuItem[] = [ ), }, { - location: "topRight", + location: "topLeft", item: , }, - exitMenuItem, ]; diff --git a/packages/cli/src/pages/ConfirmExit.tsx b/packages/cli/src/pages/ConfirmExit.tsx new file mode 100644 index 000000000000..d73b56e2617d --- /dev/null +++ b/packages/cli/src/pages/ConfirmExit.tsx @@ -0,0 +1,28 @@ +import { Box, Text, useApp, useInput } from "ink"; +import { useDriver } from "../hooks/useDriver"; +import { useNavigation } from "../hooks/useNavigation"; + +export const ConfirmExitPage: React.FC = () => { + const { exit } = useApp(); + const { destroyDriver } = useDriver(); + const { back } = useNavigation(); + + useInput(async (input, key) => { + if (key.return) { + await destroyDriver(); + exit(); + } else if (key.escape) { + back(); + } + }); + + return ( + + Are you sure you want to exit? + + Press RETURN to exit, or{" "} + ESCAPE to cancel. + + + ); +}; diff --git a/packages/cli/src/pages/MainMenu.tsx b/packages/cli/src/pages/MainMenu.tsx new file mode 100644 index 000000000000..dd4a340c2dc7 --- /dev/null +++ b/packages/cli/src/pages/MainMenu.tsx @@ -0,0 +1,17 @@ +import { Text } from "ink"; +import { useMenu } from "../hooks/useMenu"; +import { + destroyDriverMenuItem, + exitMenuItem, + toggleLogMenuItem, +} from "../lib/menu"; + +export interface MainMenuPageProps { + // TODO: +} + +export const MainMenuPage: React.FC = (props) => { + useMenu([toggleLogMenuItem, destroyDriverMenuItem, exitMenuItem]); + + return TODO; +}; diff --git a/packages/cli/src/pages/Prepare.tsx b/packages/cli/src/pages/Prepare.tsx index 7896ce8a6aad..85a3b3b530f7 100644 --- a/packages/cli/src/pages/Prepare.tsx +++ b/packages/cli/src/pages/Prepare.tsx @@ -3,9 +3,9 @@ import { useState } from "react"; import { HotkeyLabel } from "../components/HotkeyLabel"; import { Logo } from "../components/Logo"; import { useGlobals } from "../hooks/useGlobals"; -import { MenuItem, useMenu } from "../hooks/useMenu"; +import { useMenu } from "../hooks/useMenu"; import { CLIPage, useNavigation } from "../hooks/useNavigation"; -import { toggleLogMenuItem } from "../lib/menu"; +import { exitMenuItem, toggleLogMenuItem } from "../lib/menu"; export interface PreparePageProps { // TODO: @@ -13,26 +13,26 @@ export interface PreparePageProps { export const PreparePage: React.FC = (props) => { const { usbPath } = useGlobals(); - const [navigate] = useNavigation(); + const { navigate } = useNavigation(); const [visible, setVisible] = useState(false); - const startDriverMenuItem: MenuItem = { - location: "bottomLeft", - item: ( - { - navigate(CLIPage.StartingDriver); - }} - /> - ), - visible: !!usbPath, - }; + // const startDriverMenuItem: MenuItem = { + // location: "bottomLeft", + // item: ( + // { + // navigate(CLIPage.StartingDriver); + // }} + // /> + // ), + // visible: !!usbPath, + // }; useMenu([ - startDriverMenuItem, + // startDriverMenuItem, toggleLogMenuItem, { location: "bottomRight", @@ -46,6 +46,7 @@ export const PreparePage: React.FC = (props) => { /> ), }, + exitMenuItem, ]); return ( @@ -53,7 +54,18 @@ export const PreparePage: React.FC = (props) => { {usbPath ? ( - Ready to start the driver. + + Ready to + { + navigate(CLIPage.StartingDriver); + }} + /> + the driver. + ) : ( Select a USB path in the options, then start the driver. From a7fbdbdbce611732e021c49d07d868baa12fdcf1 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Fri, 3 Mar 2023 15:18:09 +0100 Subject: [PATCH 11/27] feat: device table (WIP) --- packages/cli/package.json | 1 + packages/cli/src/cli.tsx | 5 +- .../cli/src/components/CommandPalette.tsx | 31 ++++++++++ packages/cli/src/components/HDivider.tsx | 4 +- packages/cli/src/components/HotkeyLabel.tsx | 2 +- packages/cli/src/components/VDivider.tsx | 4 +- packages/cli/src/pages/Devices.tsx | 60 +++++++++++++++++++ packages/cli/src/pages/MainMenu.tsx | 54 +++++++++++++++-- yarn.lock | 15 ++++- 9 files changed, 167 insertions(+), 9 deletions(-) create mode 100644 packages/cli/src/components/CommandPalette.tsx create mode 100644 packages/cli/src/pages/Devices.tsx diff --git a/packages/cli/package.json b/packages/cli/package.json index 1e7dd82a0a63..f02143312aa6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -56,6 +56,7 @@ "esbuild": "0.15.7", "ink": "^3.2.0", "ink-spinner": "^4.0.3", + "ink-table": "^3.0.0", "ink-text-input": "^4.0.3", "ink-use-stdout-dimensions": "^1.0.5", "prettier": "^2.8.1", diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 14b54a94ff79..f8a6929892a7 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -150,7 +150,10 @@ const CLI: React.FC = () => { bottomLabels={ !modalMessage && menuItemSlots.bottom } - height={rows} + height={ + rows - + (layout === "horizontal" ? 4 : 10) + } width={columns} paddingY={1} flexDirection={ diff --git a/packages/cli/src/components/CommandPalette.tsx b/packages/cli/src/components/CommandPalette.tsx new file mode 100644 index 000000000000..9a582e10c17b --- /dev/null +++ b/packages/cli/src/components/CommandPalette.tsx @@ -0,0 +1,31 @@ +import { Box } from "ink"; +import React from "react"; +import { Frame } from "./Frame"; +import { HotkeyLabel, HotkeyLabelProps } from "./HotkeyLabel"; +import { VDivider } from "./VDivider"; + +export interface CommandPaletteProps { + commands: HotkeyLabelProps[]; +} + +export const CommandPalette: React.FC = (props) => { + return ( + + {props.commands.map((p, i) => ( + + {i > 0 && } + + + + + ))} + + ); +}; diff --git a/packages/cli/src/components/HDivider.tsx b/packages/cli/src/components/HDivider.tsx index 503aeed72d98..c7b70af3b41e 100644 --- a/packages/cli/src/components/HDivider.tsx +++ b/packages/cli/src/components/HDivider.tsx @@ -3,10 +3,12 @@ import React, { useEffect, useRef, useState } from "react"; export interface HDividerProps extends TextProps { character?: string; + margin?: number; } export const HDivider: React.FC = ({ character = "─", + margin = 1, ...textProps }) => { const ref = useRef(null); @@ -24,7 +26,7 @@ export const HDivider: React.FC = ({ }); return ( - + {text} ); diff --git a/packages/cli/src/components/HotkeyLabel.tsx b/packages/cli/src/components/HotkeyLabel.tsx index 4f18583d2d36..5f9e37bb797e 100644 --- a/packages/cli/src/components/HotkeyLabel.tsx +++ b/packages/cli/src/components/HotkeyLabel.tsx @@ -17,7 +17,7 @@ const specialKeys: Record = { pageDown: "PgDn", pageUp: "PgUp", return: "↲", - escape: "", + escape: "Esc", tab: "↹", backspace: "⤆", delete: "Del", diff --git a/packages/cli/src/components/VDivider.tsx b/packages/cli/src/components/VDivider.tsx index 69c332713735..873b90338443 100644 --- a/packages/cli/src/components/VDivider.tsx +++ b/packages/cli/src/components/VDivider.tsx @@ -3,10 +3,12 @@ import { useEffect, useRef, useState } from "react"; export interface VDividerProps extends TextProps { character?: string; + margin?: number; } export const VDivider: React.FC = ({ character = "│", + margin = 1, ...textProps }) => { const ref = useRef(null); @@ -24,7 +26,7 @@ export const VDivider: React.FC = ({ }); return ( - + {text} ); diff --git a/packages/cli/src/pages/Devices.tsx b/packages/cli/src/pages/Devices.tsx new file mode 100644 index 000000000000..2802d50be510 --- /dev/null +++ b/packages/cli/src/pages/Devices.tsx @@ -0,0 +1,60 @@ +import { Box, Text } from "ink"; +import Table from "ink-table"; +import type { PropsWithChildren } from "react"; +import { CommandPalette } from "../components/CommandPalette"; +import { useDriver } from "../hooks/useDriver"; +import { useMenu } from "../hooks/useMenu"; + +const okText = "✓"; +const nokText = "✗"; + +const Cell: React.FC> = ({ + children, + column, +}) => { + if ( + column === 3 /* ready */ && + typeof children === "string" && + children.length === 1 + ) { + return ( + + {children} + + ); + } else { + return {children}; + } +}; + +export const DevicesPage: React.FC = () => { + useMenu([ + { + location: "topCenter", + item: "Devices", + }, + ]); + + const { driver } = useDriver(); + const nodes = [...driver.controller.nodes.values()]; + const nodesData = nodes.map((node) => ({ + "#": node.id, + Model: node.label + " " + node.deviceConfig?.description ?? "", + Type: node.deviceClass?.specific.label, + Rdy: node.ready ? "✓" : "✗", + })); + + return ( + + + + {/* @ts-expect-error cell type is wrong */} + + + ); +}; diff --git a/packages/cli/src/pages/MainMenu.tsx b/packages/cli/src/pages/MainMenu.tsx index dd4a340c2dc7..9fee52db124c 100644 --- a/packages/cli/src/pages/MainMenu.tsx +++ b/packages/cli/src/pages/MainMenu.tsx @@ -1,17 +1,63 @@ -import { Text } from "ink"; -import { useMenu } from "../hooks/useMenu"; +import { Box } from "ink"; +import { useState } from "react"; +import { HotkeyLabel } from "../components/HotkeyLabel"; +import { MenuItem, useMenu } from "../hooks/useMenu"; import { destroyDriverMenuItem, exitMenuItem, toggleLogMenuItem, } from "../lib/menu"; +import { DevicesPage } from "./Devices"; export interface MainMenuPageProps { // TODO: } +enum MainMenuSubPage { + None, + Devices, +} + export const MainMenuPage: React.FC = (props) => { - useMenu([toggleLogMenuItem, destroyDriverMenuItem, exitMenuItem]); + const [page, setPage] = useState(MainMenuSubPage.None); + + const backMenuItem: MenuItem = { + location: "bottomLeft", + item: ( + { + setPage(MainMenuSubPage.None); + }} + /> + ), + }; + + const devicesMenuItem: MenuItem = { + location: "bottomLeft", + item: ( + { + setPage(MainMenuSubPage.Devices); + }} + /> + ), + }; + + useMenu([ + page !== MainMenuSubPage.Devices && devicesMenuItem, + page !== MainMenuSubPage.None && backMenuItem, + toggleLogMenuItem, + destroyDriverMenuItem, + exitMenuItem, + ]); - return TODO; + return ( + + {page === MainMenuSubPage.Devices && } + + ); }; diff --git a/yarn.lock b/yarn.lock index 30ad30bfb9b0..30423997ca4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1814,6 +1814,7 @@ __metadata: esbuild: 0.15.7 ink: ^3.2.0 ink-spinner: ^4.0.3 + ink-table: ^3.0.0 ink-text-input: ^4.0.3 ink-use-stdout-dimensions: ^1.0.5 prettier: ^2.8.1 @@ -5239,6 +5240,18 @@ __metadata: languageName: node linkType: hard +"ink-table@npm:^3.0.0": + version: 3.0.0 + resolution: "ink-table@npm:3.0.0" + dependencies: + object-hash: ^2.0.3 + peerDependencies: + ink: ">=3.0.0" + react: ">=16.8.0" + checksum: 281ec6295251435a268d3ad3f9382f63eab0548234b3ee6daef07663ac5f24f76f59d5f6ab527f0407b0def05ab6e7ddd8e52c69ce52db251fac40fd0d2c10a1 + languageName: node + linkType: hard + "ink-text-input@npm:^4.0.3": version: 4.0.3 resolution: "ink-text-input@npm:4.0.3" @@ -6689,7 +6702,7 @@ __metadata: languageName: node linkType: hard -"object-hash@npm:^2.0.1": +"object-hash@npm:^2.0.1, object-hash@npm:^2.0.3": version: 2.2.0 resolution: "object-hash@npm:2.2.0" checksum: 55ba841e3adce9c4f1b9b46b41983eda40f854e0d01af2802d3ae18a7085a17168d6b81731d43fdf1d6bcbb3c9f9c56d22c8fea992203ad90a38d7d919bc28f1 From eed1ef824fe65da8b93ff65418ed64bb55a3c287 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Fri, 3 Mar 2023 15:34:25 +0100 Subject: [PATCH 12/27] fix: ooops --- packages/cli/src/lib/driver.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/lib/driver.ts b/packages/cli/src/lib/driver.ts index d6dc3141343e..9d7f7f5af72b 100644 --- a/packages/cli/src/lib/driver.ts +++ b/packages/cli/src/lib/driver.ts @@ -1,13 +1,11 @@ import path from "path"; import type winston from "winston"; - -// This line is the boundary between ESM and CommonJS. We need it because zwave-js uses __dirname and __filename -const { Driver } = require("zwave-js") as typeof import("zwave-js"); +import { Driver } from "zwave-js"; export async function startDriver( port: string, logTransport: winston.transport, -): Promise { +): Promise { const driver = new Driver(port, { logConfig: { // Do not log to console or file @@ -31,8 +29,8 @@ export async function startDriver( ), }, storage: { - cacheDir: path.join(__dirname, "cache"), - lockDir: path.join(__dirname, "cache/locks"), + cacheDir: path.join(process.cwd(), "cache"), + lockDir: path.join(process.cwd(), "cache/locks"), }, allowBootloaderOnly: true, }) From a2272913c7902de855a408ea3839b55957b63a3c Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Fri, 3 Mar 2023 15:36:17 +0100 Subject: [PATCH 13/27] chore: un-hide some .yarn files --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c4c40d61fc9f..07dd39750b92 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,7 +28,7 @@ ".husky/_/**/*": true, "packages/config/config/devices/index.json": true, "packages/*/package-lock.json": true, - ".yarn/*": true, + ".yarn/*.*": true, ".yarn/patches": false, ".yarn/releases": false, ".yarn/plugins": false, From 28d3a1e3575bbeaacb646705fe6ae3ad4d73c9b0 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Fri, 3 Mar 2023 23:45:04 +0100 Subject: [PATCH 14/27] feat: work on devices table --- .vscode/launch.json | 3 +- .../ink-table-npm-3.0.0-64b4e73397.patch | 233 ------------------ package.json | 3 +- packages/cli/package.json | 2 +- packages/cli/src/cli.tsx | 9 +- .../cli/src/components/CommandPalette.tsx | 9 +- packages/cli/src/components/HotkeyLabel.tsx | 2 +- packages/cli/src/hooks/useForceRerender.ts | 6 + packages/cli/src/pages/ConfirmExit.tsx | 4 +- packages/cli/src/pages/Devices.tsx | 157 +++++++++++- packages/cli/src/pages/MainMenu.tsx | 38 +-- yarn.lock | 38 +-- 12 files changed, 201 insertions(+), 303 deletions(-) delete mode 100644 .yarn/patches/ink-table-npm-3.0.0-64b4e73397.patch create mode 100644 packages/cli/src/hooks/useForceRerender.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index db85cd21070d..2cfecc532882 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,7 +11,8 @@ "runtimeArgs": [ "node", "--async-stack-traces", - "${workspaceFolder}/packages/cli/build/cli.js" + "${workspaceFolder}/packages/cli/build/cli.js", + "--start" ], "env": { // "DEV": "true" diff --git a/.yarn/patches/ink-table-npm-3.0.0-64b4e73397.patch b/.yarn/patches/ink-table-npm-3.0.0-64b4e73397.patch deleted file mode 100644 index 9e7604b83294..000000000000 --- a/.yarn/patches/ink-table-npm-3.0.0-64b4e73397.patch +++ /dev/null @@ -1,233 +0,0 @@ -diff --git a/README.md b/README.md -index 699d51dc41299693f72b19f2876c8ef09570a16a..0560480a9762d6a3ed4b5f8dab259167cece5b53 100644 ---- a/README.md -+++ b/README.md -@@ -13,7 +13,7 @@ npm install ink-table - ## Usage - - ```jsx --import Table from 'ink-table' -+import { Table } from 'ink-table' - - const data = [ - { -diff --git a/dist/index.d.ts b/dist/index.d.ts -index 9287fe4ed6b6da8a18149ae56f65ccca27fef483..3b7f4b98a2412e87e196ea3a22e9b07fcd4bb728 100644 ---- a/dist/index.d.ts -+++ b/dist/index.d.ts -@@ -1,9 +1,12 @@ - import React from 'react'; --declare type Scalar = string | number | boolean | null | undefined; --declare type ScalarDict = { -+type Scalar = string | number | boolean | null | undefined; -+type ScalarDict = { - [key: string]: Scalar; - }; --export declare type TableProps = { -+export type CellProps = React.PropsWithChildren<{ -+ column: number; -+}>; -+export type TableProps = { - /** - * List of values (rows). - */ -@@ -23,13 +26,13 @@ export declare type TableProps = { - /** - * Component used to render a cell in the table. - */ -- cell: (props: React.PropsWithChildren<{}>) => JSX.Element; -+ cell: (props: CellProps) => JSX.Element; - /** - * Component used to render the skeleton of the table. - */ - skeleton: (props: React.PropsWithChildren<{}>) => JSX.Element; - }; --export default class Table extends React.Component, 'data'> & Partial>> { -+export declare class Table extends React.Component, 'data'> & Partial>> { - /** - * Merges provided configuration with defaults. - */ -@@ -56,12 +59,12 @@ export default class Table extends React.Component) => JSX.Element; - render(): JSX.Element; - } --declare type RowProps = { -+type RowProps = { - key: string; - data: Partial; - columns: Column[]; - }; --declare type Column = { -+type Column = { - key: string; - column: keyof T; - width: number; -@@ -73,7 +76,7 @@ export declare function Header(props: React.PropsWithChildren<{}>): JSX.Element; - /** - * Renders a cell in the table. - */ --export declare function Cell(props: React.PropsWithChildren<{}>): JSX.Element; -+export declare function Cell(props: CellProps): JSX.Element; - /** - * Redners the scaffold of the table. - */ -diff --git a/dist/index.d.ts.map b/dist/index.d.ts.map -index d4a29c524a775f460b47f9bb8cad7cf4debaf931..277335bfee61857bf3171dd19673c82b56b61997 100644 ---- a/dist/index.d.ts.map -+++ b/dist/index.d.ts.map -@@ -1 +1 @@ --{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,aAAK,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAA;AAE1D,aAAK,UAAU,GAAG;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CACtB,CAAA;AAED,oBAAY,UAAU,CAAC,CAAC,SAAS,UAAU,IAAI;IAC7C;;OAEG;IACH,IAAI,EAAE,CAAC,EAAE,CAAA;IACT;;OAEG;IACH,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,CAAA;IACpB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IACf;;OAEG;IACH,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,OAAO,CAAA;IAC3D;;OAEG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,OAAO,CAAA;IACzD;;OAEG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,OAAO,CAAA;CAC9D,CAAA;AAID,MAAM,CAAC,OAAO,OAAO,KAAK,CAAC,CAAC,SAAS,UAAU,CAAE,SAAQ,KAAK,CAAC,SAAS,CACtE,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CACrD;IAGC;;OAEG;IACH,SAAS,IAAI,UAAU,CAAC,CAAC,CAAC;IAW1B;;OAEG;IACH,WAAW,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE;IAa1B;;;;;OAKG;IACH,UAAU,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE;IA0BzB;;OAEG;IACH,WAAW,IAAI,OAAO,CAAC,CAAC,CAAC;IAczB,MAAM,sCAWJ;IAGF,OAAO,sCAWL;IAGF,SAAS,sCAWP;IAGF,IAAI,sCAWF;IAGF,MAAM,sCAWJ;IAIF,MAAM;CA+BP;AA+BD,aAAK,QAAQ,CAAC,CAAC,SAAS,UAAU,IAAI;IACpC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IAChB,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAA;CACrB,CAAA;AAED,aAAK,MAAM,CAAC,CAAC,IAAI;IACf,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAC,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AA+DD;;GAEG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC,eAMxD;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC,eAEtD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC,eAE1D"} -\ No newline at end of file -+{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,KAAK,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAA;AAE1D,KAAK,UAAU,GAAG;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,SAAS,GAAG,KAAK,CAAC,iBAAiB,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAEnE,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,UAAU,IAAI;IAC7C;;OAEG;IACH,IAAI,EAAE,CAAC,EAAE,CAAA;IACT;;OAEG;IACH,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,EAAE,CAAA;IACpB;;OAEG;IACH,OAAO,EAAE,MAAM,CAAA;IACf;;OAEG;IACH,MAAM,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,OAAO,CAAA;IAC3D;;OAEG;IACH,IAAI,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,GAAG,CAAC,OAAO,CAAA;IACvC;;OAEG;IACH,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC,KAAK,GAAG,CAAC,OAAO,CAAA;CAC9D,CAAA;AAID,qBAAa,KAAK,CAAC,CAAC,SAAS,UAAU,CAAE,SAAQ,KAAK,CAAC,SAAS,CAC9D,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CACrD;IAGC;;OAEG;IACH,SAAS,IAAI,UAAU,CAAC,CAAC,CAAC;IAW1B;;OAEG;IACH,WAAW,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE;IAa1B;;;;;OAKG;IACH,UAAU,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE;IA0BzB;;OAEG;IACH,WAAW,IAAI,OAAO,CAAC,CAAC,CAAC;IAczB,MAAM,sCAWJ;IAGF,OAAO,sCAWL;IAGF,SAAS,sCAWP;IAGF,IAAI,sCAWF;IAGF,MAAM,sCAWJ;IAIF,MAAM;CA+BP;AA+BD,KAAK,QAAQ,CAAC,CAAC,SAAS,UAAU,IAAI;IACpC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;IAChB,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAA;CACrB,CAAA;AAED,KAAK,MAAM,CAAC,CAAC,IAAI;IACf,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAC,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AA+DD;;GAEG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC,eAMxD;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,SAAS,eAEpC;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC,eAE1D"} -\ No newline at end of file -diff --git a/dist/index.js b/dist/index.js -index 28ca7f7803896afdc857683544c8966d10a111c7..800986bca11571bdd10435fe7ad767409aa7521e 100644 ---- a/dist/index.js -+++ b/dist/index.js -@@ -1,14 +1,9 @@ --"use strict"; --var __importDefault = (this && this.__importDefault) || function (mod) { -- return (mod && mod.__esModule) ? mod : { "default": mod }; --}; --Object.defineProperty(exports, "__esModule", { value: true }); --exports.Skeleton = exports.Cell = exports.Header = void 0; --const react_1 = __importDefault(require("react")); --const ink_1 = require("ink"); --const object_hash_1 = require("object-hash"); -+import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime"; -+import React from 'react'; -+import { Box, Text } from 'ink'; -+import { sha1 } from 'object-hash'; - /* Table */ --class Table extends react_1.default.Component { -+export class Table extends React.Component { - constructor() { - /* Config */ - super(...arguments); -@@ -148,21 +143,14 @@ class Table extends react_1.default.Component { - /** - * Render the table line by line. - */ -- return (react_1.default.createElement(ink_1.Box, { flexDirection: "column" }, -- this.header({ key: 'header', columns, data: {} }), -- this.heading({ key: 'heading', columns, data: headings }), -- this.props.data.map((row, index) => { -- // Calculate the hash of the row based on its value and position -- const key = `row-${object_hash_1.sha1(row)}-${index}`; -- // Construct a row. -- return (react_1.default.createElement(ink_1.Box, { flexDirection: "column", key: key }, -- this.separator({ key: `separator-${key}`, columns, data: {} }), -- this.data({ key: `data-${key}`, columns, data: row }))); -- }), -- this.footer({ key: 'footer', columns, data: {} }))); -+ return (_jsxs(Box, { flexDirection: "column", children: [this.header({ key: 'header', columns, data: {} }), this.heading({ key: 'heading', columns, data: headings }), this.props.data.map((row, index) => { -+ // Calculate the hash of the row based on its value and position -+ const key = `row-${sha1(row)}-${index}`; -+ // Construct a row. -+ return (_jsxs(Box, { flexDirection: "column", children: [this.separator({ key: `separator-${key}`, columns, data: {} }), this.data({ key: `data-${key}`, columns, data: row })] }, key)); -+ }), this.footer({ key: 'footer', columns, data: {} })] })); - } - } --exports.default = Table; - /** - * Constructs a Row element from the configuration. - */ -@@ -170,54 +158,48 @@ function row(config) { - /* This is a component builder. We return a function. */ - const skeleton = config.skeleton; - /* Row */ -- return (props) => (react_1.default.createElement(ink_1.Box, { flexDirection: "row" }, -- react_1.default.createElement(skeleton.component, null, skeleton.left), -- intersperse((i) => { -- const key = `${props.key}-hseparator-${i}`; -- // The horizontal separator. -- return (react_1.default.createElement(skeleton.component, { key: key }, skeleton.cross)); -- }, -- // Values. -- props.columns.map((column) => { -- // content -- const value = props.data[column.column]; -- if (value == undefined || value == null) { -- const key = `${props.key}-empty-${column.key}`; -- return (react_1.default.createElement(config.cell, { key: key }, skeleton.line.repeat(column.width))); -- } -- else { -- const key = `${props.key}-cell-${column.key}`; -- // margins -- const ml = config.padding; -- const mr = column.width - String(value).length - config.padding; -- return ( -- /* prettier-ignore */ -- react_1.default.createElement(config.cell, { key: key }, `${skeleton.line.repeat(ml)}${String(value)}${skeleton.line.repeat(mr)}`)); -- } -- })), -- react_1.default.createElement(skeleton.component, null, skeleton.right))); -+ return (props) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(skeleton.component, { children: skeleton.left }), ...intersperse((i) => { -+ const key = `${props.key}-hseparator-${i}`; -+ // The horizontal separator. -+ return (_jsx(skeleton.component, { children: skeleton.cross }, key)); -+ }, -+ // Values. -+ props.columns.map((column, colI) => { -+ // content -+ const value = props.data[column.column]; -+ if (value == undefined || value == null) { -+ const key = `${props.key}-empty-${column.key}`; -+ return (_jsx(config.cell, { column: colI, children: skeleton.line.repeat(column.width) }, key)); -+ } -+ else { -+ const key = `${props.key}-cell-${column.key}`; -+ // margins -+ const ml = config.padding; -+ const mr = column.width - String(value).length - config.padding; -+ return ( -+ /* prettier-ignore */ -+ _jsx(config.cell, { column: colI, children: `${skeleton.line.repeat(ml)}${String(value)}${skeleton.line.repeat(mr)}` }, key)); -+ } -+ })), _jsx(skeleton.component, { children: skeleton.right })] })); - } - /** - * Renders the header of a table. - */ --function Header(props) { -- return (react_1.default.createElement(ink_1.Text, { bold: true, color: "blue" }, props.children)); -+export function Header(props) { -+ return (_jsx(Text, { bold: true, color: "blue", children: props.children })); - } --exports.Header = Header; - /** - * Renders a cell in the table. - */ --function Cell(props) { -- return react_1.default.createElement(ink_1.Text, null, props.children); -+export function Cell(props) { -+ return _jsx(Text, { children: props.children }); - } --exports.Cell = Cell; - /** - * Redners the scaffold of the table. - */ --function Skeleton(props) { -- return react_1.default.createElement(ink_1.Text, { bold: true }, props.children); -+export function Skeleton(props) { -+ return _jsx(Text, { bold: true, children: props.children }); - } --exports.Skeleton = Skeleton; - /* Utility functions */ - /** - * Intersperses a list of elements with another element. -diff --git a/package.json b/package.json -index 5ebd7734200cfab6483c5d0f32261ad870acefa6..2c73c97b1da24f557a895001070a51698f9d4abc 100644 ---- a/package.json -+++ b/package.json -@@ -2,7 +2,9 @@ - "name": "ink-table", - "version": "3.0.0", - "description": "A table component for Ink.", -- "main": "dist/index.js", -+ "type": "module", -+ "module": "dist/index.js", -+ "types": "dist/index.d.ts", - "repository": "maticzav/ink-table", - "author": "Matic Zavadlal ", - "license": "MIT", diff --git a/package.json b/package.json index 9a3d7787cd16..4c0d43c48985 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,8 @@ "colors": "1.4.0", "yoga-layout-prebuilt@^1.9.6": "patch:yoga-layout-prebuilt@npm%3A1.10.0#./.yarn/patches/yoga-layout-prebuilt-npm-1.10.0-855b15449f.patch", "react-devtools-core@^4.27.2": "patch:react-devtools-core@npm%3A4.27.2#./.yarn/patches/react-devtools-core-npm-4.27.2-7a013e485e.patch", - "ink-table@^3.0.0": "patch:ink-table@npm%3A3.0.0#./.yarn/patches/ink-table-npm-3.0.0-64b4e73397.patch" + "ink-table@^3.0.0": "patch:ink-table@npm%3A3.0.0#./.yarn/patches/ink-table-npm-3.0.0-64b4e73397.patch", + "@alcalzone/ink-table@^1.0.0": "patch:@alcalzone/ink-table@npm%3A1.0.0#./.yarn/patches/@alcalzone-ink-table-npm-1.0.0-65fbd8d761.patch" }, "scripts": { "w": "yarn ts maintenance/watch.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index f85ed46964d2..8096ac0ddf26 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -48,6 +48,7 @@ "zwave-js": "workspace:*" }, "devDependencies": { + "@alcalzone/ink-table": "~1.1.0", "@esm2cjs/p-queue": "^7.3.0", "@types/ink-spinner": "^3.0.1", "@types/ink-text-input": "^2.0.2", @@ -59,7 +60,6 @@ "esbuild-register": "^3.4.2", "ink": "^4.0.0", "ink-spinner": "^5.0.0", - "ink-table": "^3.0.0", "ink-text-input": "^5.0.0", "ink-use-stdout-dimensions": "^1.0.5", "prettier": "^2.8.1", diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index ebd50c5092ef..417ee857823b 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -29,9 +29,10 @@ const MIN_ROWS = 30; const logBuffer = new LinesBuffer(10000); const logTransport = createLogTransport(logBuffer.stream); - const clearLog = () => logBuffer.clear(); +const autostart = process.argv.includes("--start"); + const CLI: React.FC = () => { const [columns, rows] = useStdoutDimensions(); @@ -43,7 +44,7 @@ const CLI: React.FC = () => { setLayout(columns >= 180 ? "horizontal" : "vertical"); }, [columns, setLayout]); - const [usbPath, setUSBPath] = useState("/dev/ttyACM0"); + const [usbPath, setUSBPath] = useState("/dev/ttyUSB0"); const [driver, setDriver] = useState(); const destroyDriver = useCallback(async () => { if (driver) { @@ -53,7 +54,9 @@ const CLI: React.FC = () => { const [logVisible, setLogVisible] = useState(false); - const [cliPage, setCLIPage] = useState(CLIPage.Prepare); + const [cliPage, setCLIPage] = useState( + usbPath && autostart ? CLIPage.StartingDriver : CLIPage.Prepare, + ); const [prevCliPage, setPrevCLIPage] = useState(); const navigate = useCallback( diff --git a/packages/cli/src/components/CommandPalette.tsx b/packages/cli/src/components/CommandPalette.tsx index 9a582e10c17b..7c882b976cb4 100644 --- a/packages/cli/src/components/CommandPalette.tsx +++ b/packages/cli/src/components/CommandPalette.tsx @@ -1,10 +1,11 @@ import { Box } from "ink"; import React from "react"; -import { Frame } from "./Frame"; -import { HotkeyLabel, HotkeyLabelProps } from "./HotkeyLabel"; -import { VDivider } from "./VDivider"; +import { Frame } from "./Frame.js"; +import { HotkeyLabel, HotkeyLabelProps } from "./HotkeyLabel.js"; +import { VDivider } from "./VDivider.js"; export interface CommandPaletteProps { + label?: React.ReactNode; commands: HotkeyLabelProps[]; } @@ -12,7 +13,7 @@ export const CommandPalette: React.FC = (props) => { return ( = (props) => { return ( {label}{" "} - ({renderHotkey(hotkey, modifiers)}) + {renderHotkey(hotkey, modifiers)} ); } diff --git a/packages/cli/src/hooks/useForceRerender.ts b/packages/cli/src/hooks/useForceRerender.ts new file mode 100644 index 000000000000..203a30509af7 --- /dev/null +++ b/packages/cli/src/hooks/useForceRerender.ts @@ -0,0 +1,6 @@ +import { useState } from "react"; + +export function useForceRerender() { + const [value, setValue] = useState(0); + return () => setValue((value) => value + 1); +} diff --git a/packages/cli/src/pages/ConfirmExit.tsx b/packages/cli/src/pages/ConfirmExit.tsx index d73b56e2617d..c8baeaa7c6e8 100644 --- a/packages/cli/src/pages/ConfirmExit.tsx +++ b/packages/cli/src/pages/ConfirmExit.tsx @@ -1,6 +1,6 @@ import { Box, Text, useApp, useInput } from "ink"; -import { useDriver } from "../hooks/useDriver"; -import { useNavigation } from "../hooks/useNavigation"; +import { useDriver } from "../hooks/useDriver.js"; +import { useNavigation } from "../hooks/useNavigation.js"; export const ConfirmExitPage: React.FC = () => { const { exit } = useApp(); diff --git a/packages/cli/src/pages/Devices.tsx b/packages/cli/src/pages/Devices.tsx index e31d61d067d8..f2c5a4fd1e7d 100644 --- a/packages/cli/src/pages/Devices.tsx +++ b/packages/cli/src/pages/Devices.tsx @@ -1,12 +1,23 @@ +import { AllColumnProps, CellProps, Table } from "@alcalzone/ink-table"; import { Box, Text } from "ink"; -import { CellProps, Table } from "ink-table"; +import { useEffect, useState } from "react"; +import { DeviceClass, NodeStatus, ZWaveNode } from "zwave-js"; import { CommandPalette } from "../components/CommandPalette.js"; +import { HotkeyLabel } from "../components/HotkeyLabel.js"; import { useDriver } from "../hooks/useDriver.js"; -import { useMenu } from "../hooks/useMenu.js"; +import { useForceRerender } from "../hooks/useForceRerender.js"; const okText = "✓"; const nokText = "✗"; +const statusTexts = { + Unknown: ["?", "gray"], + Alive: ["●", "blueBright"], + Dead: ["☠", "red"], + Awake: ["☻", "blueBright"], + Asleep: ["z", "yellow"], +} as const; + const Cell: ((props: CellProps) => JSX.Element) | undefined = ({ children, column, @@ -22,38 +33,158 @@ const Cell: ((props: CellProps) => JSX.Element) | undefined = ({ {children} ); + } else if ( + column === 4 /* status */ && + typeof children === "string" && + children.trim().length === 1 + ) { + const trimmed = children.trim(); + const status = + statusTexts[NodeStatus[trimmed as any] as keyof typeof statusTexts]; + return ( + + {status ? children.replace(trimmed, status?.[0]) : children} + + ); } else { return {children}; } }; +function getDeviceType(cls: DeviceClass | undefined): string { + if (!cls) return "unknown"; + + const deviceType = cls.specific.zwavePlusDeviceType; + if (deviceType) return deviceType; + + const hasSpecificDeviceClass = cls.specific.key !== 0; + if (hasSpecificDeviceClass) return cls.specific.label; + return cls.generic.label; +} + +function getCustomName(node: ZWaveNode): string | undefined { + return [node.name, node.location && `(${node.location})`] + .filter((x) => !!x) + .join(" "); +} + +function getModel(node: ZWaveNode): string { + const mfg = node.deviceConfig?.manufacturer; + const model = node.label; + const desc = node.deviceConfig?.description; + return [mfg, model, desc].filter((x) => !!x).join(" "); +} + export const DevicesPage: React.FC = () => { - useMenu([ - { - location: "topCenter", - item: "Devices", - }, + const { driver } = useDriver(); + const forceRerender = useForceRerender(); + + const [maxRows, setMaxRows] = useState(10); + + const [nodeIDs, setNodeIDs] = useState([ + ...driver.controller.nodes.keys(), ]); - const { driver } = useDriver(); - const nodes = [...driver.controller.nodes.values()]; + const nodes = nodeIDs + .map((id) => driver.controller.nodes.get(id)) + .filter((x): x is ZWaveNode => !!x); + const nodesData = nodes.map((node) => ({ "#": node.id, - Model: node.label + " " + node.deviceConfig?.description ?? "", - Type: node.deviceClass?.specific.label, - Rdy: node.ready ? "✓" : "✗", + Model: getCustomName(node) || getModel(node), + Type: getDeviceType(node.deviceClass), + R: node.ready ? "✓" : "✗", + S: node.status, })); + // Register event handlers to update the table + useEffect(() => { + const updateIDs = () => { + setNodeIDs([...driver.controller.nodes.keys()]); + }; + + const addNodeEventHandlers = (node: ZWaveNode) => { + node.on("interview started", forceRerender) + .on("interview completed", forceRerender) + .on("ready", forceRerender) + .on("alive", forceRerender) + .on("dead", forceRerender) + .on("sleep", forceRerender) + .on("wake up", forceRerender); + }; + const removeNodeEventHandlers = (node: ZWaveNode) => { + node.off("interview started", forceRerender) + .off("interview completed", forceRerender) + .off("ready", forceRerender) + .off("alive", forceRerender) + .off("dead", forceRerender) + .off("sleep", forceRerender) + .off("wake up", forceRerender); + }; + + const nodeAdded = (node: ZWaveNode) => { + updateIDs(); + addNodeEventHandlers(node); + }; + const nodeRemoved = (node: ZWaveNode) => { + updateIDs(); + removeNodeEventHandlers(node); + }; + driver.controller.on("node added", nodeAdded); + driver.controller.on("node removed", nodeRemoved); + for (const node of driver.controller.nodes.values()) { + addNodeEventHandlers(node); + } + + return () => { + driver.controller.off("node added", nodeAdded); + driver.controller.off("node removed", nodeRemoved); + for (const node of driver.controller.nodes.values()) { + removeNodeEventHandlers(node); + } + }; + }, []); + + const columns: AllColumnProps[] = [ + { key: "#", align: "right" }, + { key: "Model" }, + { key: "Type", align: "center" }, + { key: "R" }, + { key: "S" }, + ]; + return ( + Devices + + } commands={[ { label: "Add", hotkey: "+" }, { label: "Remove", hotkey: "-" }, + { label: "Select", hotkey: "s" }, ]} > -
+
+ + + + Total{" "} + + {nodeIDs.length} + {" "} + devices. + + {nodeIDs.length > maxRows && ( + + Use and{" "} + to scroll. + + )} + ); }; diff --git a/packages/cli/src/pages/MainMenu.tsx b/packages/cli/src/pages/MainMenu.tsx index 9fee52db124c..031b5e2a22af 100644 --- a/packages/cli/src/pages/MainMenu.tsx +++ b/packages/cli/src/pages/MainMenu.tsx @@ -1,38 +1,38 @@ import { Box } from "ink"; import { useState } from "react"; -import { HotkeyLabel } from "../components/HotkeyLabel"; -import { MenuItem, useMenu } from "../hooks/useMenu"; +import { HotkeyLabel } from "../components/HotkeyLabel.js"; +import { MenuItem, useMenu } from "../hooks/useMenu.js"; import { destroyDriverMenuItem, exitMenuItem, toggleLogMenuItem, -} from "../lib/menu"; -import { DevicesPage } from "./Devices"; +} from "../lib/menu.js"; +import { DevicesPage } from "./Devices.js"; export interface MainMenuPageProps { // TODO: } enum MainMenuSubPage { - None, + // None, Devices, } export const MainMenuPage: React.FC = (props) => { - const [page, setPage] = useState(MainMenuSubPage.None); + const [page, setPage] = useState(MainMenuSubPage.Devices); - const backMenuItem: MenuItem = { - location: "bottomLeft", - item: ( - { - setPage(MainMenuSubPage.None); - }} - /> - ), - }; + // const backMenuItem: MenuItem = { + // location: "bottomLeft", + // item: ( + // { + // setPage(MainMenuSubPage.None); + // }} + // /> + // ), + // }; const devicesMenuItem: MenuItem = { location: "bottomLeft", @@ -49,7 +49,7 @@ export const MainMenuPage: React.FC = (props) => { useMenu([ page !== MainMenuSubPage.Devices && devicesMenuItem, - page !== MainMenuSubPage.None && backMenuItem, + // page !== MainMenuSubPage.Devices && backMenuItem, toggleLogMenuItem, destroyDriverMenuItem, exitMenuItem, diff --git a/yarn.lock b/yarn.lock index 35f2c96ff1ba..5cac323f43ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -52,6 +52,18 @@ __metadata: languageName: node linkType: hard +"@alcalzone/ink-table@npm:~1.1.0": + version: 1.1.0 + resolution: "@alcalzone/ink-table@npm:1.1.0" + dependencies: + object-hash: ^2.0.3 + peerDependencies: + ink: ">=3.0.0" + react: ">=16.8.0" + checksum: bab57684264a7140877e0b01b631dd2e53c471fea5b06b563b46691552ed3905e6f1c59d1c34575d46223336c38e928649b4c113ff387021008a779d79894c61 + languageName: node + linkType: hard + "@alcalzone/jsonl-db@npm:^2.5.3": version: 2.5.3 resolution: "@alcalzone/jsonl-db@npm:2.5.3" @@ -1802,6 +1814,7 @@ __metadata: version: 0.0.0-use.local resolution: "@zwave-js/cli@workspace:packages/cli" dependencies: + "@alcalzone/ink-table": ~1.1.0 "@esm2cjs/p-queue": ^7.3.0 "@types/ink-spinner": ^3.0.1 "@types/ink-text-input": ^2.0.2 @@ -1815,7 +1828,6 @@ __metadata: esbuild-register: ^3.4.2 ink: ^4.0.0 ink-spinner: ^5.0.0 - ink-table: ^3.0.0 ink-text-input: ^5.0.0 ink-use-stdout-dimensions: ^1.0.5 prettier: ^2.8.1 @@ -5236,30 +5248,6 @@ __metadata: languageName: node linkType: hard -"ink-table@npm:3.0.0": - version: 3.0.0 - resolution: "ink-table@npm:3.0.0" - dependencies: - object-hash: ^2.0.3 - peerDependencies: - ink: ">=3.0.0" - react: ">=16.8.0" - checksum: 281ec6295251435a268d3ad3f9382f63eab0548234b3ee6daef07663ac5f24f76f59d5f6ab527f0407b0def05ab6e7ddd8e52c69ce52db251fac40fd0d2c10a1 - languageName: node - linkType: hard - -"ink-table@patch:ink-table@npm%3A3.0.0#./.yarn/patches/ink-table-npm-3.0.0-64b4e73397.patch::locator=%40zwave-js%2Frepo%40workspace%3A.": - version: 3.0.0 - resolution: "ink-table@patch:ink-table@npm%3A3.0.0#./.yarn/patches/ink-table-npm-3.0.0-64b4e73397.patch::version=3.0.0&hash=7c4409&locator=%40zwave-js%2Frepo%40workspace%3A." - dependencies: - object-hash: ^2.0.3 - peerDependencies: - ink: ">=3.0.0" - react: ">=16.8.0" - checksum: e348d1fa35a3df1544d9ec69a89d1218dc917496f96c10c1822e107ce066a483877908b63bd92063505805d1d9fa3b43d88a4d16281ca8a0731490b9f3eb0a14 - languageName: node - linkType: hard - "ink-text-input@npm:^5.0.0": version: 5.0.0 resolution: "ink-text-input@npm:5.0.0" From 0a183a75a70e360a01d1bc3ed0359abb2b13614f Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Sat, 4 Mar 2023 23:44:42 +0100 Subject: [PATCH 15/27] feat: node selection --- packages/cli/src/cli.tsx | 72 ++++++++++----- packages/cli/src/components/ModalMessage.tsx | 41 --------- packages/cli/src/components/Modals.tsx | 94 ++++++++++++++++++++ packages/cli/src/hooks/useDialogs.ts | 6 +- packages/cli/src/pages/Devices.tsx | 23 ++++- 5 files changed, 170 insertions(+), 66 deletions(-) delete mode 100644 packages/cli/src/components/ModalMessage.tsx create mode 100644 packages/cli/src/components/Modals.tsx diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 417ee857823b..c6683e5fe85b 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -4,7 +4,7 @@ import type { Driver } from "zwave-js"; import { Frame } from "./components/Frame.js"; import { HDivider } from "./components/HDivider.js"; import { Log } from "./components/Log.js"; -import { ModalMessage, ModalMessageState } from "./components/ModalMessage.js"; +import { ModalMessage, ModalQuery, ModalState } from "./components/Modals.js"; import { SetUSBPath } from "./components/setUSBPath.js"; import { StartingDriverPage } from "./components/StartingDriver.js"; import { VDivider } from "./components/VDivider.js"; @@ -76,15 +76,37 @@ const CLI: React.FC = () => { return false; }, [prevCliPage, setCLIPage, setPrevCLIPage]); - const [modalMessage, setModalMessage] = useState(); + const [modalState, setModalState] = useState(); const showError = useCallback( (message: React.ReactNode) => { - setModalMessage({ - message: message, + setModalState({ + type: "message", + message, color: "red", + onSubmit: () => setModalState(undefined), }); }, - [setModalMessage], + [setModalState], + ); + const queryInput = useCallback( + (message: React.ReactNode, initial?: string) => { + return new Promise((resolve) => { + setModalState({ + type: "query", + message, + initial, + onSubmit: (value) => { + setModalState(undefined); + resolve(value); + }, + onCancel: () => { + setModalState(undefined); + resolve(undefined); + }, + }); + }); + }, + [setModalState], ); const [menuItemSlots, updateMenuItems] = useMenuItemSlots(defaultMenuItems); @@ -145,13 +167,13 @@ const CLI: React.FC = () => { destroyDriver, }} > - + { alignItems="stretch" justifyContent="space-around" > - {!modalMessage && ( + {!modalState && ( <> { )} )} - {modalMessage && ( - - setModalMessage(undefined) - } - color={modalMessage.color} - > - {modalMessage.message} - - )} + {modalState && + (modalState.type === "message" ? ( + + {modalState.message} + + ) : ( + + {modalState.message} + + ))} diff --git a/packages/cli/src/components/ModalMessage.tsx b/packages/cli/src/components/ModalMessage.tsx deleted file mode 100644 index 85ea77961df5..000000000000 --- a/packages/cli/src/components/ModalMessage.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Box, Text, TextProps, useInput } from "ink"; -import type { ReactNode } from "react"; -import { Center } from "./Center.js"; - -export interface ModalMessageState { - message: ReactNode; - color?: TextProps["color"]; -} - -export interface ModalMessageProps { - color?: TextProps["color"]; - onContinue: () => void; -} - -export const ModalMessage: React.FC< - React.PropsWithChildren -> = (props) => { - useInput((input, key) => { - if (key.return) { - props.onContinue(); - } - }); - - return ( -
- - {props.children} - - - Press ENTER to continue... - - -
- ); -}; diff --git a/packages/cli/src/components/Modals.tsx b/packages/cli/src/components/Modals.tsx new file mode 100644 index 000000000000..24425a3fe3f1 --- /dev/null +++ b/packages/cli/src/components/Modals.tsx @@ -0,0 +1,94 @@ +import { Box, Text, TextProps, useInput } from "ink"; +import { UncontrolledTextInput } from "ink-text-input"; +import type { ReactNode } from "react"; +import { Center } from "./Center.js"; + +export type ModalState = { + message: ReactNode; + color?: TextProps["color"]; +} & ( + | { + type: "message"; + onSubmit: () => void; + } + | { + type: "query"; + initial?: string; + onSubmit: (input: string) => void; + onCancel?: () => void; + } +); + +export type ModalMessageProps = Omit< + ModalState & { type: "message" }, + "message" | "type" +>; + +export const ModalMessage: React.FC< + React.PropsWithChildren +> = (props) => { + useInput((input, key) => { + if (key.return) { + props.onSubmit(); + } + }); + + return ( +
+ + {props.children} + + + ENTER to continue... + + +
+ ); +}; + +export type ModalQueryProps = Omit< + ModalState & { type: "query" }, + "message" | "type" +>; + +export const ModalQuery: React.FC> = ( + props, +) => { + useInput((input, key) => { + if (key.escape && props.onCancel) { + props.onCancel(); + } + // Submitting is handled by the input component + }); + + return ( +
+ + {props.children} + + + + + ENTER to confirm, ESCAPE{" "} + to cancel + + +
+ ); +}; diff --git a/packages/cli/src/hooks/useDialogs.ts b/packages/cli/src/hooks/useDialogs.ts index 6adc1c841c2a..2a21ad141eda 100644 --- a/packages/cli/src/hooks/useDialogs.ts +++ b/packages/cli/src/hooks/useDialogs.ts @@ -1,7 +1,11 @@ import React from "react"; export type IDialogsContext = { - showError: (message: React.ReactNode) => void; + showError(message: React.ReactNode): void; + queryInput( + message: React.ReactNode, + initial?: string, + ): Promise; }; // Context that stores references to the methods that show Notifications and Modals diff --git a/packages/cli/src/pages/Devices.tsx b/packages/cli/src/pages/Devices.tsx index f2c5a4fd1e7d..c2ed64204f56 100644 --- a/packages/cli/src/pages/Devices.tsx +++ b/packages/cli/src/pages/Devices.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import { DeviceClass, NodeStatus, ZWaveNode } from "zwave-js"; import { CommandPalette } from "../components/CommandPalette.js"; import { HotkeyLabel } from "../components/HotkeyLabel.js"; +import { useDialogs } from "../hooks/useDialogs.js"; import { useDriver } from "../hooks/useDriver.js"; import { useForceRerender } from "../hooks/useForceRerender.js"; @@ -78,6 +79,7 @@ function getModel(node: ZWaveNode): string { export const DevicesPage: React.FC = () => { const { driver } = useDriver(); const forceRerender = useForceRerender(); + const { queryInput } = useDialogs(); const [maxRows, setMaxRows] = useState(10); @@ -162,9 +164,24 @@ export const DevicesPage: React.FC = () => { } commands={[ - { label: "Add", hotkey: "+" }, - { label: "Remove", hotkey: "-" }, - { label: "Select", hotkey: "s" }, + { + label: "Select", + hotkey: "return", + onPress: async () => { + const nodeId = await queryInput("Enter node ID"); + if (!nodeId) return; + const nodeIdNum = parseInt(nodeId, 10); + if ( + !Number.isNaN(nodeIdNum) && + nodeIDs.includes(nodeIdNum) + ) { + // TODO: Select node ID + throw new Error(`Node ${nodeIdNum} selected`); + } + }, + }, + { label: "Include", hotkey: "+" }, + { label: "Exclude", hotkey: "-" }, ]} > From 5d6239a9e61a3de77de830f66733c71f541391ce Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Fri, 10 Mar 2023 23:51:04 +0100 Subject: [PATCH 16/27] feat: unknown model, remove failed node --- .vscode/typescriptreact.code-snippets | 4 +- packages/cli/src/cli.tsx | 73 ++++++++----------- .../cli/src/components/DestroyingDriver.tsx | 29 ++++++++ packages/cli/src/hooks/useDriver.ts | 1 - packages/cli/src/hooks/useNavigation.ts | 44 ++++++++++- packages/cli/src/lib/menu.tsx | 5 +- packages/cli/src/pages/Devices.tsx | 32 +++++++- packages/cli/src/pages/RemoveFailedNode.tsx | 42 +++++++++++ 8 files changed, 178 insertions(+), 52 deletions(-) create mode 100644 packages/cli/src/components/DestroyingDriver.tsx create mode 100644 packages/cli/src/pages/RemoveFailedNode.tsx diff --git a/.vscode/typescriptreact.code-snippets b/.vscode/typescriptreact.code-snippets index 562a43152834..cbf28efb5c31 100644 --- a/.vscode/typescriptreact.code-snippets +++ b/.vscode/typescriptreact.code-snippets @@ -2,12 +2,14 @@ "React Functional Component": { "prefix": "rfc", "body": [ + "import { Text } from \"ink\";", + "", "export interface ${0:$TM_FILENAME_BASE}Props {", "\t// TODO:", "}", "", "export const ${0:$TM_FILENAME_BASE}: React.FC<${0:$TM_FILENAME_BASE}Props> = (props) => {", - "\treturn
TODO
;", + "\treturn TODO;", "};" ] } diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index c6683e5fe85b..543c7109e2a9 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -6,20 +6,21 @@ import { HDivider } from "./components/HDivider.js"; import { Log } from "./components/Log.js"; import { ModalMessage, ModalQuery, ModalState } from "./components/Modals.js"; import { SetUSBPath } from "./components/setUSBPath.js"; -import { StartingDriverPage } from "./components/StartingDriver.js"; import { VDivider } from "./components/VDivider.js"; import { ActionsContext } from "./hooks/useActions.js"; import { DialogsContext } from "./hooks/useDialogs.js"; import { DriverContext } from "./hooks/useDriver.js"; import { GlobalsContext } from "./hooks/useGlobals.js"; import { MenuContext, useMenuItemSlots } from "./hooks/useMenu.js"; -import { CLIPage, NavigationContext } from "./hooks/useNavigation.js"; +import { + CLIPage, + CLIPageWithProps, + getPageComponent, + NavigationContext, +} from "./hooks/useNavigation.js"; import { useStdoutDimensions } from "./hooks/useStdoutDimensions.js"; import { createLogTransport, LinesBuffer } from "./lib/logging.js"; import { defaultMenuItems } from "./lib/menu.js"; -import { ConfirmExitPage } from "./pages/ConfirmExit.js"; -import { MainMenuPage } from "./pages/MainMenu.js"; -import { PreparePage } from "./pages/Prepare.js"; process.on("unhandledRejection", (err) => { throw err; @@ -46,23 +47,19 @@ const CLI: React.FC = () => { const [usbPath, setUSBPath] = useState("/dev/ttyUSB0"); const [driver, setDriver] = useState(); - const destroyDriver = useCallback(async () => { - if (driver) { - await driver.destroy(); - } - }, [driver]); const [logVisible, setLogVisible] = useState(false); - const [cliPage, setCLIPage] = useState( - usbPath && autostart ? CLIPage.StartingDriver : CLIPage.Prepare, - ); - const [prevCliPage, setPrevCLIPage] = useState(); + const [cliPage, setCLIPage] = useState({ + page: usbPath && autostart ? CLIPage.StartingDriver : CLIPage.Prepare, + props: {}, + }); + const [prevCliPage, setPrevCLIPage] = useState(); const navigate = useCallback( - (to: CLIPage) => { + (to: CLIPage, pageProps?: {}) => { setPrevCLIPage(cliPage); - setCLIPage(to); + setCLIPage({ page: to, props: pageProps }); }, [cliPage, setCLIPage, setPrevCLIPage], ); @@ -140,6 +137,8 @@ const CLI: React.FC = () => { ); } + const PageComponent = getPageComponent(cliPage.page); + return ( { > { value={{ driver: driver!, setDriver, - destroyDriver, }} > { flexGrow={1} justifyContent="center" > - {cliPage === - CLIPage.Prepare && ( - - )} - - {cliPage === + {/* TODO: This should be merged into `selectPage` */} + {cliPage.page === CLIPage.SetUSBPath && ( - setCLIPage( - CLIPage.Prepare, - ) + setCLIPage({ + page: CLIPage.Prepare, + }) } onSubmit={(path) => { setUSBPath(path); - setCLIPage( - CLIPage.Prepare, - ); + setCLIPage({ + page: CLIPage.Prepare, + }); }} /> )} - {cliPage === - CLIPage.StartingDriver && ( - - )} - - {cliPage === - CLIPage.MainMenu && ( - - )} - - {cliPage === - CLIPage.ConfirmExit && ( - + {PageComponent && ( + )} {logVisible && ( diff --git a/packages/cli/src/components/DestroyingDriver.tsx b/packages/cli/src/components/DestroyingDriver.tsx new file mode 100644 index 000000000000..d8f9c4d12ff8 --- /dev/null +++ b/packages/cli/src/components/DestroyingDriver.tsx @@ -0,0 +1,29 @@ +import { Text } from "ink"; +import Spinner from "ink-spinner"; +import { useEffect } from "react"; +import { useDriver } from "../hooks/useDriver.js"; +import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; +import { Center } from "./Center.js"; + +export const DestroyingDriverPage: React.FC = () => { + const { destroyDriver } = useDriver(); + const { navigate } = useNavigation(); + + // When opening this page, destroy the driver + useEffect(() => { + destroyDriver().then(() => { + navigate(CLIPage.Prepare); + }); + }, []); + + return ( +
+ + + + {" "} + Destroying driver... + +
+ ); +}; diff --git a/packages/cli/src/hooks/useDriver.ts b/packages/cli/src/hooks/useDriver.ts index be5bf9810a6d..8c6885251843 100644 --- a/packages/cli/src/hooks/useDriver.ts +++ b/packages/cli/src/hooks/useDriver.ts @@ -4,7 +4,6 @@ import type { Driver } from "zwave-js"; interface IDriverContext { driver: Driver; setDriver: (driver: Driver) => void; - destroyDriver: () => Promise; } export const DriverContext = React.createContext({} as any); diff --git a/packages/cli/src/hooks/useNavigation.ts b/packages/cli/src/hooks/useNavigation.ts index 247fa199848e..d79da30d973c 100644 --- a/packages/cli/src/hooks/useNavigation.ts +++ b/packages/cli/src/hooks/useNavigation.ts @@ -1,17 +1,35 @@ import React from "react"; +import { DestroyingDriverPage } from "../components/DestroyingDriver.js"; +import { SetUSBPath } from "../components/setUSBPath.js"; +import { StartingDriverPage } from "../components/StartingDriver.js"; +import { ConfirmExitPage } from "../pages/ConfirmExit.js"; +import { MainMenuPage } from "../pages/MainMenu.js"; +import { PreparePage } from "../pages/Prepare.js"; +import { RemoveFailedNodePage } from "../pages/RemoveFailedNode.js"; export enum CLIPage { Prepare, SetUSBPath, StartingDriver, + DestroyingDriver, + MainMenu, + + RemoveFailedNode, + ConfirmExit, } +export interface CLIPageWithProps { + page: CLIPage; + props?: {}; +} + interface INavigationContext { previousPage?: CLIPage; currentPage: CLIPage; - navigate: (page: CLIPage) => void; + // TODO: type this better + navigate: (page: CLIPage, pageProps?: {}) => void; back: () => boolean; } @@ -20,3 +38,27 @@ export const NavigationContext = React.createContext( ); export const useNavigation = () => React.useContext(NavigationContext); + +export function getPageComponent(cliPage: CLIPage): React.FC | undefined { + switch (cliPage) { + case CLIPage.Prepare: + return PreparePage; + case CLIPage.SetUSBPath: + return SetUSBPath; + + case CLIPage.StartingDriver: + return StartingDriverPage; + case CLIPage.DestroyingDriver: + return DestroyingDriverPage; + + case CLIPage.MainMenu: + return MainMenuPage; + + case CLIPage.RemoveFailedNode: + return RemoveFailedNodePage; + + case CLIPage.ConfirmExit: + return ConfirmExitPage; + } + return undefined; +} diff --git a/packages/cli/src/lib/menu.tsx b/packages/cli/src/lib/menu.tsx index 070e89e293b2..ec9e59712a3a 100644 --- a/packages/cli/src/lib/menu.tsx +++ b/packages/cli/src/lib/menu.tsx @@ -58,10 +58,7 @@ const DestroyDriverMenuItem: React.FC = () => { { - await destroyDriver(); - navigate(CLIPage.Prepare); - }} + onPress={() => navigate(CLIPage.DestroyingDriver)} /> ); }; diff --git a/packages/cli/src/pages/Devices.tsx b/packages/cli/src/pages/Devices.tsx index c2ed64204f56..7ae954bf2ab3 100644 --- a/packages/cli/src/pages/Devices.tsx +++ b/packages/cli/src/pages/Devices.tsx @@ -7,6 +7,7 @@ import { HotkeyLabel } from "../components/HotkeyLabel.js"; import { useDialogs } from "../hooks/useDialogs.js"; import { useDriver } from "../hooks/useDriver.js"; import { useForceRerender } from "../hooks/useForceRerender.js"; +import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; const okText = "✓"; const nokText = "✗"; @@ -19,11 +20,19 @@ const statusTexts = { Asleep: ["z", "yellow"], } as const; +const unknownText = "(unknown)"; + const Cell: ((props: CellProps) => JSX.Element) | undefined = ({ children, column, }) => { if ( + column === 1 /* model */ && + typeof children === "string" && + children.trim() === unknownText + ) { + return {children}; + } else if ( column === 3 /* ready */ && typeof children === "string" && children.trim().length === 1 @@ -80,6 +89,7 @@ export const DevicesPage: React.FC = () => { const { driver } = useDriver(); const forceRerender = useForceRerender(); const { queryInput } = useDialogs(); + const { navigate } = useNavigation(); const [maxRows, setMaxRows] = useState(10); @@ -93,7 +103,7 @@ export const DevicesPage: React.FC = () => { const nodesData = nodes.map((node) => ({ "#": node.id, - Model: getCustomName(node) || getModel(node), + Model: getCustomName(node) || getModel(node) || unknownText, Type: getDeviceType(node.deviceClass), R: node.ready ? "✓" : "✗", S: node.status, @@ -182,6 +192,26 @@ export const DevicesPage: React.FC = () => { }, { label: "Include", hotkey: "+" }, { label: "Exclude", hotkey: "-" }, + { label: "Replace failed", hotkey: "r" }, + { + label: "Remove failed", + hotkey: "f", + onPress: async () => { + const nodeId = await queryInput( + "Enter ID of the node to remove", + ); + if (!nodeId) return; + const nodeIdNum = parseInt(nodeId, 10); + if ( + !Number.isNaN(nodeIdNum) && + nodeIDs.includes(nodeIdNum) + ) { + navigate(CLIPage.RemoveFailedNode, { + nodeId: nodeIdNum, + }); + } + }, + }, ]} > diff --git a/packages/cli/src/pages/RemoveFailedNode.tsx b/packages/cli/src/pages/RemoveFailedNode.tsx new file mode 100644 index 000000000000..9d8cc9860406 --- /dev/null +++ b/packages/cli/src/pages/RemoveFailedNode.tsx @@ -0,0 +1,42 @@ +import { Text } from "ink"; +import Spinner from "ink-spinner"; +import { useEffect } from "react"; +import { Center } from "../components/Center.js"; +import { useDialogs } from "../hooks/useDialogs.js"; +import { useDriver } from "../hooks/useDriver.js"; +import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; + +export interface RemoveFailedNodePageProps { + nodeId: number; +} + +export const RemoveFailedNodePage: React.FC = ( + props, +) => { + const { driver } = useDriver(); + const { navigate } = useNavigation(); + const { showError } = useDialogs(); + + useEffect(() => { + (async () => { + try { + await driver.controller.removeFailedNode(props.nodeId); + } catch (e: any) { + showError(`Failed to remove node: ${e.message}`); + } finally { + navigate(CLIPage.MainMenu); + } + })(); + }, []); + + return ( +
+ + + + {" "} + Removing failed node {props.nodeId}... + +
+ ); +}; From 037a28baf7d1edf823f4a5656fd9f0968d4db176 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Sat, 11 Mar 2023 00:28:57 +0100 Subject: [PATCH 17/27] feat: exclude nodes, event handlers --- packages/cli/src/cli.tsx | 16 +++- packages/cli/src/hooks/useDialogs.ts | 1 + packages/cli/src/hooks/useDriver.ts | 29 +++++++- packages/cli/src/hooks/useNavigation.ts | 8 +- .../DestroyingDriver.tsx | 2 +- packages/cli/src/pages/Devices.tsx | 8 +- packages/cli/src/pages/ExcludeNode.tsx | 74 +++++++++++++++++++ packages/cli/src/pages/RemoveFailedNode.tsx | 3 +- .../{components => pages}/StartingDriver.tsx | 0 packages/zwave-js/src/Controller.ts | 5 +- packages/zwave-js/src/Controller_safe.ts | 4 + packages/zwave-js/src/Driver.ts | 1 + .../zwave-js/src/lib/controller/Controller.ts | 2 +- 13 files changed, 144 insertions(+), 9 deletions(-) rename packages/cli/src/{components => pages}/DestroyingDriver.tsx (92%) create mode 100644 packages/cli/src/pages/ExcludeNode.tsx rename packages/cli/src/{components => pages}/StartingDriver.tsx (100%) diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 543c7109e2a9..f76d40756723 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -26,6 +26,9 @@ process.on("unhandledRejection", (err) => { throw err; }); +// We may have more than 10 input listeners active at any given time +process.stdin.setMaxListeners(100); + const MIN_ROWS = 30; const logBuffer = new LinesBuffer(10000); @@ -85,6 +88,17 @@ const CLI: React.FC = () => { }, [setModalState], ); + const showSuccess = useCallback( + (message: React.ReactNode) => { + setModalState({ + type: "message", + message, + color: "green", + onSubmit: () => setModalState(undefined), + }); + }, + [setModalState], + ); const queryInput = useCallback( (message: React.ReactNode, initial?: string) => { return new Promise((resolve) => { @@ -166,7 +180,7 @@ const CLI: React.FC = () => { }} > { return { driver, setDriver, destroyDriver }; }; + +export function useDriverEvent( + type: K, + listener: DriverEventCallbacks[K], +): void { + const { driver } = React.useContext(DriverContext); + React.useEffect(() => { + driver.on(type, listener); + return () => void driver.off(type, listener); + }, [listener, type]); +} + +export function useControllerEvent( + type: K, + listener: ControllerEventCallbacks[K], +): void { + const { driver } = React.useContext(DriverContext); + const controller = driver.controller; + React.useEffect(() => { + controller.on(type, listener); + return () => void controller.off(type, listener); + }, [listener, type]); +} diff --git a/packages/cli/src/hooks/useNavigation.ts b/packages/cli/src/hooks/useNavigation.ts index d79da30d973c..3f2dcb56f47d 100644 --- a/packages/cli/src/hooks/useNavigation.ts +++ b/packages/cli/src/hooks/useNavigation.ts @@ -1,11 +1,12 @@ import React from "react"; -import { DestroyingDriverPage } from "../components/DestroyingDriver.js"; import { SetUSBPath } from "../components/setUSBPath.js"; -import { StartingDriverPage } from "../components/StartingDriver.js"; import { ConfirmExitPage } from "../pages/ConfirmExit.js"; +import { DestroyingDriverPage } from "../pages/DestroyingDriver.js"; +import { ExcludeNodePage } from "../pages/ExcludeNode.js"; import { MainMenuPage } from "../pages/MainMenu.js"; import { PreparePage } from "../pages/Prepare.js"; import { RemoveFailedNodePage } from "../pages/RemoveFailedNode.js"; +import { StartingDriverPage } from "../pages/StartingDriver.js"; export enum CLIPage { Prepare, @@ -15,6 +16,7 @@ export enum CLIPage { MainMenu, + ExcludeNode, RemoveFailedNode, ConfirmExit, @@ -54,6 +56,8 @@ export function getPageComponent(cliPage: CLIPage): React.FC | undefined { case CLIPage.MainMenu: return MainMenuPage; + case CLIPage.ExcludeNode: + return ExcludeNodePage; case CLIPage.RemoveFailedNode: return RemoveFailedNodePage; diff --git a/packages/cli/src/components/DestroyingDriver.tsx b/packages/cli/src/pages/DestroyingDriver.tsx similarity index 92% rename from packages/cli/src/components/DestroyingDriver.tsx rename to packages/cli/src/pages/DestroyingDriver.tsx index d8f9c4d12ff8..6692cc140698 100644 --- a/packages/cli/src/components/DestroyingDriver.tsx +++ b/packages/cli/src/pages/DestroyingDriver.tsx @@ -1,9 +1,9 @@ import { Text } from "ink"; import Spinner from "ink-spinner"; import { useEffect } from "react"; +import { Center } from "../components/Center.js"; import { useDriver } from "../hooks/useDriver.js"; import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; -import { Center } from "./Center.js"; export const DestroyingDriverPage: React.FC = () => { const { destroyDriver } = useDriver(); diff --git a/packages/cli/src/pages/Devices.tsx b/packages/cli/src/pages/Devices.tsx index 7ae954bf2ab3..b844b94e70c9 100644 --- a/packages/cli/src/pages/Devices.tsx +++ b/packages/cli/src/pages/Devices.tsx @@ -191,7 +191,13 @@ export const DevicesPage: React.FC = () => { }, }, { label: "Include", hotkey: "+" }, - { label: "Exclude", hotkey: "-" }, + { + label: "Exclude", + hotkey: "-", + onPress: () => { + navigate(CLIPage.ExcludeNode); + }, + }, { label: "Replace failed", hotkey: "r" }, { label: "Remove failed", diff --git a/packages/cli/src/pages/ExcludeNode.tsx b/packages/cli/src/pages/ExcludeNode.tsx new file mode 100644 index 000000000000..9756f9ab2b0f --- /dev/null +++ b/packages/cli/src/pages/ExcludeNode.tsx @@ -0,0 +1,74 @@ +import { getErrorMessage } from "@zwave-js/shared"; +import { Text, useInput } from "ink"; +import Spinner from "ink-spinner"; +import { useEffect, useState } from "react"; +import { ExclusionStrategy } from "zwave-js"; +import { Center } from "../components/Center.js"; +import { useDialogs } from "../hooks/useDialogs.js"; +import { useControllerEvent, useDriver } from "../hooks/useDriver.js"; +import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; + +export interface ExcludeNodePageProps {} + +export const ExcludeNodePage: React.FC = (props) => { + const { driver } = useDriver(); + const { navigate } = useNavigation(); + const { showError, showSuccess } = useDialogs(); + const [message, setMessage] = useState("Starting exclusion..."); + + useInput(async (input, key) => { + if (key.escape) { + await driver.controller.stopExclusion(); + navigate(CLIPage.MainMenu); + } + }); + + useEffect(() => { + (async () => { + try { + const result = await driver.controller.beginExclusion({ + strategy: ExclusionStrategy.DisableProvisioningEntry, + }); + if (result) { + setMessage( + "Exclusion started, push the button on the device to exclude it.", + ); + } else { + showError("Failed to start exclusion!"); + navigate(CLIPage.MainMenu); + } + } catch (e) { + showError(`Failed to start exclusion: ${getErrorMessage(e)}`); + navigate(CLIPage.MainMenu); + } + })(); + }, []); + + // useControllerEvent("exclusion stopped", () => { + // navigate(CLIPage.MainMenu); + // }); + + useControllerEvent("exclusion failed", () => { + showError("Exclusion failed!"); + navigate(CLIPage.MainMenu); + }); + + useControllerEvent("node removed", (node) => { + showSuccess(`Node ${node.id} was removed!`); + navigate(CLIPage.MainMenu); + }); + + return ( +
+ + + + {" "} + {message} + + + Press ESCAPE to cancel. + +
+ ); +}; diff --git a/packages/cli/src/pages/RemoveFailedNode.tsx b/packages/cli/src/pages/RemoveFailedNode.tsx index 9d8cc9860406..baeb8be89a06 100644 --- a/packages/cli/src/pages/RemoveFailedNode.tsx +++ b/packages/cli/src/pages/RemoveFailedNode.tsx @@ -15,12 +15,13 @@ export const RemoveFailedNodePage: React.FC = ( ) => { const { driver } = useDriver(); const { navigate } = useNavigation(); - const { showError } = useDialogs(); + const { showError, showSuccess } = useDialogs(); useEffect(() => { (async () => { try { await driver.controller.removeFailedNode(props.nodeId); + showSuccess(`Node ${props.nodeId} removed!`); } catch (e: any) { showError(`Failed to remove node: ${e.message}`); } finally { diff --git a/packages/cli/src/components/StartingDriver.tsx b/packages/cli/src/pages/StartingDriver.tsx similarity index 100% rename from packages/cli/src/components/StartingDriver.tsx rename to packages/cli/src/pages/StartingDriver.tsx diff --git a/packages/zwave-js/src/Controller.ts b/packages/zwave-js/src/Controller.ts index 00909495827b..2aabf1672625 100644 --- a/packages/zwave-js/src/Controller.ts +++ b/packages/zwave-js/src/Controller.ts @@ -13,7 +13,10 @@ export { TXReport, } from "@zwave-js/core/safe"; export { ZWaveController } from "./lib/controller/Controller"; -export type { ControllerEvents } from "./lib/controller/Controller"; +export type { + ControllerEventCallbacks, + ControllerEvents, +} from "./lib/controller/Controller"; export type { ControllerStatistics } from "./lib/controller/ControllerStatistics"; export { ZWaveFeature } from "./lib/controller/Features"; export * from "./lib/controller/Inclusion"; diff --git a/packages/zwave-js/src/Controller_safe.ts b/packages/zwave-js/src/Controller_safe.ts index 26a582d5970b..a410964209e5 100644 --- a/packages/zwave-js/src/Controller_safe.ts +++ b/packages/zwave-js/src/Controller_safe.ts @@ -6,6 +6,10 @@ export { RssiError, TXReport, } from "@zwave-js/core/safe"; +export type { + ControllerEventCallbacks, + ControllerEvents, +} from "./lib/controller/Controller"; export type { ControllerStatistics } from "./lib/controller/ControllerStatistics"; export { ZWaveFeature } from "./lib/controller/Features"; export * from "./lib/controller/Inclusion"; diff --git a/packages/zwave-js/src/Driver.ts b/packages/zwave-js/src/Driver.ts index 00ab47d272f2..d38bead7791a 100644 --- a/packages/zwave-js/src/Driver.ts +++ b/packages/zwave-js/src/Driver.ts @@ -8,6 +8,7 @@ export type { ResponseRole, } from "@zwave-js/serial"; export { Driver, libName, libVersion } from "./lib/driver/Driver"; +export type { DriverEventCallbacks } from "./lib/driver/Driver"; export type { EditableZWaveOptions, ZWaveOptions, diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 65f0313d5d4a..a0e3a4b3db88 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -332,7 +332,7 @@ import { } from "./_Types"; // Strongly type the event emitter events -interface ControllerEventCallbacks +export interface ControllerEventCallbacks extends StatisticsEventCallbacks { "inclusion failed": () => void; "exclusion failed": () => void; From 617fac318e23f5393a58650b0ac0643508567e38 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Sat, 11 Mar 2023 23:28:33 +0100 Subject: [PATCH 18/27] feat: include/exclude --- packages/cli/package.json | 1 + packages/cli/src/cli.tsx | 107 +++++++++++--- packages/cli/src/components/Modals.tsx | 36 ++++- packages/cli/src/hooks/useDialogs.ts | 6 +- packages/cli/src/hooks/useNavigation.ts | 11 ++ packages/cli/src/pages/Devices.tsx | 53 ++++++- packages/cli/src/pages/IncludeNode.tsx | 170 ++++++++++++++++++++++ packages/cli/src/pages/StartingDriver.tsx | 7 +- yarn.lock | 32 ++++ 9 files changed, 388 insertions(+), 35 deletions(-) create mode 100644 packages/cli/src/pages/IncludeNode.tsx diff --git a/packages/cli/package.json b/packages/cli/package.json index 8096ac0ddf26..684d4072d115 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -59,6 +59,7 @@ "esbuild": "0.15.7", "esbuild-register": "^3.4.2", "ink": "^4.0.0", + "ink-select-input": "^5.0.0", "ink-spinner": "^5.0.0", "ink-text-input": "^5.0.0", "ink-use-stdout-dimensions": "^1.0.5", diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index f76d40756723..024cbdc7bfd2 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -4,7 +4,12 @@ import type { Driver } from "zwave-js"; import { Frame } from "./components/Frame.js"; import { HDivider } from "./components/HDivider.js"; import { Log } from "./components/Log.js"; -import { ModalMessage, ModalQuery, ModalState } from "./components/Modals.js"; +import { + InlineQuery, + ModalMessage, + ModalQuery, + ModalState, +} from "./components/Modals.js"; import { SetUSBPath } from "./components/setUSBPath.js"; import { VDivider } from "./components/VDivider.js"; import { ActionsContext } from "./hooks/useActions.js"; @@ -88,6 +93,17 @@ const CLI: React.FC = () => { }, [setModalState], ); + const showWarning = useCallback( + (message: React.ReactNode) => { + setModalState({ + type: "message", + message, + color: "yellow", + onSubmit: () => setModalState(undefined), + }); + }, + [setModalState], + ); const showSuccess = useCallback( (message: React.ReactNode) => { setModalState({ @@ -100,10 +116,17 @@ const CLI: React.FC = () => { [setModalState], ); const queryInput = useCallback( - (message: React.ReactNode, initial?: string) => { + ( + message: React.ReactNode, + options: { + initial?: string; + inline?: boolean; + } = {}, + ) => { + const { initial, inline = false } = options; return new Promise((resolve) => { setModalState({ - type: "query", + type: inline ? "queryInline" : "query", message, initial, onSubmit: (value) => { @@ -180,7 +203,12 @@ const CLI: React.FC = () => { }} > { : "column" } alignItems="stretch" - justifyContent="space-around" + justifyContent="space-between" > - {!modalState && ( + {(!modalState || + modalState.type === "queryInline") && ( <> {/* TODO: This should be merged into `selectPage` */} @@ -231,6 +262,24 @@ const CLI: React.FC = () => { {...cliPage.props} /> )} + + {modalState?.type === + "queryInline" && ( + + {modalState.message} + + )} {logVisible && ( { )} )} - {modalState && - (modalState.type === "message" ? ( - - {modalState.message} - - ) : ( - - {modalState.message} - - ))} + {modalState && ( + <> + {modalState.type === "message" && ( + + {modalState.message} + + )} + {modalState.type === "query" && ( + + {modalState.message} + + )} + + )} diff --git a/packages/cli/src/components/Modals.tsx b/packages/cli/src/components/Modals.tsx index 24425a3fe3f1..d7e22a8c32f5 100644 --- a/packages/cli/src/components/Modals.tsx +++ b/packages/cli/src/components/Modals.tsx @@ -12,7 +12,7 @@ export type ModalState = { onSubmit: () => void; } | { - type: "query"; + type: "query" | "queryInline"; initial?: string; onSubmit: (input: string) => void; onCancel?: () => void; @@ -92,3 +92,37 @@ export const ModalQuery: React.FC> = ( ); }; + +export type InlineQueryProps = Omit< + ModalState & { type: "queryInline" }, + "message" | "type" +>; + +export const InlineQuery: React.FC< + React.PropsWithChildren +> = (props) => { + useInput((input, key) => { + if (key.escape && props.onCancel) { + props.onCancel(); + } + // Submitting is handled by the input component + }); + + return ( + + {props.children}: + + + + + ); +}; diff --git a/packages/cli/src/hooks/useDialogs.ts b/packages/cli/src/hooks/useDialogs.ts index 50ebba58404e..a98af5495a04 100644 --- a/packages/cli/src/hooks/useDialogs.ts +++ b/packages/cli/src/hooks/useDialogs.ts @@ -2,10 +2,14 @@ import React from "react"; export type IDialogsContext = { showError(message: React.ReactNode): void; + showWarning(message: React.ReactNode): void; showSuccess(message: React.ReactNode): void; queryInput( message: React.ReactNode, - initial?: string, + options?: { + initial?: string; + inline?: boolean; + }, ): Promise; }; diff --git a/packages/cli/src/hooks/useNavigation.ts b/packages/cli/src/hooks/useNavigation.ts index 3f2dcb56f47d..8c12a87c5ae4 100644 --- a/packages/cli/src/hooks/useNavigation.ts +++ b/packages/cli/src/hooks/useNavigation.ts @@ -3,6 +3,10 @@ import { SetUSBPath } from "../components/setUSBPath.js"; import { ConfirmExitPage } from "../pages/ConfirmExit.js"; import { DestroyingDriverPage } from "../pages/DestroyingDriver.js"; import { ExcludeNodePage } from "../pages/ExcludeNode.js"; +import { + BootstrappingNodePage, + IncludeNodePage, +} from "../pages/IncludeNode.js"; import { MainMenuPage } from "../pages/MainMenu.js"; import { PreparePage } from "../pages/Prepare.js"; import { RemoveFailedNodePage } from "../pages/RemoveFailedNode.js"; @@ -16,6 +20,8 @@ export enum CLIPage { MainMenu, + IncludeNode, + BootstrappingNode, ExcludeNode, RemoveFailedNode, @@ -56,6 +62,11 @@ export function getPageComponent(cliPage: CLIPage): React.FC | undefined { case CLIPage.MainMenu: return MainMenuPage; + case CLIPage.IncludeNode: + return IncludeNodePage; + case CLIPage.BootstrappingNode: + return BootstrappingNodePage; + case CLIPage.ExcludeNode: return ExcludeNodePage; case CLIPage.RemoveFailedNode: diff --git a/packages/cli/src/pages/Devices.tsx b/packages/cli/src/pages/Devices.tsx index b844b94e70c9..1c046629e281 100644 --- a/packages/cli/src/pages/Devices.tsx +++ b/packages/cli/src/pages/Devices.tsx @@ -88,7 +88,7 @@ function getModel(node: ZWaveNode): string { export const DevicesPage: React.FC = () => { const { driver } = useDriver(); const forceRerender = useForceRerender(); - const { queryInput } = useDialogs(); + const { queryInput, showError } = useDialogs(); const { navigate } = useNavigation(); const [maxRows, setMaxRows] = useState(10); @@ -178,7 +178,12 @@ export const DevicesPage: React.FC = () => { label: "Select", hotkey: "return", onPress: async () => { - const nodeId = await queryInput("Enter node ID"); + const nodeId = await queryInput( + "Select → Node ID", + { + inline: true, + }, + ); if (!nodeId) return; const nodeIdNum = parseInt(nodeId, 10); if ( @@ -186,11 +191,19 @@ export const DevicesPage: React.FC = () => { nodeIDs.includes(nodeIdNum) ) { // TODO: Select node ID - throw new Error(`Node ${nodeIdNum} selected`); + // throw new Error(`Node ${nodeIdNum} selected`); + } else { + showError("Node not found"); } }, }, - { label: "Include", hotkey: "+" }, + { + label: "Include", + hotkey: "+", + onPress: () => { + navigate(CLIPage.IncludeNode); + }, + }, { label: "Exclude", hotkey: "-", @@ -198,13 +211,39 @@ export const DevicesPage: React.FC = () => { navigate(CLIPage.ExcludeNode); }, }, - { label: "Replace failed", hotkey: "r" }, + { + label: "Replace failed", + hotkey: "r", + onPress: async () => { + const nodeId = await queryInput( + "Replace failed → Node ID", + { + inline: true, + }, + ); + if (!nodeId) return; + const nodeIdNum = parseInt(nodeId, 10); + if ( + !Number.isNaN(nodeIdNum) && + nodeIDs.includes(nodeIdNum) + ) { + // navigate(CLIPage.ReplaceFailedNode, { + // nodeId: nodeIdNum, + // }); + } else { + showError("Node not found"); + } + }, + }, { label: "Remove failed", hotkey: "f", onPress: async () => { const nodeId = await queryInput( - "Enter ID of the node to remove", + "Remove failed → Node ID", + { + inline: true, + }, ); if (!nodeId) return; const nodeIdNum = parseInt(nodeId, 10); @@ -215,6 +254,8 @@ export const DevicesPage: React.FC = () => { navigate(CLIPage.RemoveFailedNode, { nodeId: nodeIdNum, }); + } else { + showError("Node not found"); } }, }, diff --git a/packages/cli/src/pages/IncludeNode.tsx b/packages/cli/src/pages/IncludeNode.tsx new file mode 100644 index 000000000000..db9bd22a545f --- /dev/null +++ b/packages/cli/src/pages/IncludeNode.tsx @@ -0,0 +1,170 @@ +import { SecurityClass } from "@zwave-js/core"; +import { getEnumMemberName, getErrorMessage } from "@zwave-js/shared"; +import { Box, Text, useInput } from "ink"; +import SelectInput from "ink-select-input"; +import Spinner from "ink-spinner"; +import { useCallback, useState } from "react"; +import { InclusionStrategy } from "zwave-js"; +import { Center } from "../components/Center.js"; +import { useDialogs } from "../hooks/useDialogs.js"; +import { useControllerEvent, useDriver } from "../hooks/useDriver.js"; +import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; + +export interface IncludeNodePageProps {} + +enum IncludeNodeStep { + SelectStrategy, + PushDaButton, +} + +const inclusionStrategies = [ + { + label: "Default", + value: InclusionStrategy.Default, + }, + { + label: "No Encryption", + value: InclusionStrategy.Insecure, + }, +]; + +// We need to split this into two components, else querying the DSK will +// reset the local state because the component gets unmounted + +export const IncludeNodePage: React.FC = (props) => { + const { driver } = useDriver(); + const { navigate } = useNavigation(); + const { showError, showWarning, showSuccess, queryInput } = useDialogs(); + + const [step, setStep] = useState(IncludeNodeStep.SelectStrategy); + const [nodeId, setNodeId] = useState(); + + useInput(async (input, key) => { + if (key.escape) { + await driver.controller.stopInclusion(); + navigate(CLIPage.MainMenu); + } + }); + + useControllerEvent("inclusion failed", () => { + showError("Inclusion failed!"); + navigate(CLIPage.MainMenu); + }); + + useControllerEvent("node found", (node) => { + navigate(CLIPage.BootstrappingNode, { nodeId: node.id }); + }); + + const selectStrategy = useCallback( + async (strategy: typeof inclusionStrategies[number]) => { + setStep(IncludeNodeStep.PushDaButton); + try { + const result = await driver.controller.beginInclusion({ + strategy: strategy.value as any, + userCallbacks: { + async grantSecurityClasses(requested) { + // TODO: Ask user + return requested; + }, + async validateDSKAndEnterPIN(dsk) { + const pin = await queryInput( + `Please enter S2 PIN and verify DSK: _____${dsk}`, + ); + return pin || false; + }, + async abort() { + navigate(CLIPage.MainMenu); + }, + }, + }); + if (result) { + setStep(IncludeNodeStep.PushDaButton); + } else { + showError("Failed to start inclusion!"); + navigate(CLIPage.MainMenu); + } + } catch (e) { + showError(`Failed to start inclusion: ${getErrorMessage(e)}`); + navigate(CLIPage.MainMenu); + } + }, + [driver.controller], + ); + + switch (step) { + case IncludeNodeStep.SelectStrategy: + return ( +
+ + Select the inclusion strategy: + + + Press ESCAPE to cancel. + + +
+ ); + + case IncludeNodeStep.PushDaButton: + return ( +
+ + + + {" "} + Inclusion started, push the button on the device to + include it. + + + Press ESCAPE to cancel. + +
+ ); + } + + return We shouldn't be here!; +}; + +export interface BootstrappingNodePageProps { + nodeId: number; +} + +export const BootstrappingNodePage: React.FC = ( + props, +) => { + const { navigate } = useNavigation(); + const { showWarning, showSuccess } = useDialogs(); + + useControllerEvent("node added", (node, result) => { + if (result.lowSecurity) { + showWarning( + `Node ${node.id} was added with lower than intended security!`, + ); + } else { + let message: string = `Node ${node.id} was added!`; + const secClass = node.getHighestSecurityClass(); + if (secClass && secClass > SecurityClass.None) { + message += ` Security class: ${getEnumMemberName( + SecurityClass, + secClass, + )}`; + } + showSuccess(message); + } + navigate(CLIPage.MainMenu); + }); + + return ( +
+ + + + {" "} + Found node {props.nodeId}, bootstrapping... + +
+ ); +}; diff --git a/packages/cli/src/pages/StartingDriver.tsx b/packages/cli/src/pages/StartingDriver.tsx index 4016186e4099..f07002df49a3 100644 --- a/packages/cli/src/pages/StartingDriver.tsx +++ b/packages/cli/src/pages/StartingDriver.tsx @@ -1,7 +1,8 @@ -import { Box, Text } from "ink"; +import { Text } from "ink"; import Spinner from "ink-spinner"; import { useCallback, useEffect, useState } from "react"; import type { Driver } from "zwave-js"; +import { Center } from "../components/Center.js"; import { useDialogs } from "../hooks/useDialogs.js"; import { useDriver } from "../hooks/useDriver.js"; import { useGlobals } from "../hooks/useGlobals.js"; @@ -62,13 +63,13 @@ export const StartingDriverPage: React.FC = () => { }, []); return ( - +
{" "} {message} - +
); }; diff --git a/yarn.lock b/yarn.lock index 5cac323f43ee..477ae8c08b55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1827,6 +1827,7 @@ __metadata: esbuild: 0.15.7 esbuild-register: ^3.4.2 ink: ^4.0.0 + ink-select-input: ^5.0.0 ink-spinner: ^5.0.0 ink-text-input: ^5.0.0 ink-use-stdout-dimensions: ^1.0.5 @@ -2456,6 +2457,13 @@ __metadata: languageName: node linkType: hard +"arr-rotate@npm:^1.0.0": + version: 1.0.0 + resolution: "arr-rotate@npm:1.0.0" + checksum: f996e94a7b8325c23fe3d7bf95f4f1a5fd1baba34c6bcebb5a8bd0f9b955569293f4cc61f02b0a242380923fca235948e95f6dbf544a6f183207d80e8f2d442d + languageName: node + linkType: hard + "array-find-index@npm:^1.0.1": version: 1.0.2 resolution: "array-find-index@npm:1.0.2" @@ -4451,6 +4459,16 @@ __metadata: languageName: node linkType: hard +"figures@npm:^5.0.0": + version: 5.0.0 + resolution: "figures@npm:5.0.0" + dependencies: + escape-string-regexp: ^5.0.0 + is-unicode-supported: ^1.2.0 + checksum: e6e8b6d1df2f554d4effae4a5ceff5d796f9449f6d4e912d74dab7d5f25916ecda6c305b9084833157d56485a0c78b37164430ddc5675bcee1330e346710669e + languageName: node + linkType: hard + "file-entry-cache@npm:^6.0.1": version: 6.0.1 resolution: "file-entry-cache@npm:6.0.1" @@ -5236,6 +5254,20 @@ __metadata: languageName: node linkType: hard +"ink-select-input@npm:^5.0.0": + version: 5.0.0 + resolution: "ink-select-input@npm:5.0.0" + dependencies: + arr-rotate: ^1.0.0 + figures: ^5.0.0 + lodash.isequal: ^4.5.0 + peerDependencies: + ink: ^4.0.0 + react: ^18.0.0 + checksum: 9e21351f7ae1ad1721eca04038e25afd4b6f85ea0abd51235db2ed6894e209198687924a2ae8aca972571cb0601d735810c462676b7ebfd68ae3cafd0d67613c + languageName: node + linkType: hard + "ink-spinner@npm:^5.0.0": version: 5.0.0 resolution: "ink-spinner@npm:5.0.0" From c74163d941570261bf89de0388711dcc8b9ea6e8 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Sat, 11 Mar 2023 23:38:37 +0100 Subject: [PATCH 19/27] fix: confirm exit layout --- .vscode/launch.json | 1 + packages/cli/src/cli.tsx | 6 +++++- packages/cli/src/pages/ConfirmExit.tsx | 7 ++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2cfecc532882..3a22fe963e0e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,6 +15,7 @@ "--start" ], "env": { + "NODE_ENV": "development" // "DEV": "true" }, "skipFiles": ["/**"], diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 024cbdc7bfd2..87ddd8a5a857 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -217,7 +217,11 @@ const CLI: React.FC = () => { } height={ rows - - (layout === "horizontal" ? 4 : 10) + (process.env.NODE_ENV === "development" + ? layout === "horizontal" + ? 4 + : 10 + : 0) } width={columns} paddingY={1} diff --git a/packages/cli/src/pages/ConfirmExit.tsx b/packages/cli/src/pages/ConfirmExit.tsx index c8baeaa7c6e8..ea1838a5a583 100644 --- a/packages/cli/src/pages/ConfirmExit.tsx +++ b/packages/cli/src/pages/ConfirmExit.tsx @@ -1,4 +1,5 @@ -import { Box, Text, useApp, useInput } from "ink"; +import { Text, useApp, useInput } from "ink"; +import { Center } from "../components/Center.js"; import { useDriver } from "../hooks/useDriver.js"; import { useNavigation } from "../hooks/useNavigation.js"; @@ -17,12 +18,12 @@ export const ConfirmExitPage: React.FC = () => { }); return ( - +
Are you sure you want to exit? Press RETURN to exit, or{" "} ESCAPE to cancel. - +
); }; From 611cbfdfd0070379c32929c5f31c3270ded650ff Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Mon, 13 Mar 2023 13:25:47 +0100 Subject: [PATCH 20/27] feat: replace node, re-interview --- packages/cli/src/cli.tsx | 42 +++-- packages/cli/src/hooks/useNavigation.ts | 24 ++- packages/cli/src/pages/BootstrappingNode.tsx | 49 ++++++ packages/cli/src/pages/DeviceDetails.tsx | 37 ++++ .../pages/{Devices.tsx => DeviceOverview.tsx} | 165 ++++++++++++++---- packages/cli/src/pages/ExcludeNode.tsx | 12 +- packages/cli/src/pages/IncludeNode.tsx | 63 ++----- packages/cli/src/pages/MainMenu.tsx | 63 ------- packages/cli/src/pages/RemoveFailedNode.tsx | 2 +- packages/cli/src/pages/ReplaceFailedNode.tsx | 163 +++++++++++++++++ packages/cli/src/pages/StartingDriver.tsx | 4 +- 11 files changed, 441 insertions(+), 183 deletions(-) create mode 100644 packages/cli/src/pages/BootstrappingNode.tsx create mode 100644 packages/cli/src/pages/DeviceDetails.tsx rename packages/cli/src/pages/{Devices.tsx => DeviceOverview.tsx} (64%) delete mode 100644 packages/cli/src/pages/MainMenu.tsx create mode 100644 packages/cli/src/pages/ReplaceFailedNode.tsx diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 87ddd8a5a857..bf6651c6e1c1 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,4 +1,4 @@ -import { Box, render, Text, useInput } from "ink"; +import { Box, render, Spacer, Text, useInput } from "ink"; import { useCallback, useEffect, useState } from "react"; import type { Driver } from "zwave-js"; import { Frame } from "./components/Frame.js"; @@ -144,6 +144,7 @@ const CLI: React.FC = () => { ); const [menuItemSlots, updateMenuItems] = useMenuItemSlots(defaultMenuItems); + const menuVisible = !modalState || modalState.type === "queryInline"; // Prevent the app from exiting automatically useInput(() => { @@ -211,9 +212,9 @@ const CLI: React.FC = () => { }} > { flexDirection="column" flexGrow={1} alignItems="stretch" - justifyContent="center" + // justifyContent="center" > {/* TODO: This should be merged into `selectPage` */} {cliPage.page === @@ -269,20 +270,25 @@ const CLI: React.FC = () => { {modalState?.type === "queryInline" && ( - - {modalState.message} - + <> + + + {modalState.message} + + )}
{logVisible && ( diff --git a/packages/cli/src/hooks/useNavigation.ts b/packages/cli/src/hooks/useNavigation.ts index 8c12a87c5ae4..bd3a55a58a34 100644 --- a/packages/cli/src/hooks/useNavigation.ts +++ b/packages/cli/src/hooks/useNavigation.ts @@ -1,15 +1,15 @@ import React from "react"; import { SetUSBPath } from "../components/setUSBPath.js"; +import { BootstrappingNodePage } from "../pages/BootstrappingNode.js"; import { ConfirmExitPage } from "../pages/ConfirmExit.js"; import { DestroyingDriverPage } from "../pages/DestroyingDriver.js"; +import { DeviceDetailsPage } from "../pages/DeviceDetails.js"; +import { DeviceOverviewPage } from "../pages/DeviceOverview.js"; import { ExcludeNodePage } from "../pages/ExcludeNode.js"; -import { - BootstrappingNodePage, - IncludeNodePage, -} from "../pages/IncludeNode.js"; -import { MainMenuPage } from "../pages/MainMenu.js"; +import { IncludeNodePage } from "../pages/IncludeNode.js"; import { PreparePage } from "../pages/Prepare.js"; import { RemoveFailedNodePage } from "../pages/RemoveFailedNode.js"; +import { ReplaceFailedNodePage } from "../pages/ReplaceFailedNode.js"; import { StartingDriverPage } from "../pages/StartingDriver.js"; export enum CLIPage { @@ -18,11 +18,13 @@ export enum CLIPage { StartingDriver, DestroyingDriver, - MainMenu, + DeviceOverview, + DeviceDetails, IncludeNode, BootstrappingNode, ExcludeNode, + ReplaceFailedNode, RemoveFailedNode, ConfirmExit, @@ -59,16 +61,20 @@ export function getPageComponent(cliPage: CLIPage): React.FC | undefined { case CLIPage.DestroyingDriver: return DestroyingDriverPage; - case CLIPage.MainMenu: - return MainMenuPage; + case CLIPage.DeviceOverview: + return DeviceOverviewPage; + case CLIPage.DeviceDetails: + return DeviceDetailsPage; case CLIPage.IncludeNode: return IncludeNodePage; case CLIPage.BootstrappingNode: return BootstrappingNodePage; - case CLIPage.ExcludeNode: return ExcludeNodePage; + + case CLIPage.ReplaceFailedNode: + return ReplaceFailedNodePage; case CLIPage.RemoveFailedNode: return RemoveFailedNodePage; diff --git a/packages/cli/src/pages/BootstrappingNode.tsx b/packages/cli/src/pages/BootstrappingNode.tsx new file mode 100644 index 000000000000..861ad7a3eca2 --- /dev/null +++ b/packages/cli/src/pages/BootstrappingNode.tsx @@ -0,0 +1,49 @@ +import { SecurityClass } from "@zwave-js/core"; +import { getEnumMemberName } from "@zwave-js/shared"; +import { Text } from "ink"; +import Spinner from "ink-spinner"; +import { Center } from "../components/Center.js"; +import { useDialogs } from "../hooks/useDialogs.js"; +import { useControllerEvent } from "../hooks/useDriver.js"; +import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; + +export interface BootstrappingNodePageProps { + nodeId: number; +} + +export const BootstrappingNodePage: React.FC = ( + props, +) => { + const { navigate } = useNavigation(); + const { showWarning, showSuccess } = useDialogs(); + + useControllerEvent("node added", (node, result) => { + if (result.lowSecurity) { + showWarning( + `Node ${node.id} was added with lower than intended security!`, + ); + } else { + let message: string = `Node ${node.id} was added!`; + const secClass = node.getHighestSecurityClass(); + if (secClass && secClass > SecurityClass.None) { + message += ` Security class: ${getEnumMemberName( + SecurityClass, + secClass, + )}`; + } + showSuccess(message); + } + navigate(CLIPage.DeviceOverview); + }); + + return ( +
+ + + + {" "} + Found node {props.nodeId}, bootstrapping... + +
+ ); +}; diff --git a/packages/cli/src/pages/DeviceDetails.tsx b/packages/cli/src/pages/DeviceDetails.tsx new file mode 100644 index 000000000000..90a1a3c5a0db --- /dev/null +++ b/packages/cli/src/pages/DeviceDetails.tsx @@ -0,0 +1,37 @@ +import { Text } from "ink"; +import { HotkeyLabel } from "../components/HotkeyLabel.js"; +import { useMenu, type MenuItem } from "../hooks/useMenu.js"; +import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; +import { + destroyDriverMenuItem, + exitMenuItem, + toggleLogMenuItem, +} from "../lib/menu.js"; + +export interface DeviceDetailsPageProps { + nodeId: number; +} + +export const DeviceDetailsPage: React.FC = (props) => { + const { navigate } = useNavigation(); + + const deviceMenuItem: MenuItem = { + location: "bottomLeft", + item: ( + navigate(CLIPage.DeviceOverview)} + /> + ), + }; + + useMenu([ + deviceMenuItem, + toggleLogMenuItem, + destroyDriverMenuItem, + exitMenuItem, + ]); + + return {props.nodeId}; +}; diff --git a/packages/cli/src/pages/Devices.tsx b/packages/cli/src/pages/DeviceOverview.tsx similarity index 64% rename from packages/cli/src/pages/Devices.tsx rename to packages/cli/src/pages/DeviceOverview.tsx index 1c046629e281..64dddad69ef0 100644 --- a/packages/cli/src/pages/Devices.tsx +++ b/packages/cli/src/pages/DeviceOverview.tsx @@ -1,25 +1,46 @@ import { AllColumnProps, CellProps, Table } from "@alcalzone/ink-table"; import { Box, Text } from "ink"; +import Spinner from "ink-spinner"; import { useEffect, useState } from "react"; -import { DeviceClass, NodeStatus, ZWaveNode } from "zwave-js"; +import { + DeviceClass, + getEnumMemberName, + InterviewStage, + NodeStatus, + ZWaveNode, +} from "zwave-js"; import { CommandPalette } from "../components/CommandPalette.js"; import { HotkeyLabel } from "../components/HotkeyLabel.js"; import { useDialogs } from "../hooks/useDialogs.js"; import { useDriver } from "../hooks/useDriver.js"; import { useForceRerender } from "../hooks/useForceRerender.js"; +import { useMenu } from "../hooks/useMenu.js"; import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; +import { + destroyDriverMenuItem, + exitMenuItem, + toggleLogMenuItem, +} from "../lib/menu.js"; const okText = "✓"; const nokText = "✗"; const statusTexts = { Unknown: ["?", "gray"], - Alive: ["●", "blueBright"], - Dead: ["☠", "red"], - Awake: ["☻", "blueBright"], - Asleep: ["z", "yellow"], + Alive: [okText, "greenBright"], + Dead: [nokText, "red"], + Awake: ["awake", "blueBright"], + Asleep: ["asleep", "yellow"], } as const; +const interviewTexts = { + None: ["█⬝⬝⬝", "gray"], + ProtocolInfo: ["██⬝⬝", "gray"], + NodeInfo: ["███⬝", "gray"], + CommandClasses: ["████", "gray"], + Complete: [okText, "green"], +}; + const unknownText = "(unknown)"; const Cell: ((props: CellProps) => JSX.Element) | undefined = ({ @@ -48,17 +69,67 @@ const Cell: ((props: CellProps) => JSX.Element) | undefined = ({ typeof children === "string" && children.trim().length === 1 ) { + const originalLength = children.length; const trimmed = children.trim(); const status = statusTexts[NodeStatus[trimmed as any] as keyof typeof statusTexts]; - return ( - - {status ? children.replace(trimmed, status?.[0]) : children} - - ); - } else { - return {children}; + if (status) { + const newLength = status[0].length; + const padL = " ".repeat( + Math.floor((originalLength - newLength) / 2), + ); + const padR = " ".repeat(originalLength - newLength - padL.length); + return ( + + {padL} + {status[0]} + {padR} + + ); + } + } else if ( + column === 5 /* Interview Stage */ && + typeof children === "string" && + children.trim().length === 1 + ) { + const originalLength = children.length; + const trimmed = children.trim(); + const stage: InterviewStage = parseInt(trimmed) as any; + const text = + interviewTexts[ + getEnumMemberName( + InterviewStage, + stage, + ) as keyof typeof interviewTexts + ]; + if (text) { + const newLength = + text[0].length + (stage < InterviewStage.Complete ? 2 : 0); + const padL = " ".repeat( + Math.floor((originalLength - newLength) / 2), + ); + const padR = " ".repeat(originalLength - newLength - padL.length); + return ( + <> + {padL} + {stage < InterviewStage.Complete && ( + + {" "} + + )} + + {text?.[0]} + {padR} + + + ); + } } + + return {children}; }; function getDeviceType(cls: DeviceClass | undefined): string { @@ -85,12 +156,14 @@ function getModel(node: ZWaveNode): string { return [mfg, model, desc].filter((x) => !!x).join(" "); } -export const DevicesPage: React.FC = () => { +export const DeviceOverviewPage: React.FC = () => { const { driver } = useDriver(); const forceRerender = useForceRerender(); const { queryInput, showError } = useDialogs(); const { navigate } = useNavigation(); + useMenu([toggleLogMenuItem, destroyDriverMenuItem, exitMenuItem]); + const [maxRows, setMaxRows] = useState(10); const [nodeIDs, setNodeIDs] = useState([ @@ -105,8 +178,9 @@ export const DevicesPage: React.FC = () => { "#": node.id, Model: getCustomName(node) || getModel(node) || unknownText, Type: getDeviceType(node.deviceClass), - R: node.ready ? "✓" : "✗", - S: node.status, + Ready: node.ready ? "✓" : "✗", + Status: node.status, + Interview: node.interviewStage, })); // Register event handlers to update the table @@ -122,7 +196,10 @@ export const DevicesPage: React.FC = () => { .on("alive", forceRerender) .on("dead", forceRerender) .on("sleep", forceRerender) - .on("wake up", forceRerender); + .on("wake up", forceRerender) + .on("interview started", forceRerender) + .on("interview stage completed", forceRerender) + .on("interview completed", forceRerender); }; const removeNodeEventHandlers = (node: ZWaveNode) => { node.off("interview started", forceRerender) @@ -131,7 +208,10 @@ export const DevicesPage: React.FC = () => { .off("alive", forceRerender) .off("dead", forceRerender) .off("sleep", forceRerender) - .off("wake up", forceRerender); + .off("wake up", forceRerender) + .off("interview started", forceRerender) + .off("interview stage completed", forceRerender) + .off("interview completed", forceRerender); }; const nodeAdded = (node: ZWaveNode) => { @@ -161,8 +241,9 @@ export const DevicesPage: React.FC = () => { { key: "#", align: "right" }, { key: "Model" }, { key: "Type", align: "center" }, - { key: "R" }, - { key: "S" }, + { key: "Ready", align: "center" }, + { key: "Status", align: "center" }, + { key: "Interview", align: "center" }, ]; return ( @@ -180,9 +261,7 @@ export const DevicesPage: React.FC = () => { onPress: async () => { const nodeId = await queryInput( "Select → Node ID", - { - inline: true, - }, + { inline: true }, ); if (!nodeId) return; const nodeIdNum = parseInt(nodeId, 10); @@ -190,8 +269,9 @@ export const DevicesPage: React.FC = () => { !Number.isNaN(nodeIdNum) && nodeIDs.includes(nodeIdNum) ) { - // TODO: Select node ID - // throw new Error(`Node ${nodeIdNum} selected`); + navigate(CLIPage.DeviceDetails, { + nodeId: nodeIdNum, + }); } else { showError("Node not found"); } @@ -217,9 +297,7 @@ export const DevicesPage: React.FC = () => { onPress: async () => { const nodeId = await queryInput( "Replace failed → Node ID", - { - inline: true, - }, + { inline: true }, ); if (!nodeId) return; const nodeIdNum = parseInt(nodeId, 10); @@ -227,9 +305,9 @@ export const DevicesPage: React.FC = () => { !Number.isNaN(nodeIdNum) && nodeIDs.includes(nodeIdNum) ) { - // navigate(CLIPage.ReplaceFailedNode, { - // nodeId: nodeIdNum, - // }); + navigate(CLIPage.ReplaceFailedNode, { + nodeId: nodeIdNum, + }); } else { showError("Node not found"); } @@ -241,9 +319,7 @@ export const DevicesPage: React.FC = () => { onPress: async () => { const nodeId = await queryInput( "Remove failed → Node ID", - { - inline: true, - }, + { inline: true }, ); if (!nodeId) return; const nodeIdNum = parseInt(nodeId, 10); @@ -259,6 +335,29 @@ export const DevicesPage: React.FC = () => { } }, }, + { + label: "Re-Interview", + hotkey: "i", + onPress: async () => { + const nodeId = await queryInput( + "Re-Interview → Node ID", + { inline: true }, + ); + if (!nodeId) return; + const nodeIdNum = parseInt(nodeId, 10); + if ( + !Number.isNaN(nodeIdNum) && + nodeIDs.includes(nodeIdNum) + ) { + driver.controller.nodes + .get(nodeIdNum) + ?.refreshInfo() + .catch(() => {}); + } else { + showError("Node not found"); + } + }, + }, ]} > diff --git a/packages/cli/src/pages/ExcludeNode.tsx b/packages/cli/src/pages/ExcludeNode.tsx index 9756f9ab2b0f..4b0382174b45 100644 --- a/packages/cli/src/pages/ExcludeNode.tsx +++ b/packages/cli/src/pages/ExcludeNode.tsx @@ -19,7 +19,7 @@ export const ExcludeNodePage: React.FC = (props) => { useInput(async (input, key) => { if (key.escape) { await driver.controller.stopExclusion(); - navigate(CLIPage.MainMenu); + navigate(CLIPage.DeviceOverview); } }); @@ -35,27 +35,27 @@ export const ExcludeNodePage: React.FC = (props) => { ); } else { showError("Failed to start exclusion!"); - navigate(CLIPage.MainMenu); + navigate(CLIPage.DeviceOverview); } } catch (e) { showError(`Failed to start exclusion: ${getErrorMessage(e)}`); - navigate(CLIPage.MainMenu); + navigate(CLIPage.DeviceOverview); } })(); }, []); // useControllerEvent("exclusion stopped", () => { - // navigate(CLIPage.MainMenu); + // navigate(CLIPage.DeviceOverview); // }); useControllerEvent("exclusion failed", () => { showError("Exclusion failed!"); - navigate(CLIPage.MainMenu); + navigate(CLIPage.DeviceOverview); }); useControllerEvent("node removed", (node) => { showSuccess(`Node ${node.id} was removed!`); - navigate(CLIPage.MainMenu); + navigate(CLIPage.DeviceOverview); }); return ( diff --git a/packages/cli/src/pages/IncludeNode.tsx b/packages/cli/src/pages/IncludeNode.tsx index db9bd22a545f..61085c09686f 100644 --- a/packages/cli/src/pages/IncludeNode.tsx +++ b/packages/cli/src/pages/IncludeNode.tsx @@ -1,5 +1,4 @@ -import { SecurityClass } from "@zwave-js/core"; -import { getEnumMemberName, getErrorMessage } from "@zwave-js/shared"; +import { getErrorMessage } from "@zwave-js/shared"; import { Box, Text, useInput } from "ink"; import SelectInput from "ink-select-input"; import Spinner from "ink-spinner"; @@ -22,6 +21,10 @@ const inclusionStrategies = [ label: "Default", value: InclusionStrategy.Default, }, + { + label: "Security S0 (legacy)", + value: InclusionStrategy.Security_S0, + }, { label: "No Encryption", value: InclusionStrategy.Insecure, @@ -31,24 +34,23 @@ const inclusionStrategies = [ // We need to split this into two components, else querying the DSK will // reset the local state because the component gets unmounted -export const IncludeNodePage: React.FC = (props) => { +export const IncludeNodePage: React.FC = () => { const { driver } = useDriver(); const { navigate } = useNavigation(); - const { showError, showWarning, showSuccess, queryInput } = useDialogs(); + const { showError, queryInput } = useDialogs(); const [step, setStep] = useState(IncludeNodeStep.SelectStrategy); - const [nodeId, setNodeId] = useState(); useInput(async (input, key) => { if (key.escape) { await driver.controller.stopInclusion(); - navigate(CLIPage.MainMenu); + navigate(CLIPage.DeviceOverview); } }); useControllerEvent("inclusion failed", () => { showError("Inclusion failed!"); - navigate(CLIPage.MainMenu); + navigate(CLIPage.DeviceOverview); }); useControllerEvent("node found", (node) => { @@ -73,7 +75,7 @@ export const IncludeNodePage: React.FC = (props) => { return pin || false; }, async abort() { - navigate(CLIPage.MainMenu); + navigate(CLIPage.DeviceOverview); }, }, }); @@ -81,11 +83,11 @@ export const IncludeNodePage: React.FC = (props) => { setStep(IncludeNodeStep.PushDaButton); } else { showError("Failed to start inclusion!"); - navigate(CLIPage.MainMenu); + navigate(CLIPage.DeviceOverview); } } catch (e) { showError(`Failed to start inclusion: ${getErrorMessage(e)}`); - navigate(CLIPage.MainMenu); + navigate(CLIPage.DeviceOverview); } }, [driver.controller], @@ -127,44 +129,3 @@ export const IncludeNodePage: React.FC = (props) => { return We shouldn't be here!; }; - -export interface BootstrappingNodePageProps { - nodeId: number; -} - -export const BootstrappingNodePage: React.FC = ( - props, -) => { - const { navigate } = useNavigation(); - const { showWarning, showSuccess } = useDialogs(); - - useControllerEvent("node added", (node, result) => { - if (result.lowSecurity) { - showWarning( - `Node ${node.id} was added with lower than intended security!`, - ); - } else { - let message: string = `Node ${node.id} was added!`; - const secClass = node.getHighestSecurityClass(); - if (secClass && secClass > SecurityClass.None) { - message += ` Security class: ${getEnumMemberName( - SecurityClass, - secClass, - )}`; - } - showSuccess(message); - } - navigate(CLIPage.MainMenu); - }); - - return ( -
- - - - {" "} - Found node {props.nodeId}, bootstrapping... - -
- ); -}; diff --git a/packages/cli/src/pages/MainMenu.tsx b/packages/cli/src/pages/MainMenu.tsx deleted file mode 100644 index 031b5e2a22af..000000000000 --- a/packages/cli/src/pages/MainMenu.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { Box } from "ink"; -import { useState } from "react"; -import { HotkeyLabel } from "../components/HotkeyLabel.js"; -import { MenuItem, useMenu } from "../hooks/useMenu.js"; -import { - destroyDriverMenuItem, - exitMenuItem, - toggleLogMenuItem, -} from "../lib/menu.js"; -import { DevicesPage } from "./Devices.js"; - -export interface MainMenuPageProps { - // TODO: -} - -enum MainMenuSubPage { - // None, - Devices, -} - -export const MainMenuPage: React.FC = (props) => { - const [page, setPage] = useState(MainMenuSubPage.Devices); - - // const backMenuItem: MenuItem = { - // location: "bottomLeft", - // item: ( - // { - // setPage(MainMenuSubPage.None); - // }} - // /> - // ), - // }; - - const devicesMenuItem: MenuItem = { - location: "bottomLeft", - item: ( - { - setPage(MainMenuSubPage.Devices); - }} - /> - ), - }; - - useMenu([ - page !== MainMenuSubPage.Devices && devicesMenuItem, - // page !== MainMenuSubPage.Devices && backMenuItem, - toggleLogMenuItem, - destroyDriverMenuItem, - exitMenuItem, - ]); - - return ( - - {page === MainMenuSubPage.Devices && } - - ); -}; diff --git a/packages/cli/src/pages/RemoveFailedNode.tsx b/packages/cli/src/pages/RemoveFailedNode.tsx index baeb8be89a06..59a2f1ce12e9 100644 --- a/packages/cli/src/pages/RemoveFailedNode.tsx +++ b/packages/cli/src/pages/RemoveFailedNode.tsx @@ -25,7 +25,7 @@ export const RemoveFailedNodePage: React.FC = ( } catch (e: any) { showError(`Failed to remove node: ${e.message}`); } finally { - navigate(CLIPage.MainMenu); + navigate(CLIPage.DeviceOverview); } })(); }, []); diff --git a/packages/cli/src/pages/ReplaceFailedNode.tsx b/packages/cli/src/pages/ReplaceFailedNode.tsx new file mode 100644 index 000000000000..b92a790ee7ac --- /dev/null +++ b/packages/cli/src/pages/ReplaceFailedNode.tsx @@ -0,0 +1,163 @@ +import { getErrorMessage } from "@zwave-js/shared"; +import { Box, Text, useInput } from "ink"; +import SelectInput from "ink-select-input"; +import Spinner from "ink-spinner"; +import { useCallback, useState } from "react"; +import { InclusionStrategy } from "zwave-js"; +import { Center } from "../components/Center.js"; +import { useDialogs } from "../hooks/useDialogs.js"; +import { useControllerEvent, useDriver } from "../hooks/useDriver.js"; +import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; + +export interface ReplaceFailedNodePageProps { + nodeId: number; +} + +enum ReplaceFailedNodeStep { + SelectStrategy, + Replacing, + PushDaButton, +} + +const replaceStrategies = [ + { + label: "Security S2", + value: InclusionStrategy.Security_S2, + }, + { + label: "Security S0", + value: InclusionStrategy.Security_S0, + }, + { + label: "No Encryption", + value: InclusionStrategy.Insecure, + }, +]; + +// We need to split this into two components, else querying the DSK will +// reset the local state because the component gets unmounted + +export const ReplaceFailedNodePage: React.FC = ( + props, +) => { + const { driver } = useDriver(); + const { navigate } = useNavigation(); + const { showError, queryInput } = useDialogs(); + + const [step, setStep] = useState(ReplaceFailedNodeStep.SelectStrategy); + + useInput(async (input, key) => { + if (key.escape) { + if (step === ReplaceFailedNodeStep.Replacing) { + // Too late + return; + } + if (step === ReplaceFailedNodeStep.PushDaButton) { + await driver.controller.stopInclusion(); + } + navigate(CLIPage.DeviceOverview); + } + }); + + useControllerEvent("inclusion failed", () => { + showError("Inclusion failed!"); + navigate(CLIPage.DeviceOverview); + }); + + useControllerEvent("node found", (node) => { + navigate(CLIPage.BootstrappingNode, { nodeId: node.id }); + }); + + const selectStrategy = useCallback( + async (strategy: typeof replaceStrategies[number]) => { + setStep(ReplaceFailedNodeStep.Replacing); + try { + const result = await driver.controller.replaceFailedNode( + props.nodeId, + { + strategy: strategy.value as any, + userCallbacks: { + async grantSecurityClasses(requested) { + // TODO: Ask user + return requested; + }, + async validateDSKAndEnterPIN(dsk) { + const pin = await queryInput( + `Please enter S2 PIN and verify DSK: _____${dsk}`, + ); + return pin || false; + }, + async abort() { + navigate(CLIPage.DeviceOverview); + }, + }, + }, + ); + if (result) { + setStep(ReplaceFailedNodeStep.PushDaButton); + } else { + showError( + "Could not replace node - the controller is busy!", + ); + navigate(CLIPage.DeviceOverview); + } + } catch (e) { + showError( + `Failed to replace node ${props.nodeId}: ${getErrorMessage( + e, + )}`, + ); + navigate(CLIPage.DeviceOverview); + } + }, + [driver.controller], + ); + + switch (step) { + case ReplaceFailedNodeStep.SelectStrategy: + return ( +
+ + Select how to include the replacement node: + + + Press ESCAPE to cancel. + + +
+ ); + + case ReplaceFailedNodeStep.Replacing: + return ( +
+ + + + {" "} + The node is being replaced, please wait... + +
+ ); + + case ReplaceFailedNodeStep.PushDaButton: + return ( +
+ + + + {" "} + Ready to include the new device, push the button on the + device to include it. + + + Press ESCAPE to cancel. + +
+ ); + } + + return We shouldn't be here!; +}; diff --git a/packages/cli/src/pages/StartingDriver.tsx b/packages/cli/src/pages/StartingDriver.tsx index f07002df49a3..f64572399b9f 100644 --- a/packages/cli/src/pages/StartingDriver.tsx +++ b/packages/cli/src/pages/StartingDriver.tsx @@ -27,7 +27,7 @@ export const StartingDriverPage: React.FC = () => { [showError], ); const onDriverReady = useCallback((driver: Driver) => { - navigate(CLIPage.MainMenu); + navigate(CLIPage.DeviceOverview); }, []); const onBootloaderReady = useCallback((driver: Driver) => { setMessage("driver stuck in bootloader mode"); @@ -37,7 +37,7 @@ export const StartingDriverPage: React.FC = () => { // When opening this page, try to start the driver useEffect(() => { if (driver) { - navigate(CLIPage.MainMenu); + navigate(CLIPage.DeviceOverview); return; } else if (!usbPath) { navigate(CLIPage.Prepare); From fb03359af4c1dc18aee8791387d79972f50fd1f4 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Mon, 13 Mar 2023 14:36:05 +0100 Subject: [PATCH 21/27] feat: run scripts --- packages/cli/package.json | 1 + packages/cli/script.d.ts | 10 + packages/cli/src/cli.tsx | 382 +++++++++++--------- packages/cli/src/components/HotkeyLabel.tsx | 3 +- packages/cli/src/components/menu.tsx | 54 --- packages/cli/src/hooks/useActions.ts | 11 - packages/cli/src/hooks/useDialogs.ts | 6 +- packages/cli/src/lib/menu.tsx | 28 +- test.mjs | 7 + 9 files changed, 253 insertions(+), 249 deletions(-) create mode 100644 packages/cli/script.d.ts delete mode 100644 packages/cli/src/components/menu.tsx delete mode 100644 packages/cli/src/hooks/useActions.ts create mode 100644 test.mjs diff --git a/packages/cli/package.json b/packages/cli/package.json index 684d4072d115..a10bf0644b70 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -8,6 +8,7 @@ }, "type": "module", "module": "build/cli.js", + "types": "script.d.ts", "files": [ "bin/", "build/**/*.{js,d.ts,map}" diff --git a/packages/cli/script.d.ts b/packages/cli/script.d.ts new file mode 100644 index 000000000000..94c026939e43 --- /dev/null +++ b/packages/cli/script.d.ts @@ -0,0 +1,10 @@ +import type { Driver } from "zwave-js"; +import type { IDialogsContext } from "./src/hooks/useDialogs"; + +export interface ScriptContext { + driver: Driver; + showError: IDialogsContext["showError"]; + showWarning: IDialogsContext["showWarning"]; + showSuccess: IDialogsContext["showSuccess"]; + queryInput: IDialogsContext["queryInput"]; +} diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index bf6651c6e1c1..5cd530588976 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,5 +1,6 @@ +import { getErrorMessage } from "@zwave-js/shared"; import { Box, render, Spacer, Text, useInput } from "ink"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import type { Driver } from "zwave-js"; import { Frame } from "./components/Frame.js"; import { HDivider } from "./components/HDivider.js"; @@ -12,7 +13,6 @@ import { } from "./components/Modals.js"; import { SetUSBPath } from "./components/setUSBPath.js"; import { VDivider } from "./components/VDivider.js"; -import { ActionsContext } from "./hooks/useActions.js"; import { DialogsContext } from "./hooks/useDialogs.js"; import { DriverContext } from "./hooks/useDriver.js"; import { GlobalsContext } from "./hooks/useGlobals.js"; @@ -25,7 +25,7 @@ import { } from "./hooks/useNavigation.js"; import { useStdoutDimensions } from "./hooks/useStdoutDimensions.js"; import { createLogTransport, LinesBuffer } from "./lib/logging.js"; -import { defaultMenuItems } from "./lib/menu.js"; +import { createRunScriptMenuItem, defaultMenuItems } from "./lib/menu.js"; process.on("unhandledRejection", (err) => { throw err; @@ -54,7 +54,12 @@ const CLI: React.FC = () => { }, [columns, setLayout]); const [usbPath, setUSBPath] = useState("/dev/ttyUSB0"); - const [driver, setDriver] = useState(); + + // We cannot use setState here, because this can cause driver to be undefined when the component is re-mounted + const driver = useRef(); + const setDriver = useCallback((d: Driver) => { + driver.current = d; + }, []); const [logVisible, setLogVisible] = useState(false); @@ -82,38 +87,34 @@ const CLI: React.FC = () => { }, [prevCliPage, setCLIPage, setPrevCLIPage]); const [modalState, setModalState] = useState(); - const showError = useCallback( - (message: React.ReactNode) => { - setModalState({ - type: "message", - message, - color: "red", - onSubmit: () => setModalState(undefined), + const showMessage = useCallback( + (message: React.ReactNode, color: ModalState["color"]) => { + return new Promise((resolve) => { + setModalState({ + type: "message", + message, + color, + onSubmit: () => { + setModalState(undefined); + resolve(); + }, + }); }); }, [setModalState], ); + + const showError = useCallback( + (message: React.ReactNode) => showMessage(message, "red"), + [showMessage], + ); const showWarning = useCallback( - (message: React.ReactNode) => { - setModalState({ - type: "message", - message, - color: "yellow", - onSubmit: () => setModalState(undefined), - }); - }, - [setModalState], + (message: React.ReactNode) => showMessage(message, "yellow"), + [showMessage], ); const showSuccess = useCallback( - (message: React.ReactNode) => { - setModalState({ - type: "message", - message, - color: "green", - onSubmit: () => setModalState(undefined), - }); - }, - [setModalState], + (message: React.ReactNode) => showMessage(message, "green"), + [showMessage], ); const queryInput = useCallback( ( @@ -143,7 +144,45 @@ const CLI: React.FC = () => { [setModalState], ); - const [menuItemSlots, updateMenuItems] = useMenuItemSlots(defaultMenuItems); + const runScript = async () => { + const path = await queryInput("Script path"); + if (!path) return; + + let script: any; + try { + script = await import(`${path}?t=${Date.now()}`); + } catch { + showError(`Could not load script ${path}`); + return; + } + + if (typeof script.default !== "function") { + showError( + `Script ${path} must export a function using "export default"`, + ); + return; + } + + try { + await script.default({ + driver: driver.current!, + showSuccess, + showWarning, + showError, + }); + showSuccess("Script executed successfully!"); + } catch (e) { + showError( + `Error during script execution: ${getErrorMessage(e, true)}`, + ); + return; + } + }; + + const [menuItemSlots, updateMenuItems] = useMenuItemSlots([ + ...defaultMenuItems, + createRunScriptMenuItem(runScript), + ]); const menuVisible = !modalState || modalState.type === "queryInline"; // Prevent the app from exiting automatically @@ -196,162 +235,149 @@ const CLI: React.FC = () => { back, }} > - - + - - - {(!modalState || - modalState.type === "queryInline") && ( - <> - - {/* TODO: This should be merged into `selectPage` */} - {cliPage.page === - CLIPage.SetUSBPath && ( - - setCLIPage({ - page: CLIPage.Prepare, - }) - } - onSubmit={(path) => { - setUSBPath(path); - setCLIPage({ - page: CLIPage.Prepare, - }); - }} - /> - )} - - {PageComponent && ( - - )} - - {modalState?.type === - "queryInline" && ( - <> - - - {modalState.message} - - - )} - - {logVisible && ( - + + {/* TODO: This should be merged into `selectPage` */} + {cliPage.page === + CLIPage.SetUSBPath && ( + + setCLIPage({ + page: CLIPage.Prepare, + }) } - height={ - layout === "horizontal" - ? undefined - : Math.min( - Math.floor( - rows / - 2, - ), - 30, - ) - } - > - {layout === "horizontal" ? ( - - ) : ( - - )} - - + onSubmit={(path) => { + setUSBPath(path); + setCLIPage({ + page: CLIPage.Prepare, + }); + }} + /> )} - - )} - {modalState && ( - <> - {modalState.type === "message" && ( - - {modalState.message} - + + {PageComponent && ( + )} - {modalState.type === "query" && ( - - {modalState.message} - + + {modalState?.type === + "queryInline" && ( + <> + + + {modalState.message} + + )} - - )} - - - - +
+ {logVisible && ( + + {layout === "horizontal" ? ( + + ) : ( + + )} + + + )} + + )} + {modalState && ( + <> + {modalState.type === "message" && ( + + {modalState.message} + + )} + {modalState.type === "query" && ( + + {modalState.message} + + )} + + )} + +
+
diff --git a/packages/cli/src/components/HotkeyLabel.tsx b/packages/cli/src/components/HotkeyLabel.tsx index 4d8054754ce5..b8d5389cabd4 100644 --- a/packages/cli/src/components/HotkeyLabel.tsx +++ b/packages/cli/src/components/HotkeyLabel.tsx @@ -51,7 +51,8 @@ export const HotkeyLabel: React.FC = (props) => { hotkey && (input === hotkey || (hotkey in specialKeys && (key as any)[hotkey])) && - (!modifiers || modifiers.every((mod) => key[mod])) + ((modifiers && modifiers.every((mod) => key[mod])) || + (!modifiers && !key.ctrl && !key.shift)) ) { props.onPress?.(); } diff --git a/packages/cli/src/components/menu.tsx b/packages/cli/src/components/menu.tsx deleted file mode 100644 index f034fd0b5863..000000000000 --- a/packages/cli/src/components/menu.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Box, BoxProps, Text, TextProps, useInput } from "ink"; - -export interface MenuProps { - label?: string; - layoutProps?: BoxProps; - textProps?: TextProps; - options: { - input: string; - label: string; - textProps?: TextProps; - onSelect: () => void; - }[]; -} - -export const Menu: React.FC = (props) => { - const { options } = props; - useInput((input, key) => { - const option = options.find((o) => o.input === input); - if (option) { - option.onSelect(); - } - }); - - const innerBox = ( - - {options.map(({ input, label, textProps }) => ( - - {input}. {label} - - ))} - - ); - - if (props.label) { - return ( - - {props.label} - {innerBox} - - ); - } else { - return innerBox; - } -}; diff --git a/packages/cli/src/hooks/useActions.ts b/packages/cli/src/hooks/useActions.ts deleted file mode 100644 index 65503c3fa099..000000000000 --- a/packages/cli/src/hooks/useActions.ts +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; - -export type Action = void; - -interface IActionsContext { - do: (what: Action) => void; -} - -export const ActionsContext = React.createContext({} as any); - -export const useActions = () => React.useContext(ActionsContext); diff --git a/packages/cli/src/hooks/useDialogs.ts b/packages/cli/src/hooks/useDialogs.ts index a98af5495a04..d869f52c5127 100644 --- a/packages/cli/src/hooks/useDialogs.ts +++ b/packages/cli/src/hooks/useDialogs.ts @@ -1,9 +1,9 @@ import React from "react"; export type IDialogsContext = { - showError(message: React.ReactNode): void; - showWarning(message: React.ReactNode): void; - showSuccess(message: React.ReactNode): void; + showError(message: React.ReactNode): Promise; + showWarning(message: React.ReactNode): Promise; + showSuccess(message: React.ReactNode): Promise; queryInput( message: React.ReactNode, options?: { diff --git a/packages/cli/src/lib/menu.tsx b/packages/cli/src/lib/menu.tsx index ec9e59712a3a..1c5da57783ca 100644 --- a/packages/cli/src/lib/menu.tsx +++ b/packages/cli/src/lib/menu.tsx @@ -2,7 +2,6 @@ import { Text } from "ink"; import { libVersion } from "zwave-js"; import { HotkeyLabel } from "../components/HotkeyLabel.js"; import { USBPathInfo } from "../components/USBPathInfo.js"; -import { useDriver } from "../hooks/useDriver.js"; import { useGlobals } from "../hooks/useGlobals.js"; import type { MenuItem } from "../hooks/useMenu.js"; import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; @@ -52,7 +51,6 @@ export const exitMenuItem: MenuItem = { const DestroyDriverMenuItem: React.FC = () => { const { navigate } = useNavigation(); - const { driver, destroyDriver } = useDriver(); return ( void; +} + +const RunScriptMenuItem: React.FC = (props) => { + const { navigate } = useNavigation(); + + return ( + + ); +}; + +export function createRunScriptMenuItem(onPress: () => void): MenuItem { + return { + location: "topRight", + item: , + }; +} + +// ===================================================================== + export const defaultMenuItems: MenuItem[] = [ { location: "topLeft", diff --git a/test.mjs b/test.mjs new file mode 100644 index 000000000000..bda8e807e5f8 --- /dev/null +++ b/test.mjs @@ -0,0 +1,7 @@ +/** + * @param {import("@zwave-js/cli").ScriptContext} context + */ +export default async function run(context) { + await context.showSuccess("Hello World!"); + await context.showSuccess("Bye!"); +} From ccc97ee3b8c23f6700843821c365cb58e5941cd6 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Mon, 13 Mar 2023 16:41:21 +0100 Subject: [PATCH 22/27] feat: device details page --- .../cli/src/components/CommandPalette.tsx | 20 +- packages/cli/src/components/DeviceTable.tsx | 174 ++++++++++++++++ packages/cli/src/components/HotkeyLabel.tsx | 50 ++++- packages/cli/src/hooks/useZWaveNode.ts | 8 + packages/cli/src/pages/DeviceDetails.tsx | 87 +++++++- packages/cli/src/pages/DeviceOverview.tsx | 188 +----------------- 6 files changed, 325 insertions(+), 202 deletions(-) create mode 100644 packages/cli/src/components/DeviceTable.tsx create mode 100644 packages/cli/src/hooks/useZWaveNode.ts diff --git a/packages/cli/src/components/CommandPalette.tsx b/packages/cli/src/components/CommandPalette.tsx index 7c882b976cb4..3ed840d30d2d 100644 --- a/packages/cli/src/components/CommandPalette.tsx +++ b/packages/cli/src/components/CommandPalette.tsx @@ -6,7 +6,7 @@ import { VDivider } from "./VDivider.js"; export interface CommandPaletteProps { label?: React.ReactNode; - commands: HotkeyLabelProps[]; + commands: (HotkeyLabelProps | false | undefined | null)[]; } export const CommandPalette: React.FC = (props) => { @@ -19,14 +19,16 @@ export const CommandPalette: React.FC = (props) => { justifyContent="flex-start" borderColor="gray" > - {props.commands.map((p, i) => ( - - {i > 0 && } - - - - - ))} + {props.commands + .filter((c): c is HotkeyLabelProps => !!c) + .map((p, i) => ( + + {i > 0 && } + + + + + ))} ); }; diff --git a/packages/cli/src/components/DeviceTable.tsx b/packages/cli/src/components/DeviceTable.tsx new file mode 100644 index 000000000000..81a32df13aa3 --- /dev/null +++ b/packages/cli/src/components/DeviceTable.tsx @@ -0,0 +1,174 @@ +import { AllColumnProps, CellProps, Table } from "@alcalzone/ink-table"; +import { InterviewStage, NodeStatus } from "@zwave-js/core/safe"; +import { Text } from "ink"; +import Spinner from "ink-spinner"; +import { DeviceClass, getEnumMemberName, ZWaveNode } from "zwave-js"; + +interface DeviceTableRow { + "#": number; + Model: string; + Type: string; + Ready: string; + Status: NodeStatus; + Interview: InterviewStage; +} + +export interface DeviceTableProps { + devices: ZWaveNode[]; +} + +function getDeviceType(cls: DeviceClass | undefined): string { + if (!cls) return "unknown"; + + const deviceType = cls.specific.zwavePlusDeviceType; + if (deviceType) return deviceType; + + const hasSpecificDeviceClass = cls.specific.key !== 0; + if (hasSpecificDeviceClass) return cls.specific.label; + return cls.generic.label; +} + +function getCustomName(node: ZWaveNode): string | undefined { + return [node.name, node.location && `(${node.location})`] + .filter((x) => !!x) + .join(" "); +} + +function getModel(node: ZWaveNode): string { + const mfg = node.deviceConfig?.manufacturer; + const model = node.label; + const desc = node.deviceConfig?.description; + return [mfg, model, desc].filter((x) => !!x).join(" "); +} + +const okText = "✓"; +const nokText = "✗"; + +const statusTexts = { + Unknown: ["?", "gray"], + Alive: [okText, "greenBright"], + Dead: [nokText, "red"], + Awake: ["awake", "blueBright"], + Asleep: ["asleep", "yellow"], +} as const; + +const interviewTexts = { + None: ["█⬝⬝⬝", "gray"], + ProtocolInfo: ["██⬝⬝", "gray"], + NodeInfo: ["███⬝", "gray"], + CommandClasses: ["████", "gray"], + Complete: [okText, "green"], +}; + +const unknownText = "(unknown)"; + +const Cell: ((props: CellProps) => JSX.Element) | undefined = ({ + children, + column, +}) => { + if ( + column === 1 /* model */ && + typeof children === "string" && + children.trim() === unknownText + ) { + return {children}; + } else if ( + column === 3 /* ready */ && + typeof children === "string" && + children.trim().length === 1 + ) { + const trimmed = children.trim(); + return ( + + {children} + + ); + } else if ( + column === 4 /* status */ && + typeof children === "string" && + children.trim().length === 1 + ) { + const originalLength = children.length; + const trimmed = children.trim(); + const status = + statusTexts[NodeStatus[trimmed as any] as keyof typeof statusTexts]; + if (status) { + const newLength = status[0].length; + const padL = " ".repeat( + Math.floor((originalLength - newLength) / 2), + ); + const padR = " ".repeat(originalLength - newLength - padL.length); + return ( + + {padL} + {status[0]} + {padR} + + ); + } + } else if ( + column === 5 /* Interview Stage */ && + typeof children === "string" && + children.trim().length === 1 + ) { + const originalLength = children.length; + const trimmed = children.trim(); + const stage: InterviewStage = parseInt(trimmed) as any; + const text = + interviewTexts[ + getEnumMemberName( + InterviewStage, + stage, + ) as keyof typeof interviewTexts + ]; + if (text) { + const newLength = + text[0].length + (stage < InterviewStage.Complete ? 2 : 0); + const padL = " ".repeat( + Math.floor((originalLength - newLength) / 2), + ); + const padR = " ".repeat(originalLength - newLength - padL.length); + return ( + <> + {padL} + {stage < InterviewStage.Complete && ( + + {" "} + + )} + + {text?.[0]} + {padR} + + + ); + } + } + + return {children}; +}; + +export const DeviceTable: React.FC = (props) => { + const columns: AllColumnProps[] = [ + { key: "#", align: "right" }, + { key: "Model" }, + { key: "Type", align: "center" }, + { key: "Ready", align: "center" }, + { key: "Status", align: "center" }, + { key: "Interview", align: "center" }, + ]; + + const data = props.devices.map((node) => ({ + "#": node.id, + Model: getCustomName(node) || getModel(node) || unknownText, + Type: getDeviceType(node.deviceClass), + Ready: node.ready ? "✓" : "✗", + Status: node.status, + Interview: node.interviewStage, + })); + + return
; +}; diff --git a/packages/cli/src/components/HotkeyLabel.tsx b/packages/cli/src/components/HotkeyLabel.tsx index b8d5389cabd4..30ef7e4289a3 100644 --- a/packages/cli/src/components/HotkeyLabel.tsx +++ b/packages/cli/src/components/HotkeyLabel.tsx @@ -1,10 +1,12 @@ import { Text, TextProps, useInput } from "ink"; +import Spinner from "ink-spinner"; +import { useState } from "react"; export interface HotkeyLabelProps extends TextProps { label?: string; hotkey?: string; modifiers?: Modifiers; - onPress?: () => void; + onPress?: () => Promise | void; } type Modifiers = ("ctrl" | "shift")[]; @@ -46,22 +48,44 @@ export const HotkeyLabel: React.FC = (props) => { const { color, children, ...rest } = textProps; - useInput((input, key) => { + const [isBusy, setIsBusy] = useState(false); + + const busyProps: TextProps = isBusy + ? { + color: "greenBright", + } + : {}; + + useInput(async (input, key) => { if ( + !isBusy && hotkey && + props.onPress && (input === hotkey || (hotkey in specialKeys && (key as any)[hotkey])) && ((modifiers && modifiers.every((mod) => key[mod])) || (!modifiers && !key.ctrl && !key.shift)) ) { - props.onPress?.(); + setIsBusy(true); + await props.onPress(); + setIsBusy(false); } }); + const BusySpinner = isBusy ? ( + <> + {" "} + + + ) : null; + if (!label) { if (hotkey) { return ( - {renderHotkey(hotkey, modifiers)} + + {renderHotkey(hotkey, modifiers)} + {BusySpinner} + ); } else { throw new Error("At least one of label or hotkey must be provided"); @@ -69,7 +93,12 @@ export const HotkeyLabel: React.FC = (props) => { } if (!hotkey) { - return {label}; + return ( + + {label} + {BusySpinner} + + ); } else if (hotkey.length !== 1 && !(hotkey in specialKeys)) { throw new Error("Hotkey must be a single character or a special key"); } @@ -78,7 +107,14 @@ export const HotkeyLabel: React.FC = (props) => { hotkey.length === 1 && !modifiers?.length ? label.toLowerCase().indexOf(hotkey.toLowerCase()) : -1; - if (hotkeyIndex >= 0) { + if (isBusy) { + return ( + + {label} + {BusySpinner} + + ); + } else if (hotkeyIndex >= 0) { return ( {label.slice(0, hotkeyIndex)} @@ -86,6 +122,7 @@ export const HotkeyLabel: React.FC = (props) => { {label.slice(hotkeyIndex, hotkeyIndex + 1)} {label.slice(hotkeyIndex + 1)} + {BusySpinner} ); } else { @@ -93,6 +130,7 @@ export const HotkeyLabel: React.FC = (props) => { {label}{" "} {renderHotkey(hotkey, modifiers)} + {BusySpinner} ); } diff --git a/packages/cli/src/hooks/useZWaveNode.ts b/packages/cli/src/hooks/useZWaveNode.ts new file mode 100644 index 000000000000..f05eb4b0f1cd --- /dev/null +++ b/packages/cli/src/hooks/useZWaveNode.ts @@ -0,0 +1,8 @@ +import type { ZWaveNode } from "zwave-js"; +import { useDriver } from "./useDriver.js"; + +export function useZWaveNode(id: number): ZWaveNode | undefined { + const { driver } = useDriver(); + if (!driver.ready) return; + return driver.controller.nodes.get(id); +} diff --git a/packages/cli/src/pages/DeviceDetails.tsx b/packages/cli/src/pages/DeviceDetails.tsx index 90a1a3c5a0db..57055a0f0331 100644 --- a/packages/cli/src/pages/DeviceDetails.tsx +++ b/packages/cli/src/pages/DeviceDetails.tsx @@ -1,7 +1,14 @@ -import { Text } from "ink"; +import { Box, Text } from "ink"; +import { useEffect } from "react"; +import type { ZWaveNode } from "zwave-js"; +import { CommandPalette } from "../components/CommandPalette.js"; +import { DeviceTable } from "../components/DeviceTable.js"; import { HotkeyLabel } from "../components/HotkeyLabel.js"; +import { useDialogs } from "../hooks/useDialogs.js"; +import { useForceRerender } from "../hooks/useForceRerender.js"; import { useMenu, type MenuItem } from "../hooks/useMenu.js"; import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; +import { useZWaveNode } from "../hooks/useZWaveNode.js"; import { destroyDriverMenuItem, exitMenuItem, @@ -14,6 +21,8 @@ export interface DeviceDetailsPageProps { export const DeviceDetailsPage: React.FC = (props) => { const { navigate } = useNavigation(); + const { showSuccess, showError } = useDialogs(); + const forceRerender = useForceRerender(); const deviceMenuItem: MenuItem = { location: "bottomLeft", @@ -33,5 +42,79 @@ export const DeviceDetailsPage: React.FC = (props) => { exitMenuItem, ]); - return {props.nodeId}; + const node = useZWaveNode(props.nodeId); + // Redirect to the overview when the node is not found + useEffect(() => { + if (!node) navigate(CLIPage.DeviceOverview); + }, [node, navigate]); + + // Register event handlers to update the table + useEffect(() => { + const addNodeEventHandlers = (node: ZWaveNode) => { + node.on("interview started", forceRerender) + .on("interview completed", forceRerender) + .on("ready", forceRerender) + .on("alive", forceRerender) + .on("dead", forceRerender) + .on("sleep", forceRerender) + .on("wake up", forceRerender) + .on("interview started", forceRerender) + .on("interview stage completed", forceRerender) + .on("interview completed", forceRerender); + }; + const removeNodeEventHandlers = (node: ZWaveNode) => { + node.off("interview started", forceRerender) + .off("interview completed", forceRerender) + .off("ready", forceRerender) + .off("alive", forceRerender) + .off("dead", forceRerender) + .off("sleep", forceRerender) + .off("wake up", forceRerender) + .off("interview started", forceRerender) + .off("interview stage completed", forceRerender) + .off("interview completed", forceRerender); + }; + + if (node) addNodeEventHandlers(node); + + return () => { + if (node) removeNodeEventHandlers(node); + }; + }, [node]); + + if (!node) return <>; + return ( + + + Actions + + } + commands={[ + !node.isControllerNode && { + label: "Ping", + hotkey: "p", + onPress: async () => { + const result = await node.ping(); + if (result) { + showSuccess("Ping succeeded"); + } else { + showError("Ping failed"); + } + }, + }, + !node.isControllerNode && { + label: "Re-Interview", + hotkey: "i", + onPress: () => { + void node.refreshInfo().catch(() => {}); + }, + }, + ]} + > + + + + ); }; diff --git a/packages/cli/src/pages/DeviceOverview.tsx b/packages/cli/src/pages/DeviceOverview.tsx index 64dddad69ef0..971dabcd1bb3 100644 --- a/packages/cli/src/pages/DeviceOverview.tsx +++ b/packages/cli/src/pages/DeviceOverview.tsx @@ -1,15 +1,8 @@ -import { AllColumnProps, CellProps, Table } from "@alcalzone/ink-table"; import { Box, Text } from "ink"; -import Spinner from "ink-spinner"; import { useEffect, useState } from "react"; -import { - DeviceClass, - getEnumMemberName, - InterviewStage, - NodeStatus, - ZWaveNode, -} from "zwave-js"; +import type { ZWaveNode } from "zwave-js"; import { CommandPalette } from "../components/CommandPalette.js"; +import { DeviceTable } from "../components/DeviceTable.js"; import { HotkeyLabel } from "../components/HotkeyLabel.js"; import { useDialogs } from "../hooks/useDialogs.js"; import { useDriver } from "../hooks/useDriver.js"; @@ -22,140 +15,6 @@ import { toggleLogMenuItem, } from "../lib/menu.js"; -const okText = "✓"; -const nokText = "✗"; - -const statusTexts = { - Unknown: ["?", "gray"], - Alive: [okText, "greenBright"], - Dead: [nokText, "red"], - Awake: ["awake", "blueBright"], - Asleep: ["asleep", "yellow"], -} as const; - -const interviewTexts = { - None: ["█⬝⬝⬝", "gray"], - ProtocolInfo: ["██⬝⬝", "gray"], - NodeInfo: ["███⬝", "gray"], - CommandClasses: ["████", "gray"], - Complete: [okText, "green"], -}; - -const unknownText = "(unknown)"; - -const Cell: ((props: CellProps) => JSX.Element) | undefined = ({ - children, - column, -}) => { - if ( - column === 1 /* model */ && - typeof children === "string" && - children.trim() === unknownText - ) { - return {children}; - } else if ( - column === 3 /* ready */ && - typeof children === "string" && - children.trim().length === 1 - ) { - const trimmed = children.trim(); - return ( - - {children} - - ); - } else if ( - column === 4 /* status */ && - typeof children === "string" && - children.trim().length === 1 - ) { - const originalLength = children.length; - const trimmed = children.trim(); - const status = - statusTexts[NodeStatus[trimmed as any] as keyof typeof statusTexts]; - if (status) { - const newLength = status[0].length; - const padL = " ".repeat( - Math.floor((originalLength - newLength) / 2), - ); - const padR = " ".repeat(originalLength - newLength - padL.length); - return ( - - {padL} - {status[0]} - {padR} - - ); - } - } else if ( - column === 5 /* Interview Stage */ && - typeof children === "string" && - children.trim().length === 1 - ) { - const originalLength = children.length; - const trimmed = children.trim(); - const stage: InterviewStage = parseInt(trimmed) as any; - const text = - interviewTexts[ - getEnumMemberName( - InterviewStage, - stage, - ) as keyof typeof interviewTexts - ]; - if (text) { - const newLength = - text[0].length + (stage < InterviewStage.Complete ? 2 : 0); - const padL = " ".repeat( - Math.floor((originalLength - newLength) / 2), - ); - const padR = " ".repeat(originalLength - newLength - padL.length); - return ( - <> - {padL} - {stage < InterviewStage.Complete && ( - - {" "} - - )} - - {text?.[0]} - {padR} - - - ); - } - } - - return {children}; -}; - -function getDeviceType(cls: DeviceClass | undefined): string { - if (!cls) return "unknown"; - - const deviceType = cls.specific.zwavePlusDeviceType; - if (deviceType) return deviceType; - - const hasSpecificDeviceClass = cls.specific.key !== 0; - if (hasSpecificDeviceClass) return cls.specific.label; - return cls.generic.label; -} - -function getCustomName(node: ZWaveNode): string | undefined { - return [node.name, node.location && `(${node.location})`] - .filter((x) => !!x) - .join(" "); -} - -function getModel(node: ZWaveNode): string { - const mfg = node.deviceConfig?.manufacturer; - const model = node.label; - const desc = node.deviceConfig?.description; - return [mfg, model, desc].filter((x) => !!x).join(" "); -} - export const DeviceOverviewPage: React.FC = () => { const { driver } = useDriver(); const forceRerender = useForceRerender(); @@ -174,15 +33,6 @@ export const DeviceOverviewPage: React.FC = () => { .map((id) => driver.controller.nodes.get(id)) .filter((x): x is ZWaveNode => !!x); - const nodesData = nodes.map((node) => ({ - "#": node.id, - Model: getCustomName(node) || getModel(node) || unknownText, - Type: getDeviceType(node.deviceClass), - Ready: node.ready ? "✓" : "✗", - Status: node.status, - Interview: node.interviewStage, - })); - // Register event handlers to update the table useEffect(() => { const updateIDs = () => { @@ -237,15 +87,6 @@ export const DeviceOverviewPage: React.FC = () => { }; }, []); - const columns: AllColumnProps[] = [ - { key: "#", align: "right" }, - { key: "Model" }, - { key: "Type", align: "center" }, - { key: "Ready", align: "center" }, - { key: "Status", align: "center" }, - { key: "Interview", align: "center" }, - ]; - return ( { } }, }, - { - label: "Re-Interview", - hotkey: "i", - onPress: async () => { - const nodeId = await queryInput( - "Re-Interview → Node ID", - { inline: true }, - ); - if (!nodeId) return; - const nodeIdNum = parseInt(nodeId, 10); - if ( - !Number.isNaN(nodeIdNum) && - nodeIDs.includes(nodeIdNum) - ) { - driver.controller.nodes - .get(nodeIdNum) - ?.refreshInfo() - .catch(() => {}); - } else { - showError("Node not found"); - } - }, - }, ]} > -
+ From d6a916e648e543e158965514435751bb100b2a1f Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Mon, 13 Mar 2023 20:19:39 +0100 Subject: [PATCH 23/27] fix: switch to ink fork with lines --- packages/cli/package.json | 2 +- packages/cli/src/cli.tsx | 22 ++++++++----- .../cli/src/components/CommandPalette.tsx | 11 +++++-- packages/cli/src/components/HDivider.tsx | 33 ------------------- packages/cli/src/components/VDivider.tsx | 33 ------------------- yarn.lock | 31 ++++++++--------- 6 files changed, 39 insertions(+), 93 deletions(-) delete mode 100644 packages/cli/src/components/HDivider.tsx delete mode 100644 packages/cli/src/components/VDivider.tsx diff --git a/packages/cli/package.json b/packages/cli/package.json index a10bf0644b70..e08f8cd802d7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -59,7 +59,7 @@ "del-cli": "^5.0.0", "esbuild": "0.15.7", "esbuild-register": "^3.4.2", - "ink": "^4.0.0", + "ink": "https://github.com/AlCalzone/ink#lines-stable", "ink-select-input": "^5.0.0", "ink-spinner": "^5.0.0", "ink-text-input": "^5.0.0", diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 5cd530588976..0d60f5fd3c67 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,9 +1,8 @@ import { getErrorMessage } from "@zwave-js/shared"; -import { Box, render, Spacer, Text, useInput } from "ink"; +import { Box, Line, render, Spacer, Text, useInput } from "ink"; import { useCallback, useEffect, useRef, useState } from "react"; import type { Driver } from "zwave-js"; import { Frame } from "./components/Frame.js"; -import { HDivider } from "./components/HDivider.js"; import { Log } from "./components/Log.js"; import { InlineQuery, @@ -12,7 +11,6 @@ import { ModalState, } from "./components/Modals.js"; import { SetUSBPath } from "./components/setUSBPath.js"; -import { VDivider } from "./components/VDivider.js"; import { DialogsContext } from "./hooks/useDialogs.js"; import { DriverContext } from "./hooks/useDriver.js"; import { GlobalsContext } from "./hooks/useGlobals.js"; @@ -327,6 +325,9 @@ const CLI: React.FC = () => { {logVisible && ( { ) } > - {layout === "horizontal" ? ( - - ) : ( - - )} + + )} diff --git a/packages/cli/src/components/CommandPalette.tsx b/packages/cli/src/components/CommandPalette.tsx index 3ed840d30d2d..933b3911c7ae 100644 --- a/packages/cli/src/components/CommandPalette.tsx +++ b/packages/cli/src/components/CommandPalette.tsx @@ -1,8 +1,7 @@ -import { Box } from "ink"; +import { Box, Line } from "ink"; import React from "react"; import { Frame } from "./Frame.js"; import { HotkeyLabel, HotkeyLabelProps } from "./HotkeyLabel.js"; -import { VDivider } from "./VDivider.js"; export interface CommandPaletteProps { label?: React.ReactNode; @@ -23,7 +22,13 @@ export const CommandPalette: React.FC = (props) => { .filter((c): c is HotkeyLabelProps => !!c) .map((p, i) => ( - {i > 0 && } + {i > 0 && ( + + )} diff --git a/packages/cli/src/components/HDivider.tsx b/packages/cli/src/components/HDivider.tsx deleted file mode 100644 index c7b70af3b41e..000000000000 --- a/packages/cli/src/components/HDivider.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Box, measureElement, Text, TextProps, type DOMElement } from "ink"; -import React, { useEffect, useRef, useState } from "react"; - -export interface HDividerProps extends TextProps { - character?: string; - margin?: number; -} - -export const HDivider: React.FC = ({ - character = "─", - margin = 1, - ...textProps -}) => { - const ref = useRef(null); - const [text, setText] = useState(character); - - useEffect(() => { - if (ref.current) { - const width = Math.max(1, measureElement(ref.current).width); - if (Number.isNaN(width)) { - setText(character); - } else { - setText(character.repeat(width)); - } - } - }); - - return ( - - {text} - - ); -}; diff --git a/packages/cli/src/components/VDivider.tsx b/packages/cli/src/components/VDivider.tsx deleted file mode 100644 index 873b90338443..000000000000 --- a/packages/cli/src/components/VDivider.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Box, measureElement, Text, TextProps, type DOMElement } from "ink"; -import { useEffect, useRef, useState } from "react"; - -export interface VDividerProps extends TextProps { - character?: string; - margin?: number; -} - -export const VDivider: React.FC = ({ - character = "│", - margin = 1, - ...textProps -}) => { - const ref = useRef(null); - const [text, setText] = useState(character); - - useEffect(() => { - if (ref.current) { - const height = Math.max(1, measureElement(ref.current).height); - if (Number.isNaN(height)) { - setText(character); - } else { - setText(new Array(height).fill(character).join("\n")); - } - } - }); - - return ( - - {text} - - ); -}; diff --git a/yarn.lock b/yarn.lock index 477ae8c08b55..af182f9a2e48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1826,7 +1826,7 @@ __metadata: del-cli: ^5.0.0 esbuild: 0.15.7 esbuild-register: ^3.4.2 - ink: ^4.0.0 + ink: "https://github.com/AlCalzone/ink#lines-stable" ink-select-input: ^5.0.0 ink-spinner: ^5.0.0 ink-text-input: ^5.0.0 @@ -3767,6 +3767,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^10.2.1": + version: 10.2.1 + resolution: "emoji-regex@npm:10.2.1" + checksum: 1aa2d16881c56531fdfc03d0b36f5c2b6221cc4097499a5665b88b711dc3fb4d5b8804f0ca6f00c56e5dcf89bac75f0487eee85da1da77df3a33accc6ecbe426 + languageName: node + linkType: hard + "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -5303,9 +5310,9 @@ __metadata: languageName: node linkType: hard -"ink@npm:^4.0.0": +"ink@https://github.com/AlCalzone/ink#lines-stable": version: 4.0.0 - resolution: "ink@npm:4.0.0" + resolution: "ink@https://github.com/AlCalzone/ink.git#commit=770eb7cddb36a326713bef20c2685a5203d92dfc" dependencies: ansi-escapes: ^6.0.0 auto-bind: ^5.0.1 @@ -5314,9 +5321,10 @@ __metadata: cli-cursor: ^4.0.0 cli-truncate: ^3.1.0 code-excerpt: ^4.0.0 + emoji-regex: ^10.2.1 indent-string: ^5.0.0 is-ci: ^3.0.1 - lodash-es: ^4.17.21 + lodash: ^4.17.21 patch-console: ^2.0.0 react-reconciler: ^0.29.0 scheduler: ^0.23.0 @@ -5338,7 +5346,7 @@ __metadata: optional: true react-devtools-core: optional: true - checksum: 7cd120ed6589e17ea4f8c0abcfeac37e3afb3d6c5b82342a7996f0a15a1a4d1ba933f926ae8e5539dd8adea3e6cfbcf5d4025d82c1d917fbcf8fc56f92213f63 + checksum: 4055659133de0390b557fc492edbab3f62edf1a4899ef932096dcddf2fac4609f18ce2a1ce6c013fa4825d7aae4ea506c9e3e30db344901afebf82aabd7a8002 languageName: node linkType: hard @@ -5960,13 +5968,6 @@ __metadata: languageName: node linkType: hard -"lodash-es@npm:^4.17.21": - version: 4.17.21 - resolution: "lodash-es@npm:4.17.21" - checksum: 05cbffad6e2adbb331a4e16fbd826e7faee403a1a04873b82b42c0f22090f280839f85b95393f487c1303c8a3d2a010048bf06151a6cbe03eee4d388fb0a12d2 - languageName: node - linkType: hard - "lodash.get@npm:^4.4.2": version: 4.4.2 resolution: "lodash.get@npm:4.4.2" @@ -9117,8 +9118,8 @@ __metadata: linkType: hard "ws@npm:^8.12.0": - version: 8.12.1 - resolution: "ws@npm:8.12.1" + version: 8.13.0 + resolution: "ws@npm:8.13.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -9127,7 +9128,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 97301c1c4d838fc81bd413f370f75c12aabe44527b31323b761eab3043a9ecb7e32ffd668548382c9a6a5ad3a1c3a9249608e8338e6b939f2f9540f1e21970b5 + checksum: 53e991bbf928faf5dc6efac9b8eb9ab6497c69feeb94f963d648b7a3530a720b19ec2e0ec037344257e05a4f35bd9ad04d9de6f289615ffb133282031b18c61c languageName: node linkType: hard From 83ece383d12ed5f0d9eaaf32ab8433e0aa7a4ced Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Tue, 14 Mar 2023 12:01:55 +0100 Subject: [PATCH 24/27] fix: debounce showing/hiding the log --- packages/cli/src/cli.tsx | 15 +++++---------- packages/cli/src/components/Log.tsx | 12 ++++++++++-- packages/cli/src/lib/debounce.ts | 10 ++++++++++ 3 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 packages/cli/src/lib/debounce.ts diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 0d60f5fd3c67..bd0159d59c04 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -22,6 +22,7 @@ import { NavigationContext, } from "./hooks/useNavigation.js"; import { useStdoutDimensions } from "./hooks/useStdoutDimensions.js"; +import { debounce } from "./lib/debounce.js"; import { createLogTransport, LinesBuffer } from "./lib/logging.js"; import { createRunScriptMenuItem, defaultMenuItems } from "./lib/menu.js"; @@ -60,6 +61,9 @@ const CLI: React.FC = () => { }, []); const [logVisible, setLogVisible] = useState(false); + const setLogVisibleDebounced = useCallback(debounce(setLogVisible, 50), [ + setLogVisible, + ]); const [cliPage, setCLIPage] = useState({ page: usbPath && autostart ? CLIPage.StartingDriver : CLIPage.Prepare, @@ -188,12 +192,6 @@ const CLI: React.FC = () => { // nothing to do }); - const performAction = useCallback(async () => { - // if (action.type === "navigate") { - // setCLIPage(action.to); - // } - }, []); - if (rows < MIN_ROWS) { return ( @@ -221,7 +219,7 @@ const CLI: React.FC = () => { usbPath, logTransport, logVisible, - setLogVisible, + setLogVisible: setLogVisibleDebounced, clearLog, }} > @@ -325,9 +323,6 @@ const CLI: React.FC = () => { {logVisible && ( = (props) => { setLog(lines.join("\n")); }, [props.buffer, logHeight]); - useEffect(() => { + const measure = useCallback(() => { if (ref.current) { - setLogHeight(measureElement(ref.current).height); + const height = measureElement(ref.current).height; + if (!Number.isNaN(height)) setLogHeight(height); } + }, [ref, setLogHeight]); + + useEffect(() => { + // For some reason, the measurement can be wrong shortly after rendering + // so we measure a second time after a short delay + measure(); + setTimeout(measure, 30); }); // Update the log state whenever `buffer` emits a change event diff --git a/packages/cli/src/lib/debounce.ts b/packages/cli/src/lib/debounce.ts new file mode 100644 index 000000000000..f84235753bfa --- /dev/null +++ b/packages/cli/src/lib/debounce.ts @@ -0,0 +1,10 @@ +export function debounce( + fn: (...args: Args) => void, + delay: number, +) { + let timer: NodeJS.Timeout; + return (...args: Args) => { + clearTimeout(timer); + timer = setTimeout(fn as any, delay, ...args); + }; +} From 67fe68c321ccad7c9f4dba836c8ad2f29a2d8d51 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 15 Mar 2023 15:48:31 +0100 Subject: [PATCH 25/27] feat: support setting RF region --- .../components/DeviceDetailsController.tsx | 77 ++++++++++++++++ .../src/components/DeviceDetailsEndNode.tsx | 87 +++++++++++++++++++ packages/cli/src/hooks/useNavigation.ts | 6 ++ packages/cli/src/lib/driver.ts | 5 +- .../cli/src/pages/Controller/SetRegion.tsx | 81 +++++++++++++++++ packages/cli/src/pages/DeviceDetails.tsx | 80 +---------------- 6 files changed, 258 insertions(+), 78 deletions(-) create mode 100644 packages/cli/src/components/DeviceDetailsController.tsx create mode 100644 packages/cli/src/components/DeviceDetailsEndNode.tsx create mode 100644 packages/cli/src/pages/Controller/SetRegion.tsx diff --git a/packages/cli/src/components/DeviceDetailsController.tsx b/packages/cli/src/components/DeviceDetailsController.tsx new file mode 100644 index 000000000000..4108a3506bea --- /dev/null +++ b/packages/cli/src/components/DeviceDetailsController.tsx @@ -0,0 +1,77 @@ +import { Box, Text } from "ink"; +import { useEffect } from "react"; +import type { ZWaveNode } from "zwave-js"; +import { useDialogs } from "../hooks/useDialogs.js"; +import { useForceRerender } from "../hooks/useForceRerender.js"; +import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; +import { CommandPalette } from "./CommandPalette.js"; +import { DeviceTable } from "./DeviceTable.js"; + +export interface DeviceDetailsControllerProps { + node: ZWaveNode; +} + +export const DeviceDetailsController: React.FC< + DeviceDetailsControllerProps +> = ({ node }) => { + const { showSuccess, showError } = useDialogs(); + const { navigate } = useNavigation(); + const forceRerender = useForceRerender(); + + // Register event handlers to update the table + useEffect(() => { + const addNodeEventHandlers = (node: ZWaveNode) => { + node.on("interview started", forceRerender) + .on("interview completed", forceRerender) + .on("ready", forceRerender) + .on("alive", forceRerender) + .on("dead", forceRerender) + .on("sleep", forceRerender) + .on("wake up", forceRerender) + .on("interview started", forceRerender) + .on("interview stage completed", forceRerender) + .on("interview completed", forceRerender); + }; + const removeNodeEventHandlers = (node: ZWaveNode) => { + node.off("interview started", forceRerender) + .off("interview completed", forceRerender) + .off("ready", forceRerender) + .off("alive", forceRerender) + .off("dead", forceRerender) + .off("sleep", forceRerender) + .off("wake up", forceRerender) + .off("interview started", forceRerender) + .off("interview stage completed", forceRerender) + .off("interview completed", forceRerender); + }; + + if (node) addNodeEventHandlers(node); + + return () => { + if (node) removeNodeEventHandlers(node); + }; + }, [node]); + + return ( + + + Actions + + } + commands={[ + { + label: "Change RF region", + hotkey: "r", + onPress: () => { + navigate(CLIPage.ControllerSetRegion); + }, + }, + ]} + > + + + + ); +}; diff --git a/packages/cli/src/components/DeviceDetailsEndNode.tsx b/packages/cli/src/components/DeviceDetailsEndNode.tsx new file mode 100644 index 000000000000..74c2dddc5bc2 --- /dev/null +++ b/packages/cli/src/components/DeviceDetailsEndNode.tsx @@ -0,0 +1,87 @@ +import { Box, Text } from "ink"; +import { useEffect } from "react"; +import type { ZWaveNode } from "zwave-js"; +import { useDialogs } from "../hooks/useDialogs.js"; +import { useForceRerender } from "../hooks/useForceRerender.js"; +import { CommandPalette } from "./CommandPalette.js"; +import { DeviceTable } from "./DeviceTable.js"; + +export interface DeviceDetailsEndNodeProps { + node: ZWaveNode; +} + +export const DeviceDetailsEndNode: React.FC = ({ + node, +}) => { + const { showSuccess, showError } = useDialogs(); + const forceRerender = useForceRerender(); + + // Register event handlers to update the table + useEffect(() => { + const addNodeEventHandlers = (node: ZWaveNode) => { + node.on("interview started", forceRerender) + .on("interview completed", forceRerender) + .on("ready", forceRerender) + .on("alive", forceRerender) + .on("dead", forceRerender) + .on("sleep", forceRerender) + .on("wake up", forceRerender) + .on("interview started", forceRerender) + .on("interview stage completed", forceRerender) + .on("interview completed", forceRerender); + }; + const removeNodeEventHandlers = (node: ZWaveNode) => { + node.off("interview started", forceRerender) + .off("interview completed", forceRerender) + .off("ready", forceRerender) + .off("alive", forceRerender) + .off("dead", forceRerender) + .off("sleep", forceRerender) + .off("wake up", forceRerender) + .off("interview started", forceRerender) + .off("interview stage completed", forceRerender) + .off("interview completed", forceRerender); + }; + + if (node) addNodeEventHandlers(node); + + return () => { + if (node) removeNodeEventHandlers(node); + }; + }, [node]); + + return ( + + + Actions + + } + commands={[ + { + label: "Ping", + hotkey: "p", + onPress: async () => { + const result = await node.ping(); + if (result) { + showSuccess("Ping succeeded"); + } else { + showError("Ping failed"); + } + }, + }, + { + label: "Re-Interview", + hotkey: "i", + onPress: () => { + void node.refreshInfo().catch(() => {}); + }, + }, + ]} + > + + + + ); +}; diff --git a/packages/cli/src/hooks/useNavigation.ts b/packages/cli/src/hooks/useNavigation.ts index bd3a55a58a34..d94ccbc8fd0a 100644 --- a/packages/cli/src/hooks/useNavigation.ts +++ b/packages/cli/src/hooks/useNavigation.ts @@ -2,6 +2,7 @@ import React from "react"; import { SetUSBPath } from "../components/setUSBPath.js"; import { BootstrappingNodePage } from "../pages/BootstrappingNode.js"; import { ConfirmExitPage } from "../pages/ConfirmExit.js"; +import { ControllerSetRegionPage } from "../pages/Controller/SetRegion.js"; import { DestroyingDriverPage } from "../pages/DestroyingDriver.js"; import { DeviceDetailsPage } from "../pages/DeviceDetails.js"; import { DeviceOverviewPage } from "../pages/DeviceOverview.js"; @@ -27,6 +28,8 @@ export enum CLIPage { ReplaceFailedNode, RemoveFailedNode, + ControllerSetRegion, + ConfirmExit, } @@ -78,6 +81,9 @@ export function getPageComponent(cliPage: CLIPage): React.FC | undefined { case CLIPage.RemoveFailedNode: return RemoveFailedNodePage; + case CLIPage.ControllerSetRegion: + return ControllerSetRegionPage; + case CLIPage.ConfirmExit: return ConfirmExitPage; } diff --git a/packages/cli/src/lib/driver.ts b/packages/cli/src/lib/driver.ts index e7fa0d7689d9..f3ccbe2b8a5f 100644 --- a/packages/cli/src/lib/driver.ts +++ b/packages/cli/src/lib/driver.ts @@ -15,8 +15,9 @@ export async function startDriver( ): Promise { const driver = new Driver(port, { logConfig: { - // Do not log to console or file - enabled: false, + // TODO: Make this configurable + enabled: true, + logToFile: true, // But log to our own transport transports: [options.logTransport], }, diff --git a/packages/cli/src/pages/Controller/SetRegion.tsx b/packages/cli/src/pages/Controller/SetRegion.tsx new file mode 100644 index 000000000000..b3e9c2c19949 --- /dev/null +++ b/packages/cli/src/pages/Controller/SetRegion.tsx @@ -0,0 +1,81 @@ +import { Box, Text } from "ink"; +import SelectInput from "ink-select-input"; +import Spinner from "ink-spinner"; +import { useCallback, useState } from "react"; +import { getEnumMemberName, RFRegion } from "zwave-js"; +import { Center } from "../../components/Center.js"; +import { useDialogs } from "../../hooks/useDialogs.js"; +import { useDriver } from "../../hooks/useDriver.js"; +import { useNavigation } from "../../hooks/useNavigation.js"; + +export interface ControllerSetRegionPageProps { + // TODO: +} + +const regions = ( + Object.entries(RFRegion).filter( + ([_, value]) => typeof value === "number", + ) as [string, RFRegion][] +).map(([name, region]) => ({ + label: name, + value: region, +})); + +type Item = (typeof regions)[number]; + +export const ControllerSetRegionPage: React.FC = ( + props, +) => { + const { driver } = useDriver(); + const [busy, setBusy] = useState(false); + const { showSuccess, showError } = useDialogs(); + const { back } = useNavigation(); + + const currentRegion = driver.controller.rfRegion; + + const setRegion = useCallback( + async ({ value: region }: Item) => { + setBusy(true); + const result = await driver.controller.setRFRegion(region); + setBusy(false); + if (result) { + showSuccess( + `RF region changed to ${getEnumMemberName( + RFRegion, + region, + )}`, + ); + } else { + showError(`Failed to change RF region.`); + } + back(); + }, + [driver.controller], + ); + + if (busy) { + return ( +
+ + + + {" "} + Setting RF region... + +
+ ); + } + + return ( + + Select RF region + r.value === currentRegion, + )} + /> + + ); +}; diff --git a/packages/cli/src/pages/DeviceDetails.tsx b/packages/cli/src/pages/DeviceDetails.tsx index 57055a0f0331..1445bf633689 100644 --- a/packages/cli/src/pages/DeviceDetails.tsx +++ b/packages/cli/src/pages/DeviceDetails.tsx @@ -1,11 +1,7 @@ -import { Box, Text } from "ink"; import { useEffect } from "react"; -import type { ZWaveNode } from "zwave-js"; -import { CommandPalette } from "../components/CommandPalette.js"; -import { DeviceTable } from "../components/DeviceTable.js"; +import { DeviceDetailsController } from "../components/DeviceDetailsController.js"; +import { DeviceDetailsEndNode } from "../components/DeviceDetailsEndNode.js"; import { HotkeyLabel } from "../components/HotkeyLabel.js"; -import { useDialogs } from "../hooks/useDialogs.js"; -import { useForceRerender } from "../hooks/useForceRerender.js"; import { useMenu, type MenuItem } from "../hooks/useMenu.js"; import { CLIPage, useNavigation } from "../hooks/useNavigation.js"; import { useZWaveNode } from "../hooks/useZWaveNode.js"; @@ -21,8 +17,6 @@ export interface DeviceDetailsPageProps { export const DeviceDetailsPage: React.FC = (props) => { const { navigate } = useNavigation(); - const { showSuccess, showError } = useDialogs(); - const forceRerender = useForceRerender(); const deviceMenuItem: MenuItem = { location: "bottomLeft", @@ -48,73 +42,7 @@ export const DeviceDetailsPage: React.FC = (props) => { if (!node) navigate(CLIPage.DeviceOverview); }, [node, navigate]); - // Register event handlers to update the table - useEffect(() => { - const addNodeEventHandlers = (node: ZWaveNode) => { - node.on("interview started", forceRerender) - .on("interview completed", forceRerender) - .on("ready", forceRerender) - .on("alive", forceRerender) - .on("dead", forceRerender) - .on("sleep", forceRerender) - .on("wake up", forceRerender) - .on("interview started", forceRerender) - .on("interview stage completed", forceRerender) - .on("interview completed", forceRerender); - }; - const removeNodeEventHandlers = (node: ZWaveNode) => { - node.off("interview started", forceRerender) - .off("interview completed", forceRerender) - .off("ready", forceRerender) - .off("alive", forceRerender) - .off("dead", forceRerender) - .off("sleep", forceRerender) - .off("wake up", forceRerender) - .off("interview started", forceRerender) - .off("interview stage completed", forceRerender) - .off("interview completed", forceRerender); - }; - - if (node) addNodeEventHandlers(node); - - return () => { - if (node) removeNodeEventHandlers(node); - }; - }, [node]); - if (!node) return <>; - return ( - - - Actions - - } - commands={[ - !node.isControllerNode && { - label: "Ping", - hotkey: "p", - onPress: async () => { - const result = await node.ping(); - if (result) { - showSuccess("Ping succeeded"); - } else { - showError("Ping failed"); - } - }, - }, - !node.isControllerNode && { - label: "Re-Interview", - hotkey: "i", - onPress: () => { - void node.refreshInfo().catch(() => {}); - }, - }, - ]} - > - - - - ); + if (node.isControllerNode) return ; + return ; }; From d92950978b1d4ad9a7401f077804f626ba502e90 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Mon, 20 Mar 2023 22:11:58 +0100 Subject: [PATCH 26/27] feat: work in progress on CLI --- packages/cli/src/cli.tsx | 2 +- test.mjs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index bd0159d59c04..6a90b8bc1858 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -175,7 +175,7 @@ const CLI: React.FC = () => { showSuccess("Script executed successfully!"); } catch (e) { showError( - `Error during script execution: ${getErrorMessage(e, true)}`, + `Error during script execution: ${getErrorMessage(e, false)}`, ); return; } diff --git a/test.mjs b/test.mjs index bda8e807e5f8..e5e16890c2e8 100644 --- a/test.mjs +++ b/test.mjs @@ -2,6 +2,7 @@ * @param {import("@zwave-js/cli").ScriptContext} context */ export default async function run(context) { - await context.showSuccess("Hello World!"); - await context.showSuccess("Bye!"); + const { driver } = context; + const node = driver.controller.nodes.get(55); + await node.commandClasses["Binary Switch"].set(false); } From 69202216219c335c3528c9dfa1b811c0bf02d10b Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Mon, 17 Apr 2023 13:39:49 +0200 Subject: [PATCH 27/27] chore: upgrade ink --- package.json | 1 - packages/cli/bin/cli.js | 2 +- packages/cli/build.sh | 1 + packages/cli/package.json | 3 +- packages/cli/src/cli.tsx | 3 +- .../cli/src/components/CommandPalette.tsx | 3 +- packages/cli/src/components/Line.tsx | 21 ++++ yarn.lock | 119 ++++++++++++------ 8 files changed, 107 insertions(+), 46 deletions(-) create mode 100644 packages/cli/src/components/Line.tsx diff --git a/package.json b/package.json index c30e18dc8342..33f356f097c4 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,6 @@ "colors": "1.4.0", "yoga-layout-prebuilt@^1.9.6": "patch:yoga-layout-prebuilt@npm%3A1.10.0#./.yarn/patches/yoga-layout-prebuilt-npm-1.10.0-855b15449f.patch", "react-devtools-core@^4.27.2": "patch:react-devtools-core@npm%3A4.27.2#./.yarn/patches/react-devtools-core-npm-4.27.2-7a013e485e.patch", - "ink-table@^3.0.0": "patch:ink-table@npm%3A3.0.0#./.yarn/patches/ink-table-npm-3.0.0-64b4e73397.patch", "@alcalzone/ink-table@^1.0.0": "patch:@alcalzone/ink-table@npm%3A1.0.0#./.yarn/patches/@alcalzone-ink-table-npm-1.0.0-65fbd8d761.patch" }, "scripts": { diff --git a/packages/cli/bin/cli.js b/packages/cli/bin/cli.js index 89394a709020..6d0be3ad0a66 100755 --- a/packages/cli/bin/cli.js +++ b/packages/cli/bin/cli.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -require("../build/cli.js"); +await import("../build/cli.js"); diff --git a/packages/cli/build.sh b/packages/cli/build.sh index f115a319a6e3..362b33b9baf9 100755 --- a/packages/cli/build.sh +++ b/packages/cli/build.sh @@ -28,5 +28,6 @@ esbuild src/cli.tsx \ --sourcemap \ --external:zwave-js \ --external:react-devtools-core \ + --external:yoga-wasm-web \ --banner:js="import { createRequire } from 'module'; var require = require || createRequire(import.meta.url);" # Fix esbuild not being able to do dynamic require() in ESM mode diff --git a/packages/cli/package.json b/packages/cli/package.json index f3652f6cb043..683d38545b12 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -46,6 +46,7 @@ "dependencies": { "@zwave-js/core": "workspace:*", "@zwave-js/shared": "workspace:*", + "yoga-wasm-web": "~0.3.3", "zwave-js": "workspace:*" }, "devDependencies": { @@ -59,7 +60,7 @@ "del-cli": "^5.0.0", "esbuild": "0.15.7", "esbuild-register": "^3.4.2", - "ink": "https://github.com/AlCalzone/ink#lines-stable", + "ink": "https://github.com/AlCalzone/ink#pixels-stable", "ink-select-input": "^5.0.0", "ink-spinner": "^5.0.0", "ink-text-input": "^5.0.0", diff --git a/packages/cli/src/cli.tsx b/packages/cli/src/cli.tsx index 6a90b8bc1858..6bd4ac819f43 100644 --- a/packages/cli/src/cli.tsx +++ b/packages/cli/src/cli.tsx @@ -1,8 +1,9 @@ import { getErrorMessage } from "@zwave-js/shared"; -import { Box, Line, render, Spacer, Text, useInput } from "ink"; +import { Box, render, Spacer, Text, useInput } from "ink"; import { useCallback, useEffect, useRef, useState } from "react"; import type { Driver } from "zwave-js"; import { Frame } from "./components/Frame.js"; +import { Line } from "./components/Line.js"; import { Log } from "./components/Log.js"; import { InlineQuery, diff --git a/packages/cli/src/components/CommandPalette.tsx b/packages/cli/src/components/CommandPalette.tsx index 933b3911c7ae..270e96edb900 100644 --- a/packages/cli/src/components/CommandPalette.tsx +++ b/packages/cli/src/components/CommandPalette.tsx @@ -1,7 +1,8 @@ -import { Box, Line } from "ink"; +import { Box } from "ink"; import React from "react"; import { Frame } from "./Frame.js"; import { HotkeyLabel, HotkeyLabelProps } from "./HotkeyLabel.js"; +import { Line } from "./Line.js"; export interface CommandPaletteProps { label?: React.ReactNode; diff --git a/packages/cli/src/components/Line.tsx b/packages/cli/src/components/Line.tsx new file mode 100644 index 000000000000..90befcde6e6f --- /dev/null +++ b/packages/cli/src/components/Line.tsx @@ -0,0 +1,21 @@ +import { Box, BoxProps } from "ink"; +import { getOuterBoxProps } from "../lib/boxProps.js"; + +export interface LineProps extends BoxProps { + orientation: "horizontal" | "vertical"; +} + +export const Line: React.FC = (props) => { + const { orientation, ...boxProps } = props; + const { ...outerBoxProps } = getOuterBoxProps(boxProps); + + return ( + + ); +}; diff --git a/yarn.lock b/yarn.lock index 713283e9f28c..861aafa99c52 100644 --- a/yarn.lock +++ b/yarn.lock @@ -52,6 +52,16 @@ __metadata: languageName: node linkType: hard +"@alcalzone/ansi-tokenize@npm:^0.1.1": + version: 0.1.1 + resolution: "@alcalzone/ansi-tokenize@npm:0.1.1" + dependencies: + ansi-styles: ^6.2.1 + is-fullwidth-code-point: ^4.0.0 + checksum: 73c453cf280cf00f4feee4de389e987a86be27d1c35279edb6b09d0d67ebd69250eaa6bb27083cf7315dca59bebb550a28fac330e5b29ac6b752f7f1644ee026 + languageName: node + linkType: hard + "@alcalzone/ink-table@npm:~1.1.0": version: 1.1.0 resolution: "@alcalzone/ink-table@npm:1.1.0" @@ -1644,13 +1654,6 @@ __metadata: languageName: node linkType: hard -"@types/yoga-layout@npm:1.9.2": - version: 1.9.2 - resolution: "@types/yoga-layout@npm:1.9.2" - checksum: dbc3d6ab997d50fe1fcca5dd6822982c8fe586145ab648e0e97c3bc4ebc93d0b40c9edd75febaba374d61f60c1379b639f6be652965c776a901bf1068f2eac87 - languageName: node - linkType: hard - "@typescript-eslint/eslint-plugin@npm:^5.48.0": version: 5.48.0 resolution: "@typescript-eslint/eslint-plugin@npm:5.48.0" @@ -1833,7 +1836,7 @@ __metadata: del-cli: ^5.0.0 esbuild: 0.15.7 esbuild-register: ^3.4.2 - ink: "https://github.com/AlCalzone/ink#lines-stable" + ink: "https://github.com/AlCalzone/ink#pixels-stable" ink-select-input: ^5.0.0 ink-spinner: ^5.0.0 ink-text-input: ^5.0.0 @@ -1843,6 +1846,7 @@ __metadata: react-devtools-core: ^4.27.2 typescript: 4.9.5 winston: ^3.8.2 + yoga-wasm-web: ~0.3.3 zwave-js: "workspace:*" bin: cli: bin/cli.js @@ -2332,11 +2336,11 @@ __metadata: linkType: hard "ansi-escapes@npm:^6.0.0": - version: 6.0.0 - resolution: "ansi-escapes@npm:6.0.0" + version: 6.1.0 + resolution: "ansi-escapes@npm:6.1.0" dependencies: type-fest: ^3.0.0 - checksum: 1ddc0b27b1d040c3c703c9cd80ee0a103817e2f9fa8f1adf0c66e970b57543ec60effdb0bd1a396ed7182bca3b1a0d8fda60ec61fee862d353db81b1c3650a78 + checksum: 7ce5d9cefd3d7345dc00161aea2ea9ad5fb3dd66658d4e8731ea047be838d755100f0823a05523d0e518e8e080746fc0a45d3ea3053099376bdd572efaedc7c1 languageName: node linkType: hard @@ -2400,6 +2404,13 @@ __metadata: languageName: node linkType: hard +"ansi-styles@npm:^6.2.1": + version: 6.2.1 + resolution: "ansi-styles@npm:6.2.1" + checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 + languageName: node + linkType: hard + "any-promise@npm:^1.0.0": version: 1.3.0 resolution: "any-promise@npm:1.3.0" @@ -3774,13 +3785,6 @@ __metadata: languageName: node linkType: hard -"emoji-regex@npm:^10.2.1": - version: 10.2.1 - resolution: "emoji-regex@npm:10.2.1" - checksum: 1aa2d16881c56531fdfc03d0b36f5c2b6221cc4097499a5665b88b711dc3fb4d5b8804f0ca6f00c56e5dcf89bac75f0487eee85da1da77df3a33accc6ecbe426 - languageName: node - linkType: hard - "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" @@ -5318,10 +5322,11 @@ __metadata: languageName: node linkType: hard -"ink@https://github.com/AlCalzone/ink#lines-stable": - version: 4.0.0 - resolution: "ink@https://github.com/AlCalzone/ink.git#commit=770eb7cddb36a326713bef20c2685a5203d92dfc" +"ink@https://github.com/AlCalzone/ink#pixels-stable": + version: 4.1.0 + resolution: "ink@https://github.com/AlCalzone/ink.git#commit=4a0a9b2a8e5c1e21474c31e2743077f3e5301b07" dependencies: + "@alcalzone/ansi-tokenize": ^0.1.1 ansi-escapes: ^6.0.0 auto-bind: ^5.0.1 chalk: ^5.2.0 @@ -5329,22 +5334,23 @@ __metadata: cli-cursor: ^4.0.0 cli-truncate: ^3.1.0 code-excerpt: ^4.0.0 - emoji-regex: ^10.2.1 indent-string: ^5.0.0 is-ci: ^3.0.1 + is-lower-case: ^2.0.2 + is-upper-case: ^2.0.2 lodash: ^4.17.21 patch-console: ^2.0.0 react-reconciler: ^0.29.0 scheduler: ^0.23.0 signal-exit: ^3.0.7 - slice-ansi: ^5.0.0 + slice-ansi: ^6.0.0 stack-utils: ^2.0.6 string-width: ^5.1.2 type-fest: ^0.12.0 widest-line: ^4.0.1 wrap-ansi: ^8.1.0 ws: ^8.12.0 - yoga-layout-prebuilt: ^1.9.6 + yoga-wasm-web: ~0.3.3 peerDependencies: "@types/react": ">=18.0.0" react: ">=18.0.0" @@ -5354,7 +5360,7 @@ __metadata: optional: true react-devtools-core: optional: true - checksum: 4055659133de0390b557fc492edbab3f62edf1a4899ef932096dcddf2fac4609f18ce2a1ce6c013fa4825d7aae4ea506c9e3e30db344901afebf82aabd7a8002 + checksum: 2e8ff8b42621b70334943cbda64242ea96a4fac8906a204aaa8c3cbbe71f58eb21a1690e27eedd7621f571249ea07eab2f74c25be01756b0e18f6360fc3b50c1 languageName: node linkType: hard @@ -5578,6 +5584,15 @@ __metadata: languageName: node linkType: hard +"is-lower-case@npm:^2.0.2": + version: 2.0.2 + resolution: "is-lower-case@npm:2.0.2" + dependencies: + tslib: ^2.0.3 + checksum: ba57dd1201e15fd9b590654736afccf1b3b68e919f40c23ef13b00ebcc639b1d9c2f81fe86415bff3e8eccffec459786c9ac9dc8f3a19cfa4484206c411c1d7d + languageName: node + linkType: hard + "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" @@ -5685,6 +5700,15 @@ __metadata: languageName: node linkType: hard +"is-upper-case@npm:^2.0.2": + version: 2.0.2 + resolution: "is-upper-case@npm:2.0.2" + dependencies: + tslib: ^2.0.3 + checksum: cf4fd43c00c2e72cd5cff911923070b89f0933b464941bd782e2315385f80b5a5acd772db3b796542e5e3cfed735f4dffd88c54d62db1ebfc5c3daa7b1af2bc6 + languageName: node + linkType: hard + "is-utf8@npm:^0.2.1": version: 0.2.1 resolution: "is-utf8@npm:0.2.1" @@ -7984,6 +8008,16 @@ __metadata: languageName: node linkType: hard +"slice-ansi@npm:^6.0.0": + version: 6.0.0 + resolution: "slice-ansi@npm:6.0.0" + dependencies: + ansi-styles: ^6.2.1 + is-fullwidth-code-point: ^4.0.0 + checksum: d0510f02af166eff9948e7cf88985c33d9ded6de0e39908b67b5e29f802c88025c27d7e1801ce1d1d1ec311fb539538086cd2a4193d2e8f735e6c5c0e63486dd + languageName: node + linkType: hard + "smart-buffer@npm:^4.1.0": version: 4.1.0 resolution: "smart-buffer@npm:4.1.0" @@ -8595,6 +8629,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.0.3": + version: 2.5.0 + resolution: "tslib@npm:2.5.0" + checksum: ae3ed5f9ce29932d049908ebfdf21b3a003a85653a9a140d614da6b767a93ef94f460e52c3d787f0e4f383546981713f165037dc2274df212ea9f8a4541004e1 + languageName: node + linkType: hard + "tslib@npm:^2.1.0": version: 2.4.0 resolution: "tslib@npm:2.4.0" @@ -8763,7 +8804,14 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^3.0.0, type-fest@npm:^3.6.1": +"type-fest@npm:^3.0.0": + version: 3.8.0 + resolution: "type-fest@npm:3.8.0" + checksum: f9a9ef00378dddd6af2be5cbb67ce4c3a61f6696c5f3ae88815c98266865766118343d928faec8a0efc012efe1d080f59bf62d8fdc382bf285f45d02dbc8fb66 + languageName: node + linkType: hard + +"type-fest@npm:^3.6.1": version: 3.6.1 resolution: "type-fest@npm:3.6.1" checksum: f7e39bf6b74a883661ec8642707f49c33cfcdc6221e1ba36b1d329c1cf301d87351b3ca0839b894cbfe47dc62140c0ce47e69c88f76800b678e0b67b7fe826e6 @@ -9271,21 +9319,10 @@ __metadata: languageName: node linkType: hard -"yoga-layout-prebuilt@npm:1.10.0": - version: 1.10.0 - resolution: "yoga-layout-prebuilt@npm:1.10.0" - dependencies: - "@types/yoga-layout": 1.9.2 - checksum: 6954c7c7b04c585a1c974391bea4734611adb85702b5e9131549a1d3dc5b94e69bcfea34121cdaeb5e702663bf290fcce5374910128e54d1031503a57c062865 - languageName: node - linkType: hard - -"yoga-layout-prebuilt@patch:yoga-layout-prebuilt@npm%3A1.10.0#./.yarn/patches/yoga-layout-prebuilt-npm-1.10.0-855b15449f.patch::locator=%40zwave-js%2Frepo%40workspace%3A.": - version: 1.10.0 - resolution: "yoga-layout-prebuilt@patch:yoga-layout-prebuilt@npm%3A1.10.0#./.yarn/patches/yoga-layout-prebuilt-npm-1.10.0-855b15449f.patch::version=1.10.0&hash=e91576&locator=%40zwave-js%2Frepo%40workspace%3A." - dependencies: - "@types/yoga-layout": 1.9.2 - checksum: 821f9b799915d25c9ad1086855e30312b1328d724baa377e2b2dffc994c54900a486b6a1bd2e41d5dc644000a1028b90ad530e2d60f56405423d3bfbc0c02a81 +"yoga-wasm-web@npm:~0.3.3": + version: 0.3.3 + resolution: "yoga-wasm-web@npm:0.3.3" + checksum: ff65192a832975ff531a1b6eae160c2da859c250feaa58b6389b684f9b48f53fda849a7ea49d12d241198309e671e6bd230a44e7155af9573d7843ac48831c98 languageName: node linkType: hard