mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-21 21:45:42 -05:00
feat: Quartz TUI
This commit is contained in:
parent
47de5cc55e
commit
292c1a1f2c
1
.gitignore
vendored
1
.gitignore
vendored
@ -10,3 +10,4 @@ private/
|
|||||||
.replit
|
.replit
|
||||||
replit.nix
|
replit.nix
|
||||||
.quartz/
|
.quartz/
|
||||||
|
quartz/cli/tui/dist/
|
||||||
|
|||||||
1109
package-lock.json
generated
1109
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,6 +17,7 @@
|
|||||||
"check": "tsc --noEmit && npx prettier . --check",
|
"check": "tsc --noEmit && npx prettier . --check",
|
||||||
"format": "npx prettier . --write",
|
"format": "npx prettier . --write",
|
||||||
"test": "tsx --test",
|
"test": "tsx --test",
|
||||||
|
"build:tui": "node quartz/cli/build-tui.mjs",
|
||||||
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1",
|
"profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1",
|
||||||
"install-plugins": "npx tsx ./quartz/plugins/loader/install-plugins.ts",
|
"install-plugins": "npx tsx ./quartz/plugins/loader/install-plugins.ts",
|
||||||
"prebuild": "npm run install-plugins"
|
"prebuild": "npm run install-plugins"
|
||||||
@ -41,6 +42,8 @@
|
|||||||
"@floating-ui/dom": "^1.7.4",
|
"@floating-ui/dom": "^1.7.4",
|
||||||
"@myriaddreamin/rehype-typst": "^0.6.0",
|
"@myriaddreamin/rehype-typst": "^0.6.0",
|
||||||
"@napi-rs/simple-git": "0.1.22",
|
"@napi-rs/simple-git": "0.1.22",
|
||||||
|
"@opentui/core": "^0.1.80",
|
||||||
|
"@opentui/react": "^0.1.80",
|
||||||
"ansi-truncate": "^1.4.0",
|
"ansi-truncate": "^1.4.0",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
@ -65,6 +68,7 @@
|
|||||||
"preact-render-to-string": "^6.6.5",
|
"preact-render-to-string": "^6.6.5",
|
||||||
"pretty-bytes": "^7.1.0",
|
"pretty-bytes": "^7.1.0",
|
||||||
"pretty-time": "^1.1.0",
|
"pretty-time": "^1.1.0",
|
||||||
|
"react": "^19.2.4",
|
||||||
"reading-time": "^1.5.0",
|
"reading-time": "^1.5.0",
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
"rehype-citation": "^2.3.1",
|
"rehype-citation": "^2.3.1",
|
||||||
@ -101,6 +105,7 @@
|
|||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^25.0.10",
|
"@types/node": "^25.0.10",
|
||||||
"@types/pretty-time": "^1.1.5",
|
"@types/pretty-time": "^1.1.5",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
"@types/source-map-support": "^0.5.10",
|
"@types/source-map-support": "^0.5.10",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@types/yargs": "^17.0.35",
|
"@types/yargs": "^17.0.35",
|
||||||
|
|||||||
378
quartz.lock.json
378
quartz.lock.json
@ -1,59 +1,11 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"explorer": {
|
"alias-redirects": {
|
||||||
"source": "github:quartz-community/explorer",
|
"source": "github:quartz-community/alias-redirects",
|
||||||
"resolved": "https://github.com/quartz-community/explorer.git",
|
"resolved": "https://github.com/quartz-community/alias-redirects.git",
|
||||||
"commit": "09452e44a4c0e9b2c1d94e6e7a5dc98137dac230",
|
"commit": "65897957ab8108d015cd4255d42d84df7377c083",
|
||||||
"installedAt": "2026-02-17T17:28:27.727Z"
|
"installedAt": "2026-02-17T17:28:43.267Z"
|
||||||
},
|
|
||||||
"graph": {
|
|
||||||
"source": "github:quartz-community/graph",
|
|
||||||
"resolved": "https://github.com/quartz-community/graph.git",
|
|
||||||
"commit": "f61ef4aaad0a560428373508e49efa8bf5d0dbf6",
|
|
||||||
"installedAt": "2026-02-17T17:28:28.934Z"
|
|
||||||
},
|
|
||||||
"search": {
|
|
||||||
"source": "github:quartz-community/search",
|
|
||||||
"resolved": "https://github.com/quartz-community/search.git",
|
|
||||||
"commit": "5d206baacbad17e99925f61c6263e98a494258d1",
|
|
||||||
"installedAt": "2026-02-17T17:28:29.442Z"
|
|
||||||
},
|
|
||||||
"backlinks": {
|
|
||||||
"source": "github:quartz-community/backlinks",
|
|
||||||
"resolved": "https://github.com/quartz-community/backlinks.git",
|
|
||||||
"commit": "8adb778566a3d8425d321d6bbc0b673cba9ec63d",
|
|
||||||
"installedAt": "2026-02-17T17:28:30.109Z"
|
|
||||||
},
|
|
||||||
"table-of-contents": {
|
|
||||||
"source": "github:quartz-community/table-of-contents",
|
|
||||||
"resolved": "https://github.com/quartz-community/table-of-contents.git",
|
|
||||||
"commit": "38e7f0198114e4466cb39ea0c2ebb01e46847d39",
|
|
||||||
"installedAt": "2026-02-17T17:28:30.541Z"
|
|
||||||
},
|
|
||||||
"comments": {
|
|
||||||
"source": "github:quartz-community/comments",
|
|
||||||
"resolved": "https://github.com/quartz-community/comments.git",
|
|
||||||
"commit": "a2bee0c8f2f899f4f181a8a6f52ef300eb6bb8e4",
|
|
||||||
"installedAt": "2026-02-17T17:28:30.983Z"
|
|
||||||
},
|
|
||||||
"breadcrumbs": {
|
|
||||||
"source": "github:quartz-community/breadcrumbs",
|
|
||||||
"resolved": "https://github.com/quartz-community/breadcrumbs.git",
|
|
||||||
"commit": "441c3c474b2a7aeecb483aa4421c7a633458e97d",
|
|
||||||
"installedAt": "2026-02-17T17:28:31.401Z"
|
|
||||||
},
|
|
||||||
"recent-notes": {
|
|
||||||
"source": "github:quartz-community/recent-notes",
|
|
||||||
"resolved": "https://github.com/quartz-community/recent-notes.git",
|
|
||||||
"commit": "96d5df9fc6d7e0e8bd3a90856faaa08d2ac3441b",
|
|
||||||
"installedAt": "2026-02-17T17:28:31.827Z"
|
|
||||||
},
|
|
||||||
"latex": {
|
|
||||||
"source": "github:quartz-community/latex",
|
|
||||||
"resolved": "https://github.com/quartz-community/latex.git",
|
|
||||||
"commit": "d7d4a8de001ec18289d12b58c8144f27ab72cb06",
|
|
||||||
"installedAt": "2026-02-17T17:28:32.350Z"
|
|
||||||
},
|
},
|
||||||
"article-title": {
|
"article-title": {
|
||||||
"source": "github:quartz-community/article-title",
|
"source": "github:quartz-community/article-title",
|
||||||
@ -61,101 +13,23 @@
|
|||||||
"commit": "031872e39ba30d8567af63c6a3891b3054432194",
|
"commit": "031872e39ba30d8567af63c6a3891b3054432194",
|
||||||
"installedAt": "2026-02-17T17:28:32.818Z"
|
"installedAt": "2026-02-17T17:28:32.818Z"
|
||||||
},
|
},
|
||||||
"tag-list": {
|
"backlinks": {
|
||||||
"source": "github:quartz-community/tag-list",
|
"source": "github:quartz-community/backlinks",
|
||||||
"resolved": "https://github.com/quartz-community/tag-list.git",
|
"resolved": "https://github.com/quartz-community/backlinks.git",
|
||||||
"commit": "a0a30b822447261a7a9bcf96d649ca7525eb3ded",
|
"commit": "8adb778566a3d8425d321d6bbc0b673cba9ec63d",
|
||||||
"installedAt": "2026-02-17T17:28:33.366Z"
|
"installedAt": "2026-02-17T17:28:30.109Z"
|
||||||
},
|
},
|
||||||
"page-title": {
|
"breadcrumbs": {
|
||||||
"source": "github:quartz-community/page-title",
|
"source": "github:quartz-community/breadcrumbs",
|
||||||
"resolved": "https://github.com/quartz-community/page-title.git",
|
"resolved": "https://github.com/quartz-community/breadcrumbs.git",
|
||||||
"commit": "d14bffe11830eff6e4489945324a0dd33d994bf6",
|
"commit": "441c3c474b2a7aeecb483aa4421c7a633458e97d",
|
||||||
"installedAt": "2026-02-17T17:28:33.828Z"
|
"installedAt": "2026-02-17T17:28:31.401Z"
|
||||||
},
|
},
|
||||||
"darkmode": {
|
"canvas-page": {
|
||||||
"source": "github:quartz-community/darkmode",
|
"source": "github:quartz-community/canvas-page",
|
||||||
"resolved": "https://github.com/quartz-community/darkmode.git",
|
"resolved": "https://github.com/quartz-community/canvas-page.git",
|
||||||
"commit": "453e59cf913b2276a5bb6d6bfdc0685304618877",
|
"commit": "18107bc8d759a1860caeda10de336ac66bf7f923",
|
||||||
"installedAt": "2026-02-17T17:28:34.395Z"
|
"installedAt": "2026-02-18T15:02:34.059Z"
|
||||||
},
|
|
||||||
"reader-mode": {
|
|
||||||
"source": "github:quartz-community/reader-mode",
|
|
||||||
"resolved": "https://github.com/quartz-community/reader-mode.git",
|
|
||||||
"commit": "d362013e0de040dcd8711e7c6bf68b93a0668bae",
|
|
||||||
"installedAt": "2026-02-17T17:28:34.989Z"
|
|
||||||
},
|
|
||||||
"content-meta": {
|
|
||||||
"source": "github:quartz-community/content-meta",
|
|
||||||
"resolved": "https://github.com/quartz-community/content-meta.git",
|
|
||||||
"commit": "be178b7ab4f521f0ca29051676f207a23b706051",
|
|
||||||
"installedAt": "2026-02-17T17:28:35.416Z"
|
|
||||||
},
|
|
||||||
"footer": {
|
|
||||||
"source": "github:quartz-community/footer",
|
|
||||||
"resolved": "https://github.com/quartz-community/footer.git",
|
|
||||||
"commit": "eef4ba0af26ea6097543784fc5cfa6820d6b8250",
|
|
||||||
"installedAt": "2026-02-17T17:28:35.903Z"
|
|
||||||
},
|
|
||||||
"content-page": {
|
|
||||||
"source": "github:quartz-community/content-page",
|
|
||||||
"resolved": "https://github.com/quartz-community/content-page.git",
|
|
||||||
"commit": "9975389d442707d8772aebeaba02676169569c60",
|
|
||||||
"installedAt": "2026-02-17T17:28:36.353Z"
|
|
||||||
},
|
|
||||||
"folder-page": {
|
|
||||||
"source": "github:quartz-community/folder-page",
|
|
||||||
"resolved": "https://github.com/quartz-community/folder-page.git",
|
|
||||||
"commit": "630c26711de6a5cb91ba2b81882f4fad91bbbaeb",
|
|
||||||
"installedAt": "2026-02-17T17:28:36.806Z"
|
|
||||||
},
|
|
||||||
"tag-page": {
|
|
||||||
"source": "github:quartz-community/tag-page",
|
|
||||||
"resolved": "https://github.com/quartz-community/tag-page.git",
|
|
||||||
"commit": "27ad16e609832805b2e11c7836937706a38caec1",
|
|
||||||
"installedAt": "2026-02-17T17:28:37.377Z"
|
|
||||||
},
|
|
||||||
"created-modified-date": {
|
|
||||||
"source": "github:quartz-community/created-modified-date",
|
|
||||||
"resolved": "https://github.com/quartz-community/created-modified-date.git",
|
|
||||||
"commit": "4d976e88da01d9fa0dbdcd11e756b1b3037b3a49",
|
|
||||||
"installedAt": "2026-02-17T17:28:37.833Z"
|
|
||||||
},
|
|
||||||
"syntax-highlighting": {
|
|
||||||
"source": "github:quartz-community/syntax-highlighting",
|
|
||||||
"resolved": "https://github.com/quartz-community/syntax-highlighting.git",
|
|
||||||
"commit": "a3ec90046f68e8d8bbbd275f07f606268cb6e001",
|
|
||||||
"installedAt": "2026-02-17T17:28:38.257Z"
|
|
||||||
},
|
|
||||||
"obsidian-flavored-markdown": {
|
|
||||||
"source": "github:quartz-community/obsidian-flavored-markdown",
|
|
||||||
"resolved": "https://github.com/quartz-community/obsidian-flavored-markdown.git",
|
|
||||||
"commit": "a5f45c21cec287e8aa2896b5032fe48962ed9b77",
|
|
||||||
"installedAt": "2026-02-17T17:28:38.693Z"
|
|
||||||
},
|
|
||||||
"github-flavored-markdown": {
|
|
||||||
"source": "github:quartz-community/github-flavored-markdown",
|
|
||||||
"resolved": "https://github.com/quartz-community/github-flavored-markdown.git",
|
|
||||||
"commit": "7e19794a519debc994e2e99c67b223661b0aac0c",
|
|
||||||
"installedAt": "2026-02-17T17:28:39.147Z"
|
|
||||||
},
|
|
||||||
"crawl-links": {
|
|
||||||
"source": "github:quartz-community/crawl-links",
|
|
||||||
"resolved": "https://github.com/quartz-community/crawl-links.git",
|
|
||||||
"commit": "3ca88b4ba1a049cc79b3cf1154b771e4eb8f7549",
|
|
||||||
"installedAt": "2026-02-17T17:28:39.629Z"
|
|
||||||
},
|
|
||||||
"description": {
|
|
||||||
"source": "github:quartz-community/description",
|
|
||||||
"resolved": "https://github.com/quartz-community/description.git",
|
|
||||||
"commit": "82f1c95e53acc7521fa6bf46e9ea20018ee28264",
|
|
||||||
"installedAt": "2026-02-17T17:28:40.060Z"
|
|
||||||
},
|
|
||||||
"hard-line-breaks": {
|
|
||||||
"source": "github:quartz-community/hard-line-breaks",
|
|
||||||
"resolved": "https://github.com/quartz-community/hard-line-breaks.git",
|
|
||||||
"commit": "b14a477db87e6657440a044e6d2fbf9d943472e6",
|
|
||||||
"installedAt": "2026-02-17T17:28:40.498Z"
|
|
||||||
},
|
},
|
||||||
"citations": {
|
"citations": {
|
||||||
"source": "github:quartz-community/citations",
|
"source": "github:quartz-community/citations",
|
||||||
@ -163,47 +37,17 @@
|
|||||||
"commit": "b5db4ea6fd6f3fc9c1ce37cd9cdba0ca9e74ecc8",
|
"commit": "b5db4ea6fd6f3fc9c1ce37cd9cdba0ca9e74ecc8",
|
||||||
"installedAt": "2026-02-17T17:28:40.960Z"
|
"installedAt": "2026-02-17T17:28:40.960Z"
|
||||||
},
|
},
|
||||||
"ox-hugo": {
|
|
||||||
"source": "github:quartz-community/ox-hugo",
|
|
||||||
"resolved": "https://github.com/quartz-community/ox-hugo.git",
|
|
||||||
"commit": "d8fdf7c6a54464e960c6b817147968cf07719086",
|
|
||||||
"installedAt": "2026-02-17T17:28:41.395Z"
|
|
||||||
},
|
|
||||||
"roam": {
|
|
||||||
"source": "github:quartz-community/roam",
|
|
||||||
"resolved": "https://github.com/quartz-community/roam.git",
|
|
||||||
"commit": "f94276c284edb01e6b4136ec6f7683f29123c54c",
|
|
||||||
"installedAt": "2026-02-17T17:28:41.832Z"
|
|
||||||
},
|
|
||||||
"remove-draft": {
|
|
||||||
"source": "github:quartz-community/remove-draft",
|
|
||||||
"resolved": "https://github.com/quartz-community/remove-draft.git",
|
|
||||||
"commit": "61744308cf739f63fe351dea8dffd6b7c434440d",
|
|
||||||
"installedAt": "2026-02-17T17:28:42.324Z"
|
|
||||||
},
|
|
||||||
"explicit-publish": {
|
|
||||||
"source": "github:quartz-community/explicit-publish",
|
|
||||||
"resolved": "https://github.com/quartz-community/explicit-publish.git",
|
|
||||||
"commit": "ba19e6f32393fa5fe788dbf39a9487bc8e73247a",
|
|
||||||
"installedAt": "2026-02-17T17:28:42.799Z"
|
|
||||||
},
|
|
||||||
"alias-redirects": {
|
|
||||||
"source": "github:quartz-community/alias-redirects",
|
|
||||||
"resolved": "https://github.com/quartz-community/alias-redirects.git",
|
|
||||||
"commit": "65897957ab8108d015cd4255d42d84df7377c083",
|
|
||||||
"installedAt": "2026-02-17T17:28:43.267Z"
|
|
||||||
},
|
|
||||||
"cname": {
|
"cname": {
|
||||||
"source": "github:quartz-community/cname",
|
"source": "github:quartz-community/cname",
|
||||||
"resolved": "https://github.com/quartz-community/cname.git",
|
"resolved": "https://github.com/quartz-community/cname.git",
|
||||||
"commit": "752470f04410576a767b48094e926d41eef3eb18",
|
"commit": "752470f04410576a767b48094e926d41eef3eb18",
|
||||||
"installedAt": "2026-02-17T17:28:43.713Z"
|
"installedAt": "2026-02-17T17:28:43.713Z"
|
||||||
},
|
},
|
||||||
"favicon": {
|
"comments": {
|
||||||
"source": "github:quartz-community/favicon",
|
"source": "github:quartz-community/comments",
|
||||||
"resolved": "https://github.com/quartz-community/favicon.git",
|
"resolved": "https://github.com/quartz-community/comments.git",
|
||||||
"commit": "71cfff28b7a41f28618f574e672b2b399c6b781d",
|
"commit": "bf03ced30c7828c3ae4dae5d313e596983f5d453",
|
||||||
"installedAt": "2026-02-17T17:28:44.365Z"
|
"installedAt": "2026-02-18T19:52:41.699Z"
|
||||||
},
|
},
|
||||||
"content-index": {
|
"content-index": {
|
||||||
"source": "github:quartz-community/content-index",
|
"source": "github:quartz-community/content-index",
|
||||||
@ -211,17 +55,179 @@
|
|||||||
"commit": "2b9c62d659b7f8e9e7cc028a201cae7061224653",
|
"commit": "2b9c62d659b7f8e9e7cc028a201cae7061224653",
|
||||||
"installedAt": "2026-02-17T17:28:44.806Z"
|
"installedAt": "2026-02-17T17:28:44.806Z"
|
||||||
},
|
},
|
||||||
|
"content-meta": {
|
||||||
|
"source": "github:quartz-community/content-meta",
|
||||||
|
"resolved": "https://github.com/quartz-community/content-meta.git",
|
||||||
|
"commit": "be178b7ab4f521f0ca29051676f207a23b706051",
|
||||||
|
"installedAt": "2026-02-17T17:28:35.416Z"
|
||||||
|
},
|
||||||
|
"content-page": {
|
||||||
|
"source": "github:quartz-community/content-page",
|
||||||
|
"resolved": "https://github.com/quartz-community/content-page.git",
|
||||||
|
"commit": "738cdc7ab6bc5718e68db4d318b1001a25a1d7d9",
|
||||||
|
"installedAt": "2026-02-18T15:02:36.681Z"
|
||||||
|
},
|
||||||
|
"crawl-links": {
|
||||||
|
"source": "github:quartz-community/crawl-links",
|
||||||
|
"resolved": "https://github.com/quartz-community/crawl-links.git",
|
||||||
|
"commit": "58bdc4878d0e0eae126f8389465ef9c7c2a1fbc9",
|
||||||
|
"installedAt": "2026-02-18T19:52:43.755Z"
|
||||||
|
},
|
||||||
|
"created-modified-date": {
|
||||||
|
"source": "github:quartz-community/created-modified-date",
|
||||||
|
"resolved": "https://github.com/quartz-community/created-modified-date.git",
|
||||||
|
"commit": "0a129541ba64a28ea747fba6f2391611c2b6185c",
|
||||||
|
"installedAt": "2026-02-18T19:52:44.210Z"
|
||||||
|
},
|
||||||
|
"darkmode": {
|
||||||
|
"source": "github:quartz-community/darkmode",
|
||||||
|
"resolved": "https://github.com/quartz-community/darkmode.git",
|
||||||
|
"commit": "453e59cf913b2276a5bb6d6bfdc0685304618877",
|
||||||
|
"installedAt": "2026-02-17T17:28:34.395Z"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"source": "github:quartz-community/description",
|
||||||
|
"resolved": "https://github.com/quartz-community/description.git",
|
||||||
|
"commit": "82f1c95e53acc7521fa6bf46e9ea20018ee28264",
|
||||||
|
"installedAt": "2026-02-17T17:28:40.060Z"
|
||||||
|
},
|
||||||
|
"explicit-publish": {
|
||||||
|
"source": "github:quartz-community/explicit-publish",
|
||||||
|
"resolved": "https://github.com/quartz-community/explicit-publish.git",
|
||||||
|
"commit": "ba19e6f32393fa5fe788dbf39a9487bc8e73247a",
|
||||||
|
"installedAt": "2026-02-17T17:28:42.799Z"
|
||||||
|
},
|
||||||
|
"explorer": {
|
||||||
|
"source": "github:quartz-community/explorer",
|
||||||
|
"resolved": "https://github.com/quartz-community/explorer.git",
|
||||||
|
"commit": "99f0b2a62f85d60352542ecaf3c516816a401068",
|
||||||
|
"installedAt": "2026-02-18T15:02:39.752Z"
|
||||||
|
},
|
||||||
|
"favicon": {
|
||||||
|
"source": "github:quartz-community/favicon",
|
||||||
|
"resolved": "https://github.com/quartz-community/favicon.git",
|
||||||
|
"commit": "71cfff28b7a41f28618f574e672b2b399c6b781d",
|
||||||
|
"installedAt": "2026-02-17T17:28:44.365Z"
|
||||||
|
},
|
||||||
|
"folder-page": {
|
||||||
|
"source": "github:quartz-community/folder-page",
|
||||||
|
"resolved": "https://github.com/quartz-community/folder-page.git",
|
||||||
|
"commit": "2684accc90cf56ebc21ab21f301fdb8b847f328a",
|
||||||
|
"installedAt": "2026-02-18T15:02:40.643Z"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"source": "github:quartz-community/footer",
|
||||||
|
"resolved": "https://github.com/quartz-community/footer.git",
|
||||||
|
"commit": "eef4ba0af26ea6097543784fc5cfa6820d6b8250",
|
||||||
|
"installedAt": "2026-02-17T17:28:35.903Z"
|
||||||
|
},
|
||||||
|
"github-flavored-markdown": {
|
||||||
|
"source": "github:quartz-community/github-flavored-markdown",
|
||||||
|
"resolved": "https://github.com/quartz-community/github-flavored-markdown.git",
|
||||||
|
"commit": "7e19794a519debc994e2e99c67b223661b0aac0c",
|
||||||
|
"installedAt": "2026-02-17T17:28:39.147Z"
|
||||||
|
},
|
||||||
|
"graph": {
|
||||||
|
"source": "github:quartz-community/graph",
|
||||||
|
"resolved": "https://github.com/quartz-community/graph.git",
|
||||||
|
"commit": "b908529c04044e907680cc9b9f37854ae1a14000",
|
||||||
|
"installedAt": "2026-02-18T15:02:42.108Z"
|
||||||
|
},
|
||||||
|
"hard-line-breaks": {
|
||||||
|
"source": "github:quartz-community/hard-line-breaks",
|
||||||
|
"resolved": "https://github.com/quartz-community/hard-line-breaks.git",
|
||||||
|
"commit": "b14a477db87e6657440a044e6d2fbf9d943472e6",
|
||||||
|
"installedAt": "2026-02-17T17:28:40.498Z"
|
||||||
|
},
|
||||||
|
"latex": {
|
||||||
|
"source": "github:quartz-community/latex",
|
||||||
|
"resolved": "https://github.com/quartz-community/latex.git",
|
||||||
|
"commit": "6660addddc5af95437d225ebf3ec2cfecf4527e7",
|
||||||
|
"installedAt": "2026-02-18T19:52:49.775Z"
|
||||||
|
},
|
||||||
|
"obsidian-flavored-markdown": {
|
||||||
|
"source": "github:quartz-community/obsidian-flavored-markdown",
|
||||||
|
"resolved": "https://github.com/quartz-community/obsidian-flavored-markdown.git",
|
||||||
|
"commit": "a5f45c21cec287e8aa2896b5032fe48962ed9b77",
|
||||||
|
"installedAt": "2026-02-17T17:28:38.693Z"
|
||||||
|
},
|
||||||
"og-image": {
|
"og-image": {
|
||||||
"source": "github:quartz-community/og-image",
|
"source": "github:quartz-community/og-image",
|
||||||
"resolved": "https://github.com/quartz-community/og-image.git",
|
"resolved": "https://github.com/quartz-community/og-image.git",
|
||||||
"commit": "9a00809fccb0e1e4b7db4ee6e6eed002952396f4",
|
"commit": "9a00809fccb0e1e4b7db4ee6e6eed002952396f4",
|
||||||
"installedAt": "2026-02-17T17:28:45.533Z"
|
"installedAt": "2026-02-17T17:28:45.533Z"
|
||||||
},
|
},
|
||||||
"canvas-page": {
|
"ox-hugo": {
|
||||||
"source": "github:quartz-community/canvas-page",
|
"source": "github:quartz-community/ox-hugo",
|
||||||
"resolved": "https://github.com/quartz-community/canvas-page.git",
|
"resolved": "https://github.com/quartz-community/ox-hugo.git",
|
||||||
"commit": "683c2da6bb068d3e596cdb3bfcca68007829cd17",
|
"commit": "d8fdf7c6a54464e960c6b817147968cf07719086",
|
||||||
"installedAt": "2026-02-17T17:28:46.012Z"
|
"installedAt": "2026-02-17T17:28:41.395Z"
|
||||||
|
},
|
||||||
|
"page-title": {
|
||||||
|
"source": "github:quartz-community/page-title",
|
||||||
|
"resolved": "https://github.com/quartz-community/page-title.git",
|
||||||
|
"commit": "d14bffe11830eff6e4489945324a0dd33d994bf6",
|
||||||
|
"installedAt": "2026-02-17T17:28:33.828Z"
|
||||||
|
},
|
||||||
|
"reader-mode": {
|
||||||
|
"source": "github:quartz-community/reader-mode",
|
||||||
|
"resolved": "https://github.com/quartz-community/reader-mode.git",
|
||||||
|
"commit": "d362013e0de040dcd8711e7c6bf68b93a0668bae",
|
||||||
|
"installedAt": "2026-02-17T17:28:34.989Z"
|
||||||
|
},
|
||||||
|
"recent-notes": {
|
||||||
|
"source": "github:quartz-community/recent-notes",
|
||||||
|
"resolved": "https://github.com/quartz-community/recent-notes.git",
|
||||||
|
"commit": "96d5df9fc6d7e0e8bd3a90856faaa08d2ac3441b",
|
||||||
|
"installedAt": "2026-02-17T17:28:31.827Z"
|
||||||
|
},
|
||||||
|
"remove-draft": {
|
||||||
|
"source": "github:quartz-community/remove-draft",
|
||||||
|
"resolved": "https://github.com/quartz-community/remove-draft.git",
|
||||||
|
"commit": "61744308cf739f63fe351dea8dffd6b7c434440d",
|
||||||
|
"installedAt": "2026-02-17T17:28:42.324Z"
|
||||||
|
},
|
||||||
|
"roam": {
|
||||||
|
"source": "github:quartz-community/roam",
|
||||||
|
"resolved": "https://github.com/quartz-community/roam.git",
|
||||||
|
"commit": "f94276c284edb01e6b4136ec6f7683f29123c54c",
|
||||||
|
"installedAt": "2026-02-17T17:28:41.832Z"
|
||||||
|
},
|
||||||
|
"search": {
|
||||||
|
"source": "github:quartz-community/search",
|
||||||
|
"resolved": "https://github.com/quartz-community/search.git",
|
||||||
|
"commit": "5d206baacbad17e99925f61c6263e98a494258d1",
|
||||||
|
"installedAt": "2026-02-17T17:28:29.442Z"
|
||||||
|
},
|
||||||
|
"spacer": {
|
||||||
|
"source": "github:quartz-community/spacer",
|
||||||
|
"resolved": "https://github.com/quartz-community/spacer.git",
|
||||||
|
"commit": "b972767017536c879d92ac00f4732fc8c993d560",
|
||||||
|
"installedAt": "2026-02-17T20:39:12.603Z"
|
||||||
|
},
|
||||||
|
"syntax-highlighting": {
|
||||||
|
"source": "github:quartz-community/syntax-highlighting",
|
||||||
|
"resolved": "https://github.com/quartz-community/syntax-highlighting.git",
|
||||||
|
"commit": "a3ec90046f68e8d8bbbd275f07f606268cb6e001",
|
||||||
|
"installedAt": "2026-02-17T17:28:38.257Z"
|
||||||
|
},
|
||||||
|
"table-of-contents": {
|
||||||
|
"source": "github:quartz-community/table-of-contents",
|
||||||
|
"resolved": "https://github.com/quartz-community/table-of-contents.git",
|
||||||
|
"commit": "38e7f0198114e4466cb39ea0c2ebb01e46847d39",
|
||||||
|
"installedAt": "2026-02-17T17:28:30.541Z"
|
||||||
|
},
|
||||||
|
"tag-list": {
|
||||||
|
"source": "github:quartz-community/tag-list",
|
||||||
|
"resolved": "https://github.com/quartz-community/tag-list.git",
|
||||||
|
"commit": "a0a30b822447261a7a9bcf96d649ca7525eb3ded",
|
||||||
|
"installedAt": "2026-02-17T17:28:33.366Z"
|
||||||
|
},
|
||||||
|
"tag-page": {
|
||||||
|
"source": "github:quartz-community/tag-page",
|
||||||
|
"resolved": "https://github.com/quartz-community/tag-page.git",
|
||||||
|
"commit": "45e16343dd6bb85174237fc66f81e835244e7935",
|
||||||
|
"installedAt": "2026-02-18T15:02:50.019Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,47 @@ import {
|
|||||||
} from "./cli/args.js"
|
} from "./cli/args.js"
|
||||||
import { version } from "./cli/constants.js"
|
import { version } from "./cli/constants.js"
|
||||||
|
|
||||||
|
async function launchTui() {
|
||||||
|
const { pathToFileURL } = await import("url")
|
||||||
|
const { join } = await import("path")
|
||||||
|
const { existsSync } = await import("fs")
|
||||||
|
const { spawn } = await import("child_process")
|
||||||
|
const tuiPath = join(process.cwd(), "quartz", "cli", "tui", "dist", "App.mjs")
|
||||||
|
|
||||||
|
if (!existsSync(tuiPath)) {
|
||||||
|
console.log("TUI not built yet. Building...")
|
||||||
|
const buildScript = pathToFileURL(join(process.cwd(), "quartz", "cli", "build-tui.mjs")).href
|
||||||
|
await import(buildScript)
|
||||||
|
console.log("TUI built successfully.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenTUI requires Bun runtime (uses bun:ffi for Zig renderer)
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const child = spawn("bun", ["run", tuiPath], {
|
||||||
|
stdio: "inherit",
|
||||||
|
cwd: process.cwd(),
|
||||||
|
})
|
||||||
|
|
||||||
|
child.on("error", (err) => {
|
||||||
|
if (err.code === "ENOENT") {
|
||||||
|
console.error(
|
||||||
|
"Error: Bun runtime not found. The TUI requires Bun to run.\n" +
|
||||||
|
"Install Bun: https://bun.sh/docs/installation",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
reject(new Error(`TUI exited with code ${code}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.scriptName("quartz")
|
.scriptName("quartz")
|
||||||
.version(version)
|
.version(version)
|
||||||
@ -59,8 +100,11 @@ yargs(hideBin(process.argv))
|
|||||||
.command("migrate", "Migrate old config to quartz.plugins.json", CommonArgv, async () => {
|
.command("migrate", "Migrate old config to quartz.plugins.json", CommonArgv, async () => {
|
||||||
await handleMigrate()
|
await handleMigrate()
|
||||||
})
|
})
|
||||||
|
.command("tui", "Launch interactive plugin manager", CommonArgv, async () => {
|
||||||
|
await launchTui()
|
||||||
|
})
|
||||||
.command(
|
.command(
|
||||||
"plugin <subcommand>",
|
"plugin [subcommand]",
|
||||||
"Manage Quartz plugins",
|
"Manage Quartz plugins",
|
||||||
(yargs) => {
|
(yargs) => {
|
||||||
return yargs
|
return yargs
|
||||||
@ -125,10 +169,11 @@ yargs(hideBin(process.argv))
|
|||||||
.command("check", "Check for plugin updates", CommonArgv, async () => {
|
.command("check", "Check for plugin updates", CommonArgv, async () => {
|
||||||
await handlePluginCheck()
|
await handlePluginCheck()
|
||||||
})
|
})
|
||||||
.demandCommand(1, "Please specify a plugin subcommand")
|
.demandCommand(0, "")
|
||||||
},
|
},
|
||||||
async () => {
|
async (argv) => {
|
||||||
// This handler is called when no subcommand is provided
|
if (!argv._.includes("plugin") || argv._.length > 1) return
|
||||||
|
await launchTui()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.showHelpOnFail(false)
|
.showHelpOnFail(false)
|
||||||
|
|||||||
21
quartz/cli/build-tui.mjs
Normal file
21
quartz/cli/build-tui.mjs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import esbuild from "esbuild"
|
||||||
|
import path from "path"
|
||||||
|
import { fileURLToPath } from "url"
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
const tuiDir = path.join(__dirname, "tui")
|
||||||
|
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: [path.join(tuiDir, "App.tsx")],
|
||||||
|
outdir: path.join(tuiDir, "dist"),
|
||||||
|
bundle: true,
|
||||||
|
platform: "node",
|
||||||
|
format: "esm",
|
||||||
|
jsx: "automatic",
|
||||||
|
jsxImportSource: "@opentui/react",
|
||||||
|
packages: "external",
|
||||||
|
sourcemap: true,
|
||||||
|
target: "esnext",
|
||||||
|
outExtension: { ".js": ".mjs" },
|
||||||
|
})
|
||||||
280
quartz/cli/plugin-data.js
Normal file
280
quartz/cli/plugin-data.js
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import { execSync } from "child_process"
|
||||||
|
|
||||||
|
const LOCKFILE_PATH = path.join(process.cwd(), "quartz.lock.json")
|
||||||
|
const PLUGINS_DIR = path.join(process.cwd(), ".quartz", "plugins")
|
||||||
|
const PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.json")
|
||||||
|
const DEFAULT_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.default.json")
|
||||||
|
|
||||||
|
export function readPluginsJson() {
|
||||||
|
if (!fs.existsSync(PLUGINS_JSON_PATH)) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(PLUGINS_JSON_PATH, "utf-8"))
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writePluginsJson(data) {
|
||||||
|
fs.writeFileSync(PLUGINS_JSON_PATH, JSON.stringify(data, null, 2) + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readDefaultPluginsJson() {
|
||||||
|
if (!fs.existsSync(DEFAULT_PLUGINS_JSON_PATH)) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(DEFAULT_PLUGINS_JSON_PATH, "utf-8"))
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readLockfile() {
|
||||||
|
if (!fs.existsSync(LOCKFILE_PATH)) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(LOCKFILE_PATH, "utf-8"))
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeLockfile(lockfile) {
|
||||||
|
if (lockfile.plugins) {
|
||||||
|
const sorted = {}
|
||||||
|
for (const key of Object.keys(lockfile.plugins).sort()) {
|
||||||
|
sorted[key] = lockfile.plugins[key]
|
||||||
|
}
|
||||||
|
lockfile = { ...lockfile, plugins: sorted }
|
||||||
|
}
|
||||||
|
fs.writeFileSync(LOCKFILE_PATH, JSON.stringify(lockfile, null, 2) + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractPluginName(source) {
|
||||||
|
if (source.startsWith("github:")) {
|
||||||
|
const withoutPrefix = source.replace("github:", "")
|
||||||
|
const [repoPath] = withoutPrefix.split("#")
|
||||||
|
const parts = repoPath.split("/")
|
||||||
|
return parts[parts.length - 1]
|
||||||
|
}
|
||||||
|
if (source.startsWith("git+") || source.startsWith("https://")) {
|
||||||
|
const url = source.replace("git+", "")
|
||||||
|
const match = url.match(/\/([^/]+?)(?:\.git)?(?:#|$)/)
|
||||||
|
return match?.[1] ?? source
|
||||||
|
}
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readManifestFromPackageJson(pluginDir) {
|
||||||
|
const pkgPath = path.join(pluginDir, "package.json")
|
||||||
|
if (!fs.existsSync(pkgPath)) return null
|
||||||
|
try {
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
||||||
|
return pkg.quartz ?? null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseGitSource(source) {
|
||||||
|
if (source.startsWith("github:")) {
|
||||||
|
const [repoPath, ref] = source.replace("github:", "").split("#")
|
||||||
|
const [owner, repo] = repoPath.split("/")
|
||||||
|
return { name: repo, url: `https://github.com/${owner}/${repo}.git`, ref }
|
||||||
|
}
|
||||||
|
if (source.startsWith("git+")) {
|
||||||
|
const url = source.replace("git+", "")
|
||||||
|
const name = path.basename(url, ".git")
|
||||||
|
return { name, url }
|
||||||
|
}
|
||||||
|
if (source.startsWith("https://")) {
|
||||||
|
const name = path.basename(source, ".git")
|
||||||
|
return { name, url: source }
|
||||||
|
}
|
||||||
|
throw new Error(`Cannot parse plugin source: ${source}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGitCommit(pluginDir) {
|
||||||
|
try {
|
||||||
|
return execSync("git rev-parse HEAD", { cwd: pluginDir, encoding: "utf-8" }).trim()
|
||||||
|
} catch {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPluginDir(name) {
|
||||||
|
return path.join(PLUGINS_DIR, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pluginDirExists(name) {
|
||||||
|
return fs.existsSync(path.join(PLUGINS_DIR, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensurePluginsDir() {
|
||||||
|
if (!fs.existsSync(PLUGINS_DIR)) {
|
||||||
|
fs.mkdirSync(PLUGINS_DIR, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges quartz.plugins.json, quartz.lock.json, and on-disk manifest data
|
||||||
|
* into enriched plugin entries with: name, displayName, source, enabled,
|
||||||
|
* options, order, layout, category, installed, locked, manifest,
|
||||||
|
* currentCommit, modified.
|
||||||
|
*/
|
||||||
|
export function getEnrichedPlugins() {
|
||||||
|
const pluginsJson = readPluginsJson()
|
||||||
|
const lockfile = readLockfile()
|
||||||
|
|
||||||
|
if (!pluginsJson?.plugins) return []
|
||||||
|
|
||||||
|
return pluginsJson.plugins.map((entry, index) => {
|
||||||
|
const name = extractPluginName(entry.source)
|
||||||
|
const pluginDir = path.join(PLUGINS_DIR, name)
|
||||||
|
const installed = fs.existsSync(pluginDir)
|
||||||
|
const locked = lockfile?.plugins?.[name] ?? null
|
||||||
|
const manifest = installed ? readManifestFromPackageJson(pluginDir) : null
|
||||||
|
const currentCommit = installed ? getGitCommit(pluginDir) : null
|
||||||
|
const modified = locked && currentCommit ? currentCommit !== locked.commit : false
|
||||||
|
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
name,
|
||||||
|
displayName: manifest?.displayName ?? name,
|
||||||
|
source: entry.source,
|
||||||
|
enabled: entry.enabled ?? true,
|
||||||
|
options: entry.options ?? {},
|
||||||
|
order: entry.order ?? 50,
|
||||||
|
layout: entry.layout ?? null,
|
||||||
|
category: manifest?.category ?? "unknown",
|
||||||
|
installed,
|
||||||
|
locked,
|
||||||
|
manifest,
|
||||||
|
currentCommit,
|
||||||
|
modified,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLayoutConfig() {
|
||||||
|
const pluginsJson = readPluginsJson()
|
||||||
|
return pluginsJson?.layout ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getGlobalConfig() {
|
||||||
|
const pluginsJson = readPluginsJson()
|
||||||
|
return pluginsJson?.configuration ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePluginEntry(index, updates) {
|
||||||
|
const json = readPluginsJson()
|
||||||
|
if (!json?.plugins?.[index]) return false
|
||||||
|
Object.assign(json.plugins[index], updates)
|
||||||
|
writePluginsJson(json)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateGlobalConfig(updates) {
|
||||||
|
const json = readPluginsJson()
|
||||||
|
if (!json) return false
|
||||||
|
json.configuration = { ...json.configuration, ...updates }
|
||||||
|
writePluginsJson(json)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateLayoutConfig(layout) {
|
||||||
|
const json = readPluginsJson()
|
||||||
|
if (!json) return false
|
||||||
|
json.layout = layout
|
||||||
|
writePluginsJson(json)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reorderPlugin(fromIndex, toIndex) {
|
||||||
|
const json = readPluginsJson()
|
||||||
|
if (!json?.plugins) return false
|
||||||
|
const [moved] = json.plugins.splice(fromIndex, 1)
|
||||||
|
json.plugins.splice(toIndex, 0, moved)
|
||||||
|
writePluginsJson(json)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removePluginEntry(index) {
|
||||||
|
const json = readPluginsJson()
|
||||||
|
if (!json?.plugins?.[index]) return false
|
||||||
|
json.plugins.splice(index, 1)
|
||||||
|
writePluginsJson(json)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addPluginEntry(entry) {
|
||||||
|
const json = readPluginsJson()
|
||||||
|
if (!json) return false
|
||||||
|
if (!json.plugins) json.plugins = []
|
||||||
|
json.plugins.push(entry)
|
||||||
|
writePluginsJson(json)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function configExists() {
|
||||||
|
return fs.existsSync(PLUGINS_JSON_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createConfigFromDefault() {
|
||||||
|
const defaultJson = readDefaultPluginsJson()
|
||||||
|
if (!defaultJson) {
|
||||||
|
// No default available — create minimal config
|
||||||
|
const minimal = {
|
||||||
|
$schema: "./quartz/plugins/quartz-plugins.schema.json",
|
||||||
|
configuration: {
|
||||||
|
pageTitle: "Quartz",
|
||||||
|
enableSPA: true,
|
||||||
|
enablePopovers: true,
|
||||||
|
analytics: { provider: "plausible" },
|
||||||
|
locale: "en-US",
|
||||||
|
baseUrl: "quartz.jzhao.xyz",
|
||||||
|
ignorePatterns: ["private", "templates", ".obsidian"],
|
||||||
|
defaultDateType: "created",
|
||||||
|
theme: {
|
||||||
|
cdnCaching: true,
|
||||||
|
typography: {
|
||||||
|
header: "Schibsted Grotesk",
|
||||||
|
body: "Source Sans Pro",
|
||||||
|
code: "IBM Plex Mono",
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
lightMode: {
|
||||||
|
light: "#faf8f8",
|
||||||
|
lightgray: "#e5e5e5",
|
||||||
|
gray: "#b8b8b8",
|
||||||
|
darkgray: "#4e4e4e",
|
||||||
|
dark: "#2b2b2b",
|
||||||
|
secondary: "#284b63",
|
||||||
|
tertiary: "#84a59d",
|
||||||
|
highlight: "rgba(143, 159, 169, 0.15)",
|
||||||
|
textHighlight: "#fff23688",
|
||||||
|
},
|
||||||
|
darkMode: {
|
||||||
|
light: "#161618",
|
||||||
|
lightgray: "#393639",
|
||||||
|
gray: "#646464",
|
||||||
|
darkgray: "#d4d4d4",
|
||||||
|
dark: "#ebebec",
|
||||||
|
secondary: "#7b97aa",
|
||||||
|
tertiary: "#84a59d",
|
||||||
|
highlight: "rgba(143, 159, 169, 0.15)",
|
||||||
|
textHighlight: "#fff23688",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
layout: { groups: {}, byPageType: {} },
|
||||||
|
}
|
||||||
|
writePluginsJson(minimal)
|
||||||
|
return minimal
|
||||||
|
}
|
||||||
|
writePluginsJson(defaultJson)
|
||||||
|
return defaultJson
|
||||||
|
}
|
||||||
|
|
||||||
|
export { LOCKFILE_PATH, PLUGINS_DIR, PLUGINS_JSON_PATH, DEFAULT_PLUGINS_JSON_PATH }
|
||||||
@ -2,51 +2,21 @@ import fs from "fs"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { execSync } from "child_process"
|
import { execSync } from "child_process"
|
||||||
import { styleText } from "util"
|
import { styleText } from "util"
|
||||||
|
import {
|
||||||
|
readPluginsJson,
|
||||||
|
writePluginsJson,
|
||||||
|
readLockfile,
|
||||||
|
writeLockfile,
|
||||||
|
extractPluginName,
|
||||||
|
readManifestFromPackageJson,
|
||||||
|
parseGitSource,
|
||||||
|
getGitCommit,
|
||||||
|
PLUGINS_DIR,
|
||||||
|
LOCKFILE_PATH,
|
||||||
|
} from "./plugin-data.js"
|
||||||
|
|
||||||
const LOCKFILE_PATH = path.join(process.cwd(), "quartz.lock.json")
|
|
||||||
const PLUGINS_DIR = path.join(process.cwd(), ".quartz", "plugins")
|
|
||||||
const PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.json")
|
|
||||||
const INTERNAL_EXPORTS = new Set(["manifest", "default"])
|
const INTERNAL_EXPORTS = new Set(["manifest", "default"])
|
||||||
|
|
||||||
function readPluginsJson() {
|
|
||||||
if (!fs.existsSync(PLUGINS_JSON_PATH)) return null
|
|
||||||
try {
|
|
||||||
return JSON.parse(fs.readFileSync(PLUGINS_JSON_PATH, "utf-8"))
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writePluginsJson(data) {
|
|
||||||
fs.writeFileSync(PLUGINS_JSON_PATH, JSON.stringify(data, null, 2) + "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractPluginName(source) {
|
|
||||||
if (source.startsWith("github:")) {
|
|
||||||
const withoutPrefix = source.replace("github:", "")
|
|
||||||
const [repoPath] = withoutPrefix.split("#")
|
|
||||||
const parts = repoPath.split("/")
|
|
||||||
return parts[parts.length - 1]
|
|
||||||
}
|
|
||||||
if (source.startsWith("git+") || source.startsWith("https://")) {
|
|
||||||
const url = source.replace("git+", "")
|
|
||||||
const match = url.match(/\/([^/]+?)(?:\.git)?(?:#|$)/)
|
|
||||||
return match?.[1] ?? source
|
|
||||||
}
|
|
||||||
return source
|
|
||||||
}
|
|
||||||
|
|
||||||
function readManifestFromPackageJson(pluginDir) {
|
|
||||||
const pkgPath = path.join(pluginDir, "package.json")
|
|
||||||
if (!fs.existsSync(pkgPath)) return null
|
|
||||||
try {
|
|
||||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
|
||||||
return pkg.quartz ?? null
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildPlugin(pluginDir, name) {
|
function buildPlugin(pluginDir, name) {
|
||||||
try {
|
try {
|
||||||
console.log(styleText("cyan", ` → ${name}: installing dependencies...`))
|
console.log(styleText("cyan", ` → ${name}: installing dependencies...`))
|
||||||
@ -128,48 +98,6 @@ async function regeneratePluginIndex() {
|
|||||||
fs.writeFileSync(indexPath, indexContent)
|
fs.writeFileSync(indexPath, indexContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
function readLockfile() {
|
|
||||||
if (!fs.existsSync(LOCKFILE_PATH)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(LOCKFILE_PATH, "utf-8")
|
|
||||||
return JSON.parse(content)
|
|
||||||
} catch {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeLockfile(lockfile) {
|
|
||||||
fs.writeFileSync(LOCKFILE_PATH, JSON.stringify(lockfile, null, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseGitSource(source) {
|
|
||||||
if (source.startsWith("github:")) {
|
|
||||||
const [repoPath, ref] = source.replace("github:", "").split("#")
|
|
||||||
const [owner, repo] = repoPath.split("/")
|
|
||||||
return { name: repo, url: `https://github.com/${owner}/${repo}.git`, ref }
|
|
||||||
}
|
|
||||||
if (source.startsWith("git+")) {
|
|
||||||
const url = source.replace("git+", "")
|
|
||||||
const name = path.basename(url, ".git")
|
|
||||||
return { name, url }
|
|
||||||
}
|
|
||||||
if (source.startsWith("https://")) {
|
|
||||||
const name = path.basename(source, ".git")
|
|
||||||
return { name, url: source }
|
|
||||||
}
|
|
||||||
throw new Error(`Cannot parse plugin source: ${source}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGitCommit(pluginDir) {
|
|
||||||
try {
|
|
||||||
return execSync("git rev-parse HEAD", { cwd: pluginDir, encoding: "utf-8" }).trim()
|
|
||||||
} catch {
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handlePluginInstall() {
|
export async function handlePluginInstall() {
|
||||||
const lockfile = readLockfile()
|
const lockfile = readLockfile()
|
||||||
|
|
||||||
|
|||||||
114
quartz/cli/tui/App.tsx
Normal file
114
quartz/cli/tui/App.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { useCallback, useState } from "react"
|
||||||
|
import { createCliRenderer } from "@opentui/core"
|
||||||
|
import { createRoot, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
||||||
|
import { Notification, type NotificationMessage } from "./components/Notification.js"
|
||||||
|
import { SetupWizard } from "./components/SetupWizard.js"
|
||||||
|
import { StatusBar } from "./components/StatusBar.js"
|
||||||
|
import { LayoutPanel } from "./panels/LayoutPanel.js"
|
||||||
|
import { PluginsPanel } from "./panels/PluginsPanel.js"
|
||||||
|
import { SettingsPanel } from "./panels/SettingsPanel.js"
|
||||||
|
import { version } from "../constants.js"
|
||||||
|
import { configExists } from "../plugin-data.js"
|
||||||
|
|
||||||
|
const TABS = ["Plugins", "Layout", "Settings"] as const
|
||||||
|
type Tab = (typeof TABS)[number]
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const renderer = useRenderer()
|
||||||
|
const { height: rows } = useTerminalDimensions()
|
||||||
|
const [hasConfig, setHasConfig] = useState(() => configExists())
|
||||||
|
const [activeTab, setActiveTab] = useState<Tab>("Plugins")
|
||||||
|
const [notification, setNotification] = useState<NotificationMessage | null>(null)
|
||||||
|
const [panelFocused, setPanelFocused] = useState(false)
|
||||||
|
|
||||||
|
const notify = useCallback((message: string, type: "success" | "error" | "info" = "info") => {
|
||||||
|
setNotification({ message, type })
|
||||||
|
setTimeout(() => setNotification(null), 3000)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useKeyboard((event) => {
|
||||||
|
if (panelFocused || !hasConfig) return
|
||||||
|
if (event.name !== "q") return
|
||||||
|
renderer.destroy()
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
useKeyboard((event) => {
|
||||||
|
if (!hasConfig || panelFocused) return
|
||||||
|
if (event.name !== "tab") return
|
||||||
|
|
||||||
|
const currentIndex = TABS.indexOf(activeTab)
|
||||||
|
const next = event.shift
|
||||||
|
? (currentIndex - 1 + TABS.length) % TABS.length
|
||||||
|
: (currentIndex + 1) % TABS.length
|
||||||
|
setActiveTab(TABS[next])
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!hasConfig) {
|
||||||
|
return (
|
||||||
|
<box flexDirection="column" height={rows}>
|
||||||
|
<box justifyContent="center" paddingY={0}>
|
||||||
|
<text>
|
||||||
|
<span fg="green">
|
||||||
|
<strong>{` Quartz v${version} Plugin Manager `}</strong>
|
||||||
|
</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
|
||||||
|
<box flexDirection="column" flexGrow={1} justifyContent="center">
|
||||||
|
<SetupWizard
|
||||||
|
onComplete={() => {
|
||||||
|
setHasConfig(true)
|
||||||
|
notify("Configuration created", "success")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
|
||||||
|
{notification && <Notification message={notification} />}
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box flexDirection="column" height={rows}>
|
||||||
|
<box justifyContent="center" paddingY={0}>
|
||||||
|
<text>
|
||||||
|
<span fg="green">
|
||||||
|
<strong>{` Quartz v${version} Plugin Manager `}</strong>
|
||||||
|
</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
|
||||||
|
<box flexDirection="row" paddingX={1} gap={2}>
|
||||||
|
{TABS.map((tab) => (
|
||||||
|
<text key={tab}>
|
||||||
|
{tab === activeTab ? (
|
||||||
|
<span fg="cyan">
|
||||||
|
<strong>[ {tab} ]</strong>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span fg="#888888">{` ${tab} `}</span>
|
||||||
|
)}
|
||||||
|
</text>
|
||||||
|
))}
|
||||||
|
</box>
|
||||||
|
|
||||||
|
<box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
|
||||||
|
{activeTab === "Plugins" && (
|
||||||
|
<PluginsPanel notify={notify} onFocusChange={setPanelFocused} />
|
||||||
|
)}
|
||||||
|
{activeTab === "Layout" && <LayoutPanel notify={notify} onFocusChange={setPanelFocused} />}
|
||||||
|
{activeTab === "Settings" && (
|
||||||
|
<SettingsPanel notify={notify} onFocusChange={setPanelFocused} />
|
||||||
|
)}
|
||||||
|
</box>
|
||||||
|
|
||||||
|
{notification && <Notification message={notification} />}
|
||||||
|
|
||||||
|
<StatusBar activeTab={activeTab} />
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderer = await createCliRenderer({ exitOnCtrlC: true })
|
||||||
|
createRoot(renderer).render(<App />)
|
||||||
572
quartz/cli/tui/async-plugin-ops.ts
Normal file
572
quartz/cli/tui/async-plugin-ops.ts
Normal file
@ -0,0 +1,572 @@
|
|||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import { execFile } from "child_process"
|
||||||
|
import { promisify } from "util"
|
||||||
|
import {
|
||||||
|
readPluginsJson,
|
||||||
|
writePluginsJson,
|
||||||
|
readLockfile,
|
||||||
|
writeLockfile,
|
||||||
|
extractPluginName,
|
||||||
|
readManifestFromPackageJson,
|
||||||
|
parseGitSource,
|
||||||
|
PLUGINS_DIR,
|
||||||
|
} from "../plugin-data.js"
|
||||||
|
|
||||||
|
export type ProgressCallback = (
|
||||||
|
message: string,
|
||||||
|
type: "info" | "success" | "error" | "warning",
|
||||||
|
) => void
|
||||||
|
|
||||||
|
export interface OperationResult {
|
||||||
|
success: boolean
|
||||||
|
installed?: number
|
||||||
|
failed?: number
|
||||||
|
updated?: string[]
|
||||||
|
errors?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LockfileEntry {
|
||||||
|
source: string
|
||||||
|
resolved: string
|
||||||
|
commit: string
|
||||||
|
installedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Lockfile {
|
||||||
|
version: string
|
||||||
|
plugins: Record<string, LockfileEntry>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginsJsonEntry {
|
||||||
|
source: string
|
||||||
|
enabled?: boolean
|
||||||
|
options?: Record<string, unknown>
|
||||||
|
order?: number
|
||||||
|
layout?: {
|
||||||
|
position: string
|
||||||
|
priority?: number
|
||||||
|
display?: string
|
||||||
|
condition?: string
|
||||||
|
group?: string
|
||||||
|
groupOptions?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginsJson {
|
||||||
|
plugins?: PluginsJsonEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginManifestComponent {
|
||||||
|
defaultPosition?: string
|
||||||
|
defaultPriority?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginManifest {
|
||||||
|
defaultEnabled?: boolean
|
||||||
|
defaultOptions?: Record<string, unknown>
|
||||||
|
defaultOrder?: number
|
||||||
|
components?: Record<string, PluginManifestComponent>
|
||||||
|
}
|
||||||
|
|
||||||
|
const INTERNAL_EXPORTS = new Set(["manifest", "default"])
|
||||||
|
const execFileAsync = promisify(execFile)
|
||||||
|
|
||||||
|
function report(
|
||||||
|
onProgress: ProgressCallback | undefined,
|
||||||
|
message: string,
|
||||||
|
type: OperationResultType,
|
||||||
|
) {
|
||||||
|
onProgress?.(message, type)
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperationResultType = "info" | "success" | "error" | "warning"
|
||||||
|
|
||||||
|
function shortCommit(commit: string) {
|
||||||
|
return commit.slice(0, 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runGit(args: string[], cwd?: string, timeout = 60000) {
|
||||||
|
const { stdout } = await execFileAsync("git", args, { cwd, timeout, encoding: "utf-8" })
|
||||||
|
return stdout.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runNpm(args: string[], cwd: string, timeout = 300000) {
|
||||||
|
await execFileAsync("npm", args, { cwd, timeout, encoding: "utf-8" })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGitCommitAsync(pluginDir: string) {
|
||||||
|
try {
|
||||||
|
const commit = await runGit(["rev-parse", "HEAD"], pluginDir)
|
||||||
|
return commit || "unknown"
|
||||||
|
} catch {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildPlugin(
|
||||||
|
pluginDir: string,
|
||||||
|
name: string,
|
||||||
|
onProgress?: ProgressCallback,
|
||||||
|
): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
report(onProgress, ` → ${name}: installing dependencies...`, "info")
|
||||||
|
await runNpm(["install"], pluginDir)
|
||||||
|
report(onProgress, ` → ${name}: building...`, "info")
|
||||||
|
await runNpm(["run", "build"], pluginDir)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
report(onProgress, ` ✗ ${name}: build failed`, "error")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function needsBuild(pluginDir: string) {
|
||||||
|
const distDir = path.join(pluginDir, "dist")
|
||||||
|
return !fs.existsSync(distDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseExportsFromDts(content: string) {
|
||||||
|
const exports: string[] = []
|
||||||
|
const exportMatches = content.matchAll(/export\s*{\s*([^}]+)\s*}(?:\s*from\s*['"]([^'"]+)['"])?/g)
|
||||||
|
for (const match of exportMatches) {
|
||||||
|
const fromModule = match[2]
|
||||||
|
if (fromModule?.startsWith("@")) continue
|
||||||
|
|
||||||
|
const names = match[1]
|
||||||
|
.split(",")
|
||||||
|
.map((n) => n.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
for (const name of names) {
|
||||||
|
const cleanName = name.split(" as ").pop()?.trim() || name.trim()
|
||||||
|
if (cleanName && !cleanName.startsWith("_") && !INTERNAL_EXPORTS.has(cleanName)) {
|
||||||
|
const finalName = cleanName.replace(/^type\s+/, "")
|
||||||
|
if (name.includes("type ")) {
|
||||||
|
exports.push(`type ${finalName}`)
|
||||||
|
} else {
|
||||||
|
exports.push(finalName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return exports
|
||||||
|
}
|
||||||
|
|
||||||
|
async function regeneratePluginIndex() {
|
||||||
|
if (!fs.existsSync(PLUGINS_DIR)) return
|
||||||
|
|
||||||
|
const entries = await fs.promises.readdir(PLUGINS_DIR, { withFileTypes: true })
|
||||||
|
const plugins = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name)
|
||||||
|
|
||||||
|
const exports: string[] = []
|
||||||
|
|
||||||
|
for (const pluginName of plugins) {
|
||||||
|
const pluginDir = path.join(PLUGINS_DIR, pluginName)
|
||||||
|
const distIndex = path.join(pluginDir, "dist", "index.d.ts")
|
||||||
|
|
||||||
|
if (!fs.existsSync(distIndex)) continue
|
||||||
|
|
||||||
|
const dtsContent = await fs.promises.readFile(distIndex, "utf-8")
|
||||||
|
const exportedNames = parseExportsFromDts(dtsContent)
|
||||||
|
|
||||||
|
if (exportedNames.length > 0) {
|
||||||
|
const namedExports = exportedNames.filter((e) => !e.startsWith("type "))
|
||||||
|
const typeExports = exportedNames.filter((e) => e.startsWith("type ")).map((e) => e.slice(5))
|
||||||
|
|
||||||
|
if (namedExports.length > 0) {
|
||||||
|
exports.push(`export { ${namedExports.join(", ")} } from "./${pluginName}"`)
|
||||||
|
}
|
||||||
|
if (typeExports.length > 0) {
|
||||||
|
exports.push(`export type { ${typeExports.join(", ")} } from "./${pluginName}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexContent = exports.join("\n") + "\n"
|
||||||
|
const indexPath = path.join(PLUGINS_DIR, "index.ts")
|
||||||
|
await fs.promises.writeFile(indexPath, indexContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tuiPluginInstall(onProgress?: ProgressCallback): Promise<OperationResult> {
|
||||||
|
const lockfile = readLockfile() as Lockfile | null
|
||||||
|
|
||||||
|
if (!lockfile) {
|
||||||
|
const message = "⚠ No quartz.lock.json found. Run 'npx quartz plugin add <repo>' first."
|
||||||
|
report(onProgress, message, "warning")
|
||||||
|
return { success: false, errors: [message] }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(PLUGINS_DIR)) {
|
||||||
|
fs.mkdirSync(PLUGINS_DIR, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
report(onProgress, "→ Installing plugins from lockfile...", "info")
|
||||||
|
let installed = 0
|
||||||
|
let failed = 0
|
||||||
|
const errors: string[] = []
|
||||||
|
const pluginsToBuild: Array<{ name: string; pluginDir: string }> = []
|
||||||
|
|
||||||
|
for (const [name, entry] of Object.entries(lockfile.plugins)) {
|
||||||
|
const pluginDir = path.join(PLUGINS_DIR, name)
|
||||||
|
|
||||||
|
if (fs.existsSync(pluginDir)) {
|
||||||
|
try {
|
||||||
|
const currentCommit = await getGitCommitAsync(pluginDir)
|
||||||
|
if (currentCommit === entry.commit && !needsBuild(pluginDir)) {
|
||||||
|
report(onProgress, ` ✓ ${name}@${shortCommit(entry.commit)} already installed`, "info")
|
||||||
|
installed++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (currentCommit !== entry.commit) {
|
||||||
|
report(onProgress, ` → ${name}: updating to ${shortCommit(entry.commit)}...`, "info")
|
||||||
|
await runGit(["fetch", "--depth", "1", "origin"], pluginDir)
|
||||||
|
await runGit(["reset", "--hard", entry.commit], pluginDir)
|
||||||
|
}
|
||||||
|
pluginsToBuild.push({ name, pluginDir })
|
||||||
|
installed++
|
||||||
|
} catch (error) {
|
||||||
|
const message = ` ✗ ${name}: failed to update`
|
||||||
|
report(onProgress, message, "error")
|
||||||
|
errors.push(error instanceof Error ? error.message : String(error))
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
report(onProgress, ` → ${name}: cloning...`, "info")
|
||||||
|
await runGit(["clone", "--depth", "1", entry.resolved, pluginDir])
|
||||||
|
if (entry.commit !== "unknown") {
|
||||||
|
await runGit(["fetch", "--depth", "1", "origin", entry.commit], pluginDir)
|
||||||
|
await runGit(["checkout", entry.commit], pluginDir)
|
||||||
|
}
|
||||||
|
report(onProgress, ` ✓ ${name}@${shortCommit(entry.commit)}`, "success")
|
||||||
|
pluginsToBuild.push({ name, pluginDir })
|
||||||
|
installed++
|
||||||
|
} catch (error) {
|
||||||
|
const message = ` ✗ ${name}: failed to clone`
|
||||||
|
report(onProgress, message, "error")
|
||||||
|
errors.push(error instanceof Error ? error.message : String(error))
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginsToBuild.length > 0) {
|
||||||
|
report(onProgress, "→ Building plugins...", "info")
|
||||||
|
for (const { name, pluginDir } of pluginsToBuild) {
|
||||||
|
if (!(await buildPlugin(pluginDir, name, onProgress))) {
|
||||||
|
failed++
|
||||||
|
installed--
|
||||||
|
} else {
|
||||||
|
report(onProgress, ` ✓ ${name} built`, "success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await regeneratePluginIndex()
|
||||||
|
|
||||||
|
if (failed === 0) {
|
||||||
|
report(onProgress, `✓ Installed ${installed} plugin(s)`, "success")
|
||||||
|
} else {
|
||||||
|
report(onProgress, `⚠ Installed ${installed} plugin(s), ${failed} failed`, "warning")
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: failed === 0, installed, failed, errors }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tuiPluginAdd(
|
||||||
|
sources: string[],
|
||||||
|
onProgress?: ProgressCallback,
|
||||||
|
): Promise<OperationResult> {
|
||||||
|
let lockfile = readLockfile() as Lockfile | null
|
||||||
|
if (!lockfile) {
|
||||||
|
lockfile = { version: "1.0.0", plugins: {} }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(PLUGINS_DIR)) {
|
||||||
|
fs.mkdirSync(PLUGINS_DIR, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const addedPlugins: Array<{ name: string; pluginDir: string; source: string }> = []
|
||||||
|
const errors: string[] = []
|
||||||
|
let failed = 0
|
||||||
|
|
||||||
|
for (const source of sources) {
|
||||||
|
try {
|
||||||
|
const { name, url, ref } = parseGitSource(source)
|
||||||
|
const pluginDir = path.join(PLUGINS_DIR, name)
|
||||||
|
|
||||||
|
if (fs.existsSync(pluginDir)) {
|
||||||
|
report(onProgress, `⚠ ${name} already exists. Use 'update' to refresh.`, "warning")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
report(onProgress, `→ Adding ${name} from ${url}...`, "info")
|
||||||
|
|
||||||
|
if (ref) {
|
||||||
|
await runGit(["clone", "--depth", "1", "--branch", ref, url, pluginDir])
|
||||||
|
} else {
|
||||||
|
await runGit(["clone", "--depth", "1", url, pluginDir])
|
||||||
|
}
|
||||||
|
|
||||||
|
const commit = await getGitCommitAsync(pluginDir)
|
||||||
|
lockfile.plugins[name] = {
|
||||||
|
source,
|
||||||
|
resolved: url,
|
||||||
|
commit,
|
||||||
|
installedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
addedPlugins.push({ name, pluginDir, source })
|
||||||
|
report(onProgress, `✓ Added ${name}@${shortCommit(commit)}`, "success")
|
||||||
|
} catch (error) {
|
||||||
|
const message = `✗ Failed to add ${source}: ${error instanceof Error ? error.message : error}`
|
||||||
|
report(onProgress, message, "error")
|
||||||
|
errors.push(error instanceof Error ? error.message : String(error))
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addedPlugins.length > 0) {
|
||||||
|
report(onProgress, "→ Building plugins...", "info")
|
||||||
|
for (const { name, pluginDir } of addedPlugins) {
|
||||||
|
if (await buildPlugin(pluginDir, name, onProgress)) {
|
||||||
|
report(onProgress, ` ✓ ${name} built`, "success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await regeneratePluginIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLockfile(lockfile)
|
||||||
|
const pluginsJson = readPluginsJson() as PluginsJson | null
|
||||||
|
if (pluginsJson?.plugins) {
|
||||||
|
for (const { pluginDir, source } of addedPlugins) {
|
||||||
|
const manifest = readManifestFromPackageJson(pluginDir) as PluginManifest | null
|
||||||
|
const newEntry: PluginsJsonEntry = {
|
||||||
|
source,
|
||||||
|
enabled: manifest?.defaultEnabled ?? true,
|
||||||
|
options: manifest?.defaultOptions ?? {},
|
||||||
|
order: manifest?.defaultOrder ?? 50,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest?.components) {
|
||||||
|
const firstComponentKey = Object.keys(manifest.components)[0]
|
||||||
|
const comp = manifest.components[firstComponentKey]
|
||||||
|
if (comp?.defaultPosition) {
|
||||||
|
newEntry.layout = {
|
||||||
|
position: comp.defaultPosition,
|
||||||
|
priority: comp.defaultPriority ?? 50,
|
||||||
|
display: "all",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginsJson.plugins.push(newEntry)
|
||||||
|
}
|
||||||
|
writePluginsJson(pluginsJson as Record<string, unknown>)
|
||||||
|
}
|
||||||
|
|
||||||
|
report(onProgress, "Updated quartz.lock.json", "info")
|
||||||
|
return { success: failed === 0, installed: addedPlugins.length, failed, errors }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tuiPluginRemove(
|
||||||
|
names: string[],
|
||||||
|
onProgress?: ProgressCallback,
|
||||||
|
): Promise<OperationResult> {
|
||||||
|
const lockfile = readLockfile() as Lockfile | null
|
||||||
|
if (!lockfile) {
|
||||||
|
const message = "⚠ No plugins installed"
|
||||||
|
report(onProgress, message, "warning")
|
||||||
|
return { success: false, errors: [message] }
|
||||||
|
}
|
||||||
|
|
||||||
|
let removed = false
|
||||||
|
let removedCount = 0
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
for (const name of names) {
|
||||||
|
const pluginDir = path.join(PLUGINS_DIR, name)
|
||||||
|
|
||||||
|
if (!lockfile.plugins[name] && !fs.existsSync(pluginDir)) {
|
||||||
|
report(onProgress, `⚠ ${name} is not installed`, "warning")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
report(onProgress, `→ Removing ${name}...`, "info")
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(pluginDir)) {
|
||||||
|
fs.rmSync(pluginDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
delete lockfile.plugins[name]
|
||||||
|
report(onProgress, `✓ Removed ${name}`, "success")
|
||||||
|
removed = true
|
||||||
|
removedCount++
|
||||||
|
} catch (error) {
|
||||||
|
report(onProgress, `✗ Failed to remove ${name}`, "error")
|
||||||
|
errors.push(error instanceof Error ? error.message : String(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removed) {
|
||||||
|
await regeneratePluginIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLockfile(lockfile)
|
||||||
|
const pluginsJson = readPluginsJson() as PluginsJson | null
|
||||||
|
if (pluginsJson?.plugins) {
|
||||||
|
pluginsJson.plugins = pluginsJson.plugins.filter(
|
||||||
|
(plugin) =>
|
||||||
|
!names.includes(extractPluginName(plugin.source)) && !names.includes(plugin.source),
|
||||||
|
)
|
||||||
|
writePluginsJson(pluginsJson as Record<string, unknown>)
|
||||||
|
}
|
||||||
|
|
||||||
|
report(onProgress, "Updated quartz.lock.json", "info")
|
||||||
|
return { success: errors.length === 0, installed: removedCount, failed: errors.length, errors }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tuiPluginUpdate(
|
||||||
|
names?: string[],
|
||||||
|
onProgress?: ProgressCallback,
|
||||||
|
): Promise<OperationResult> {
|
||||||
|
const lockfile = readLockfile() as Lockfile | null
|
||||||
|
if (!lockfile) {
|
||||||
|
const message = "⚠ No plugins installed"
|
||||||
|
report(onProgress, message, "warning")
|
||||||
|
return { success: false, errors: [message] }
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginsToUpdate = names ?? Object.keys(lockfile.plugins)
|
||||||
|
const updatedPlugins: Array<{ name: string; pluginDir: string }> = []
|
||||||
|
const errors: string[] = []
|
||||||
|
let failed = 0
|
||||||
|
|
||||||
|
for (const name of pluginsToUpdate) {
|
||||||
|
const entry = lockfile.plugins[name]
|
||||||
|
if (!entry) {
|
||||||
|
report(onProgress, `⚠ ${name} is not installed`, "warning")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginDir = path.join(PLUGINS_DIR, name)
|
||||||
|
if (!fs.existsSync(pluginDir)) {
|
||||||
|
report(onProgress, `⚠ ${name} directory missing. Run 'npx quartz plugin install'.`, "warning")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
report(onProgress, `→ Updating ${name}...`, "info")
|
||||||
|
await runGit(["fetch", "--depth", "1", "origin"], pluginDir)
|
||||||
|
await runGit(["reset", "--hard", "origin/HEAD"], pluginDir)
|
||||||
|
|
||||||
|
const newCommit = await getGitCommitAsync(pluginDir)
|
||||||
|
if (newCommit !== entry.commit) {
|
||||||
|
entry.commit = newCommit
|
||||||
|
entry.installedAt = new Date().toISOString()
|
||||||
|
updatedPlugins.push({ name, pluginDir })
|
||||||
|
report(onProgress, `✓ Updated ${name} to ${shortCommit(newCommit)}`, "success")
|
||||||
|
} else {
|
||||||
|
report(onProgress, `✓ ${name} already up to date`, "info")
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = `✗ Failed to update ${name}: ${error instanceof Error ? error.message : error}`
|
||||||
|
report(onProgress, message, "error")
|
||||||
|
errors.push(error instanceof Error ? error.message : String(error))
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updatedPlugins.length > 0) {
|
||||||
|
report(onProgress, "→ Rebuilding updated plugins...", "info")
|
||||||
|
for (const { name, pluginDir } of updatedPlugins) {
|
||||||
|
if (await buildPlugin(pluginDir, name, onProgress)) {
|
||||||
|
report(onProgress, ` ✓ ${name} rebuilt`, "success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await regeneratePluginIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
writeLockfile(lockfile)
|
||||||
|
report(onProgress, "Updated quartz.lock.json", "info")
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: failed === 0,
|
||||||
|
updated: updatedPlugins.map((plugin) => plugin.name),
|
||||||
|
failed,
|
||||||
|
errors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function tuiPluginRestore(onProgress?: ProgressCallback): Promise<OperationResult> {
|
||||||
|
const lockfile = readLockfile() as Lockfile | null
|
||||||
|
if (!lockfile) {
|
||||||
|
const message = "✗ No quartz.lock.json found. Cannot restore."
|
||||||
|
report(onProgress, message, "error")
|
||||||
|
report(
|
||||||
|
onProgress,
|
||||||
|
"Run 'npx quartz plugin add <repo>' to install plugins from scratch.",
|
||||||
|
"info",
|
||||||
|
)
|
||||||
|
return { success: false, errors: [message] }
|
||||||
|
}
|
||||||
|
|
||||||
|
report(onProgress, "→ Restoring plugins from lockfile...", "info")
|
||||||
|
|
||||||
|
if (!fs.existsSync(PLUGINS_DIR)) {
|
||||||
|
fs.mkdirSync(PLUGINS_DIR, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
let installed = 0
|
||||||
|
let failed = 0
|
||||||
|
const restoredPlugins: Array<{ name: string; pluginDir: string }> = []
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
for (const [name, entry] of Object.entries(lockfile.plugins)) {
|
||||||
|
const pluginDir = path.join(PLUGINS_DIR, name)
|
||||||
|
|
||||||
|
if (fs.existsSync(pluginDir)) {
|
||||||
|
report(onProgress, `⚠ ${name}: directory exists, skipping`, "warning")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
report(
|
||||||
|
onProgress,
|
||||||
|
`→ ${name}: cloning ${entry.resolved}@${shortCommit(entry.commit)}...`,
|
||||||
|
"info",
|
||||||
|
)
|
||||||
|
await runGit(["clone", entry.resolved, pluginDir])
|
||||||
|
await runGit(["checkout", entry.commit], pluginDir)
|
||||||
|
report(onProgress, `✓ ${name} restored`, "success")
|
||||||
|
restoredPlugins.push({ name, pluginDir })
|
||||||
|
installed++
|
||||||
|
} catch (error) {
|
||||||
|
report(onProgress, `✗ ${name}: failed to restore`, "error")
|
||||||
|
errors.push(error instanceof Error ? error.message : String(error))
|
||||||
|
failed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restoredPlugins.length > 0) {
|
||||||
|
report(onProgress, "→ Building restored plugins...", "info")
|
||||||
|
for (const { name, pluginDir } of restoredPlugins) {
|
||||||
|
if (!(await buildPlugin(pluginDir, name, onProgress))) {
|
||||||
|
failed++
|
||||||
|
installed--
|
||||||
|
} else {
|
||||||
|
report(onProgress, ` ✓ ${name} built`, "success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await regeneratePluginIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed === 0) {
|
||||||
|
report(onProgress, `✓ Restored ${installed} plugin(s)`, "success")
|
||||||
|
} else {
|
||||||
|
report(onProgress, `⚠ Restored ${installed} plugin(s), ${failed} failed`, "warning")
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: failed === 0, installed, failed, errors }
|
||||||
|
}
|
||||||
175
quartz/cli/tui/cli-modules.d.ts
vendored
Normal file
175
quartz/cli/tui/cli-modules.d.ts
vendored
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
declare module "../plugin-data.js" {
|
||||||
|
export function configExists(): boolean
|
||||||
|
export function createConfigFromDefault(): Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@opentui/core" {
|
||||||
|
export type SelectOption = { name: string; description: string; value?: any }
|
||||||
|
export type TabSelectOption = { name: string; description: string; value?: any }
|
||||||
|
export interface CliRendererOptions {
|
||||||
|
exitOnCtrlC?: boolean
|
||||||
|
useAlternateScreen?: boolean
|
||||||
|
}
|
||||||
|
export function createCliRenderer(options?: CliRendererOptions): Promise<unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "@opentui/react" {
|
||||||
|
export function createRoot(renderer: unknown): { render(element: unknown): void }
|
||||||
|
export function useKeyboard(
|
||||||
|
handler: (event: {
|
||||||
|
name: string
|
||||||
|
shift?: boolean
|
||||||
|
ctrl?: boolean
|
||||||
|
meta?: boolean
|
||||||
|
eventType?: string
|
||||||
|
repeated?: boolean
|
||||||
|
}) => void,
|
||||||
|
): void
|
||||||
|
export function useOnResize(callback: (width: number, height: number) => void): void
|
||||||
|
export function useTimeline(options?: {
|
||||||
|
duration?: number
|
||||||
|
loop?: boolean
|
||||||
|
autoplay?: boolean
|
||||||
|
}): unknown
|
||||||
|
export function useRenderer(): { destroy(): void; console: { show(): void } }
|
||||||
|
export function useTerminalDimensions(): { width: number; height: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "../../plugin-data.js" {
|
||||||
|
export function readPluginsJson(): Record<string, unknown> | null
|
||||||
|
export function writePluginsJson(data: Record<string, unknown>): void
|
||||||
|
export function readDefaultPluginsJson(): Record<string, unknown> | null
|
||||||
|
export function readLockfile(): Record<string, unknown> | null
|
||||||
|
export function writeLockfile(lockfile: Record<string, unknown>): void
|
||||||
|
export function extractPluginName(source: string): string
|
||||||
|
export function readManifestFromPackageJson(pluginDir: string): Record<string, unknown> | null
|
||||||
|
export function parseGitSource(source: string): { name: string; url: string; ref?: string }
|
||||||
|
export function getGitCommit(pluginDir: string): string
|
||||||
|
export function getPluginDir(name: string): string
|
||||||
|
export function pluginDirExists(name: string): boolean
|
||||||
|
export function ensurePluginsDir(): void
|
||||||
|
export function getEnrichedPlugins(): Array<{
|
||||||
|
index: number
|
||||||
|
name: string
|
||||||
|
displayName: string
|
||||||
|
source: string
|
||||||
|
enabled: boolean
|
||||||
|
options: Record<string, unknown>
|
||||||
|
order: number
|
||||||
|
layout: {
|
||||||
|
position: string
|
||||||
|
priority: number
|
||||||
|
display: string
|
||||||
|
condition?: string
|
||||||
|
group?: string
|
||||||
|
groupOptions?: Record<string, unknown>
|
||||||
|
} | null
|
||||||
|
category: string | string[]
|
||||||
|
installed: boolean
|
||||||
|
locked: {
|
||||||
|
source: string
|
||||||
|
resolved: string
|
||||||
|
commit: string
|
||||||
|
installedAt: string
|
||||||
|
} | null
|
||||||
|
manifest: Record<string, unknown> | null
|
||||||
|
currentCommit: string | null
|
||||||
|
modified: boolean
|
||||||
|
}>
|
||||||
|
export function getLayoutConfig(): Record<string, unknown> | null
|
||||||
|
export function getGlobalConfig(): Record<string, unknown> | null
|
||||||
|
export function updatePluginEntry(index: number, updates: Record<string, unknown>): boolean
|
||||||
|
export function updateGlobalConfig(updates: Record<string, unknown>): boolean
|
||||||
|
export function updateLayoutConfig(layout: Record<string, unknown>): boolean
|
||||||
|
export function reorderPlugin(fromIndex: number, toIndex: number): boolean
|
||||||
|
export function removePluginEntry(index: number): boolean
|
||||||
|
export function addPluginEntry(entry: Record<string, unknown>): boolean
|
||||||
|
export function configExists(): boolean
|
||||||
|
export function createConfigFromDefault(): Record<string, unknown>
|
||||||
|
export const LOCKFILE_PATH: string
|
||||||
|
export const PLUGINS_DIR: string
|
||||||
|
export const PLUGINS_JSON_PATH: string
|
||||||
|
export const DEFAULT_PLUGINS_JSON_PATH: string
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "../../plugin-git-handlers.js" {
|
||||||
|
export function handlePluginInstall(): Promise<void>
|
||||||
|
export function handlePluginAdd(sources: string[]): Promise<void>
|
||||||
|
export function handlePluginRemove(names: string[]): Promise<void>
|
||||||
|
export function handlePluginUpdate(names?: string[]): Promise<void>
|
||||||
|
export function handlePluginRestore(): Promise<void>
|
||||||
|
export function handlePluginList(): Promise<void>
|
||||||
|
export function handlePluginEnable(names: string[]): Promise<void>
|
||||||
|
export function handlePluginDisable(names: string[]): Promise<void>
|
||||||
|
export function handlePluginConfig(name: string, options?: { set?: string }): Promise<void>
|
||||||
|
export function handlePluginCheck(): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "./async-plugin-ops.js" {
|
||||||
|
export type ProgressCallback = (
|
||||||
|
message: string,
|
||||||
|
type: "info" | "success" | "error" | "warning",
|
||||||
|
) => void
|
||||||
|
export interface OperationResult {
|
||||||
|
success: boolean
|
||||||
|
installed?: number
|
||||||
|
failed?: number
|
||||||
|
updated?: string[]
|
||||||
|
errors?: string[]
|
||||||
|
}
|
||||||
|
export function tuiPluginUpdate(
|
||||||
|
names?: string[],
|
||||||
|
onProgress?: ProgressCallback,
|
||||||
|
): Promise<OperationResult>
|
||||||
|
export function tuiPluginInstall(onProgress?: ProgressCallback): Promise<OperationResult>
|
||||||
|
export function tuiPluginRestore(onProgress?: ProgressCallback): Promise<OperationResult>
|
||||||
|
export function tuiPluginAdd(
|
||||||
|
sources: string[],
|
||||||
|
onProgress?: ProgressCallback,
|
||||||
|
): Promise<OperationResult>
|
||||||
|
export function tuiPluginRemove(
|
||||||
|
names: string[],
|
||||||
|
onProgress?: ProgressCallback,
|
||||||
|
): Promise<OperationResult>
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "../async-plugin-ops.js" {
|
||||||
|
export type ProgressCallback = (
|
||||||
|
message: string,
|
||||||
|
type: "info" | "success" | "error" | "warning",
|
||||||
|
) => void
|
||||||
|
export interface OperationResult {
|
||||||
|
success: boolean
|
||||||
|
installed?: number
|
||||||
|
failed?: number
|
||||||
|
updated?: string[]
|
||||||
|
errors?: string[]
|
||||||
|
}
|
||||||
|
export function tuiPluginUpdate(
|
||||||
|
names?: string[],
|
||||||
|
onProgress?: ProgressCallback,
|
||||||
|
): Promise<OperationResult>
|
||||||
|
export function tuiPluginInstall(onProgress?: ProgressCallback): Promise<OperationResult>
|
||||||
|
export function tuiPluginRestore(onProgress?: ProgressCallback): Promise<OperationResult>
|
||||||
|
export function tuiPluginAdd(
|
||||||
|
sources: string[],
|
||||||
|
onProgress?: ProgressCallback,
|
||||||
|
): Promise<OperationResult>
|
||||||
|
export function tuiPluginRemove(
|
||||||
|
names: string[],
|
||||||
|
onProgress?: ProgressCallback,
|
||||||
|
): Promise<OperationResult>
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "../constants.js" {
|
||||||
|
export const version: string
|
||||||
|
export const ORIGIN_NAME: string
|
||||||
|
export const UPSTREAM_NAME: string
|
||||||
|
export const QUARTZ_SOURCE_BRANCH: string
|
||||||
|
export const QUARTZ_SOURCE_REPO: string
|
||||||
|
export const cwd: string
|
||||||
|
export const cacheDir: string
|
||||||
|
export const cacheFile: string
|
||||||
|
export const fp: string
|
||||||
|
export const contentCacheFolder: string
|
||||||
|
}
|
||||||
24
quartz/cli/tui/components/Notification.tsx
Normal file
24
quartz/cli/tui/components/Notification.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
export interface NotificationMessage {
|
||||||
|
message: string
|
||||||
|
type: "success" | "error" | "info"
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NotificationProps {
|
||||||
|
message: NotificationMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLOR_MAP = {
|
||||||
|
success: "green",
|
||||||
|
error: "red",
|
||||||
|
info: "cyan",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export function Notification({ message }: NotificationProps) {
|
||||||
|
return (
|
||||||
|
<box paddingX={1}>
|
||||||
|
<text>
|
||||||
|
<span fg={COLOR_MAP[message.type]}>{message.message}</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
116
quartz/cli/tui/components/SetupWizard.tsx
Normal file
116
quartz/cli/tui/components/SetupWizard.tsx
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { type SelectOption } from "@opentui/core"
|
||||||
|
import {
|
||||||
|
createConfigFromDefault,
|
||||||
|
readDefaultPluginsJson,
|
||||||
|
writePluginsJson,
|
||||||
|
} from "../../plugin-data.js"
|
||||||
|
|
||||||
|
interface SetupWizardProps {
|
||||||
|
onComplete: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type Choice = "default" | "empty"
|
||||||
|
|
||||||
|
export function SetupWizard({ onComplete }: SetupWizardProps) {
|
||||||
|
const hasDefault = readDefaultPluginsJson() !== null
|
||||||
|
const choices: { key: Choice; label: string; description: string }[] = [
|
||||||
|
...(hasDefault
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: "default" as Choice,
|
||||||
|
label: "Use default configuration",
|
||||||
|
description: "Copy quartz.plugins.default.json as your starting config",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
{
|
||||||
|
key: "empty",
|
||||||
|
label: "Start with empty configuration",
|
||||||
|
description: "Create a minimal config with no plugins",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const selectOptions: SelectOption[] = choices.map((choice) => ({
|
||||||
|
name: choice.label,
|
||||||
|
description: choice.description,
|
||||||
|
value: choice.key,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box flexDirection="column" paddingX={2} paddingY={1}>
|
||||||
|
<box flexDirection="column" marginBottom={1}>
|
||||||
|
<text>
|
||||||
|
<span fg="yellow">
|
||||||
|
<strong>No configuration found</strong>
|
||||||
|
</span>
|
||||||
|
</text>
|
||||||
|
<text>
|
||||||
|
<span fg="#888888">
|
||||||
|
quartz.plugins.json does not exist yet. How would you like to set up your configuration?
|
||||||
|
</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
|
||||||
|
<select
|
||||||
|
options={selectOptions}
|
||||||
|
focused
|
||||||
|
onSelect={(_index: number, option: SelectOption | null) => {
|
||||||
|
if (!option) return
|
||||||
|
const choice = option.value as Choice
|
||||||
|
if (choice === "default") {
|
||||||
|
createConfigFromDefault()
|
||||||
|
} else {
|
||||||
|
writePluginsJson({
|
||||||
|
$schema: "./quartz/plugins/quartz-plugins.schema.json",
|
||||||
|
configuration: {
|
||||||
|
pageTitle: "Quartz",
|
||||||
|
enableSPA: true,
|
||||||
|
enablePopovers: true,
|
||||||
|
analytics: { provider: "plausible" },
|
||||||
|
locale: "en-US",
|
||||||
|
baseUrl: "quartz.jzhao.xyz",
|
||||||
|
ignorePatterns: ["private", "templates", ".obsidian"],
|
||||||
|
defaultDateType: "created",
|
||||||
|
theme: {
|
||||||
|
cdnCaching: true,
|
||||||
|
typography: {
|
||||||
|
header: "Schibsted Grotesk",
|
||||||
|
body: "Source Sans Pro",
|
||||||
|
code: "IBM Plex Mono",
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
lightMode: {
|
||||||
|
light: "#faf8f8",
|
||||||
|
lightgray: "#e5e5e5",
|
||||||
|
gray: "#b8b8b8",
|
||||||
|
darkgray: "#4e4e4e",
|
||||||
|
dark: "#2b2b2b",
|
||||||
|
secondary: "#284b63",
|
||||||
|
tertiary: "#84a59d",
|
||||||
|
highlight: "rgba(143, 159, 169, 0.15)",
|
||||||
|
textHighlight: "#fff23688",
|
||||||
|
},
|
||||||
|
darkMode: {
|
||||||
|
light: "#161618",
|
||||||
|
lightgray: "#393639",
|
||||||
|
gray: "#646464",
|
||||||
|
darkgray: "#d4d4d4",
|
||||||
|
dark: "#ebebec",
|
||||||
|
secondary: "#7b97aa",
|
||||||
|
tertiary: "#84a59d",
|
||||||
|
highlight: "rgba(143, 159, 169, 0.15)",
|
||||||
|
textHighlight: "#fff23688",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
layout: { groups: {}, byPageType: {} },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onComplete()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
46
quartz/cli/tui/components/StatusBar.tsx
Normal file
46
quartz/cli/tui/components/StatusBar.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
const KEYBINDS: Record<string, string[]> = {
|
||||||
|
Plugins: [
|
||||||
|
"↑↓ navigate",
|
||||||
|
"e enable",
|
||||||
|
"d disable",
|
||||||
|
"a add",
|
||||||
|
"r remove",
|
||||||
|
"i install",
|
||||||
|
"u update",
|
||||||
|
"o options",
|
||||||
|
"s sort",
|
||||||
|
"Tab switch tab",
|
||||||
|
"q quit",
|
||||||
|
],
|
||||||
|
Layout: [
|
||||||
|
"↑↓ navigate",
|
||||||
|
"←→ move zone",
|
||||||
|
"K/J reorder",
|
||||||
|
"m move",
|
||||||
|
"p priority",
|
||||||
|
"v display",
|
||||||
|
"c condition",
|
||||||
|
"x remove",
|
||||||
|
"g groups",
|
||||||
|
"t page-types",
|
||||||
|
"Tab switch tab",
|
||||||
|
"q quit",
|
||||||
|
],
|
||||||
|
Settings: ["↑↓ navigate", "Enter edit", "Tab switch tab", "q quit"],
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusBarProps {
|
||||||
|
activeTab: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatusBar({ activeTab }: StatusBarProps) {
|
||||||
|
const hints = KEYBINDS[activeTab] ?? []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box border borderStyle="single" paddingX={1}>
|
||||||
|
<text>
|
||||||
|
<span fg="#888888">{hints.join(" │ ")}</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
32
quartz/cli/tui/hooks/useLayout.ts
Normal file
32
quartz/cli/tui/hooks/useLayout.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { useState, useCallback } from "react"
|
||||||
|
import { getLayoutConfig, updateLayoutConfig } from "../../plugin-data.js"
|
||||||
|
|
||||||
|
export interface LayoutZone {
|
||||||
|
position: string
|
||||||
|
components: Array<{
|
||||||
|
pluginName: string
|
||||||
|
displayName: string
|
||||||
|
priority: number
|
||||||
|
display: string
|
||||||
|
condition?: string
|
||||||
|
group?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useLayout() {
|
||||||
|
const [layout, setLayout] = useState<Record<string, unknown> | null>(() => getLayoutConfig())
|
||||||
|
|
||||||
|
const refresh = useCallback(() => {
|
||||||
|
setLayout(getLayoutConfig())
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const save = useCallback(
|
||||||
|
(newLayout: Record<string, unknown>) => {
|
||||||
|
updateLayoutConfig(newLayout)
|
||||||
|
refresh()
|
||||||
|
},
|
||||||
|
[refresh],
|
||||||
|
)
|
||||||
|
|
||||||
|
return { layout, refresh, save }
|
||||||
|
}
|
||||||
133
quartz/cli/tui/hooks/usePlugins.ts
Normal file
133
quartz/cli/tui/hooks/usePlugins.ts
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { useState, useCallback } from "react"
|
||||||
|
import {
|
||||||
|
getEnrichedPlugins,
|
||||||
|
updatePluginEntry,
|
||||||
|
addPluginEntry,
|
||||||
|
removePluginEntry,
|
||||||
|
reorderPlugin,
|
||||||
|
} from "../../plugin-data.js"
|
||||||
|
|
||||||
|
export interface EnrichedPlugin {
|
||||||
|
index: number
|
||||||
|
name: string
|
||||||
|
displayName: string
|
||||||
|
source: string
|
||||||
|
enabled: boolean
|
||||||
|
options: Record<string, unknown>
|
||||||
|
order: number
|
||||||
|
layout: {
|
||||||
|
position: string
|
||||||
|
priority: number
|
||||||
|
display: string
|
||||||
|
condition?: string
|
||||||
|
group?: string
|
||||||
|
groupOptions?: Record<string, unknown>
|
||||||
|
} | null
|
||||||
|
category: string | string[]
|
||||||
|
installed: boolean
|
||||||
|
locked: {
|
||||||
|
source: string
|
||||||
|
resolved: string
|
||||||
|
commit: string
|
||||||
|
installedAt: string
|
||||||
|
} | null
|
||||||
|
manifest: Record<string, unknown> | null
|
||||||
|
currentCommit: string | null
|
||||||
|
modified: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePlugins() {
|
||||||
|
const [plugins, setPlugins] = useState<EnrichedPlugin[]>(() => getEnrichedPlugins())
|
||||||
|
|
||||||
|
const refresh = useCallback(() => {
|
||||||
|
setPlugins(getEnrichedPlugins())
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const toggleEnabled = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
const plugin = plugins[index]
|
||||||
|
if (!plugin) return
|
||||||
|
updatePluginEntry(plugin.index, { enabled: !plugin.enabled })
|
||||||
|
refresh()
|
||||||
|
},
|
||||||
|
[plugins, refresh],
|
||||||
|
)
|
||||||
|
|
||||||
|
const setPluginOrder = useCallback(
|
||||||
|
(index: number, order: number) => {
|
||||||
|
const plugin = plugins[index]
|
||||||
|
if (!plugin) return
|
||||||
|
updatePluginEntry(plugin.index, { order })
|
||||||
|
refresh()
|
||||||
|
},
|
||||||
|
[plugins, refresh],
|
||||||
|
)
|
||||||
|
|
||||||
|
const setPluginOptions = useCallback(
|
||||||
|
(index: number, key: string, value: unknown) => {
|
||||||
|
const plugin = plugins[index]
|
||||||
|
if (!plugin) return
|
||||||
|
const newOptions = { ...plugin.options, [key]: value }
|
||||||
|
updatePluginEntry(plugin.index, { options: newOptions })
|
||||||
|
refresh()
|
||||||
|
},
|
||||||
|
[plugins, refresh],
|
||||||
|
)
|
||||||
|
|
||||||
|
const removePlugin = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
const plugin = plugins[index]
|
||||||
|
if (!plugin) return false
|
||||||
|
const result = removePluginEntry(plugin.index)
|
||||||
|
if (result) refresh()
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
[plugins, refresh],
|
||||||
|
)
|
||||||
|
|
||||||
|
const addPlugin = useCallback(
|
||||||
|
(source: string) => {
|
||||||
|
const entry = {
|
||||||
|
source,
|
||||||
|
enabled: true,
|
||||||
|
options: {},
|
||||||
|
order: 50,
|
||||||
|
}
|
||||||
|
const result = addPluginEntry(entry)
|
||||||
|
if (result) refresh()
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
[refresh],
|
||||||
|
)
|
||||||
|
|
||||||
|
const movePlugin = useCallback(
|
||||||
|
(fromIndex: number, toIndex: number) => {
|
||||||
|
const result = reorderPlugin(fromIndex, toIndex)
|
||||||
|
if (result) refresh()
|
||||||
|
return result
|
||||||
|
},
|
||||||
|
[refresh],
|
||||||
|
)
|
||||||
|
|
||||||
|
const updateLayout = useCallback(
|
||||||
|
(index: number, layout: EnrichedPlugin["layout"]) => {
|
||||||
|
const plugin = plugins[index]
|
||||||
|
if (!plugin) return
|
||||||
|
updatePluginEntry(plugin.index, { layout })
|
||||||
|
refresh()
|
||||||
|
},
|
||||||
|
[plugins, refresh],
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins,
|
||||||
|
refresh,
|
||||||
|
toggleEnabled,
|
||||||
|
setPluginOrder,
|
||||||
|
setPluginOptions,
|
||||||
|
removePlugin,
|
||||||
|
addPlugin,
|
||||||
|
movePlugin,
|
||||||
|
updateLayout,
|
||||||
|
}
|
||||||
|
}
|
||||||
20
quartz/cli/tui/hooks/useSettings.ts
Normal file
20
quartz/cli/tui/hooks/useSettings.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { useState, useCallback } from "react"
|
||||||
|
import { getGlobalConfig, updateGlobalConfig } from "../../plugin-data.js"
|
||||||
|
|
||||||
|
export function useSettings() {
|
||||||
|
const [config, setConfig] = useState<Record<string, unknown> | null>(() => getGlobalConfig())
|
||||||
|
|
||||||
|
const refresh = useCallback(() => {
|
||||||
|
setConfig(getGlobalConfig())
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const updateField = useCallback(
|
||||||
|
(key: string, value: unknown) => {
|
||||||
|
updateGlobalConfig({ [key]: value })
|
||||||
|
refresh()
|
||||||
|
},
|
||||||
|
[refresh],
|
||||||
|
)
|
||||||
|
|
||||||
|
return { config, refresh, updateField }
|
||||||
|
}
|
||||||
1514
quartz/cli/tui/panels/LayoutPanel.tsx
Normal file
1514
quartz/cli/tui/panels/LayoutPanel.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1578
quartz/cli/tui/panels/PluginsPanel.tsx
Normal file
1578
quartz/cli/tui/panels/PluginsPanel.tsx
Normal file
File diff suppressed because it is too large
Load Diff
710
quartz/cli/tui/panels/SettingsPanel.tsx
Normal file
710
quartz/cli/tui/panels/SettingsPanel.tsx
Normal file
@ -0,0 +1,710 @@
|
|||||||
|
import { useState, useMemo, useCallback, useEffect } from "react"
|
||||||
|
import { useKeyboard } from "@opentui/react"
|
||||||
|
import { useSettings } from "../hooks/useSettings.js"
|
||||||
|
|
||||||
|
type View = "list" | "edit-string" | "edit-boolean" | "edit-enum" | "edit-array" | "edit-color"
|
||||||
|
|
||||||
|
interface SettingsPanelProps {
|
||||||
|
notify: (message: string, type?: "success" | "error" | "info") => void
|
||||||
|
onFocusChange: (focused: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FlatEntry {
|
||||||
|
keyPath: string[]
|
||||||
|
displayKey: string
|
||||||
|
value: unknown
|
||||||
|
depth: number
|
||||||
|
isObject: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FieldSchema {
|
||||||
|
type: "boolean" | "string" | "enum" | "array" | "number" | "object" | "color"
|
||||||
|
enumValues?: string[]
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFieldSchema(keyPath: string[]): FieldSchema {
|
||||||
|
const path = keyPath.join(".")
|
||||||
|
|
||||||
|
if (["enableSPA", "enablePopovers", "theme.cdnCaching"].includes(path)) {
|
||||||
|
return { type: "boolean" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === "theme.fontOrigin") {
|
||||||
|
return { type: "enum", enumValues: ["googleFonts", "local"] }
|
||||||
|
}
|
||||||
|
if (path === "defaultDateType") {
|
||||||
|
return { type: "enum", enumValues: ["created", "modified", "published"] }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path === "ignorePatterns") {
|
||||||
|
return { type: "array" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.match(/^theme\.colors\.(lightMode|darkMode)\./)) {
|
||||||
|
return { type: "color" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: "string" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const HEX_COLOR_REGEX = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/
|
||||||
|
const CSS_COLOR_FUNCTION_REGEX = /^(rgba?|hsla?|hwb|lab|lch|color)\(.+\)$/i
|
||||||
|
|
||||||
|
function isValidColorValue(value: string): boolean {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) return false
|
||||||
|
if (HEX_COLOR_REGEX.test(trimmed)) return true
|
||||||
|
return CSS_COLOR_FUNCTION_REGEX.test(trimmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenConfig(
|
||||||
|
obj: Record<string, unknown>,
|
||||||
|
prefix: string[] = [],
|
||||||
|
depth = 0,
|
||||||
|
): FlatEntry[] {
|
||||||
|
const entries: FlatEntry[] = []
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
const keyPath = [...prefix, key]
|
||||||
|
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
||||||
|
entries.push({
|
||||||
|
keyPath,
|
||||||
|
displayKey: key,
|
||||||
|
value,
|
||||||
|
depth,
|
||||||
|
isObject: true,
|
||||||
|
})
|
||||||
|
entries.push(...flattenConfig(value as Record<string, unknown>, keyPath, depth + 1))
|
||||||
|
} else {
|
||||||
|
entries.push({
|
||||||
|
keyPath,
|
||||||
|
displayKey: key,
|
||||||
|
value,
|
||||||
|
depth,
|
||||||
|
isObject: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonOrString(value: string): unknown {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value)
|
||||||
|
} catch {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setNestedValue(obj: Record<string, unknown>, keyPath: string[], value: unknown): void {
|
||||||
|
let current = obj
|
||||||
|
for (let i = 0; i < keyPath.length - 1; i++) {
|
||||||
|
if (!(keyPath[i] in current) || typeof current[keyPath[i]] !== "object") {
|
||||||
|
current[keyPath[i]] = {}
|
||||||
|
}
|
||||||
|
current = current[keyPath[i]] as Record<string, unknown>
|
||||||
|
}
|
||||||
|
current[keyPath[keyPath.length - 1]] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatStringValue(value: unknown): string {
|
||||||
|
if (typeof value === "string") return JSON.stringify(value)
|
||||||
|
if (value === null) return "null"
|
||||||
|
if (value === undefined) return "undefined"
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SettingsPanel({ notify, onFocusChange }: SettingsPanelProps) {
|
||||||
|
const { config, updateField } = useSettings()
|
||||||
|
const [view, setView] = useState<View>("list")
|
||||||
|
const [editingEntry, setEditingEntry] = useState<FlatEntry | null>(null)
|
||||||
|
const [highlightedIndex, setHighlightedIndex] = useState(0)
|
||||||
|
const [highlightedBoolIndex, setHighlightedBoolIndex] = useState(0)
|
||||||
|
const [highlightedEnumIndex, setHighlightedEnumIndex] = useState(0)
|
||||||
|
const [highlightedArrayIndex, setHighlightedArrayIndex] = useState(0)
|
||||||
|
const [arrayItems, setArrayItems] = useState<string[]>([])
|
||||||
|
const [addingArrayItem, setAddingArrayItem] = useState(false)
|
||||||
|
const [editingArrayItemIndex, setEditingArrayItemIndex] = useState<number | null>(null)
|
||||||
|
const [colorError, setColorError] = useState<string | null>(null)
|
||||||
|
const [collapsed, setCollapsed] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
|
const allEntries = useMemo(() => {
|
||||||
|
if (!config) return []
|
||||||
|
return flattenConfig(config)
|
||||||
|
}, [config])
|
||||||
|
|
||||||
|
const visibleEntries = useMemo(() => {
|
||||||
|
return allEntries.filter((entry) => {
|
||||||
|
for (let i = entry.keyPath.length - 1; i > 0; i--) {
|
||||||
|
const parentPath = entry.keyPath.slice(0, i).join(".")
|
||||||
|
if (collapsed.has(parentPath)) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}, [allEntries, collapsed])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (highlightedIndex >= visibleEntries.length) {
|
||||||
|
setHighlightedIndex(Math.max(0, visibleEntries.length - 1))
|
||||||
|
}
|
||||||
|
}, [highlightedIndex, visibleEntries.length])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editingEntry) return
|
||||||
|
const schema = getFieldSchema(editingEntry.keyPath)
|
||||||
|
if (schema.type === "boolean") {
|
||||||
|
setHighlightedBoolIndex(Boolean(editingEntry.value) ? 0 : 1)
|
||||||
|
} else if (schema.type === "enum" && schema.enumValues) {
|
||||||
|
const idx = schema.enumValues.indexOf(String(editingEntry.value))
|
||||||
|
setHighlightedEnumIndex(idx >= 0 ? idx : 0)
|
||||||
|
} else if (schema.type === "array") {
|
||||||
|
setHighlightedArrayIndex(0)
|
||||||
|
}
|
||||||
|
}, [editingEntry])
|
||||||
|
|
||||||
|
const exitEdit = useCallback(() => {
|
||||||
|
setView("list")
|
||||||
|
setEditingEntry(null)
|
||||||
|
setAddingArrayItem(false)
|
||||||
|
setEditingArrayItemIndex(null)
|
||||||
|
setColorError(null)
|
||||||
|
onFocusChange(false)
|
||||||
|
}, [onFocusChange])
|
||||||
|
|
||||||
|
const applyValue = useCallback(
|
||||||
|
(keyPath: string[], value: unknown, exitAfterSave: boolean = true) => {
|
||||||
|
if (!config) return
|
||||||
|
if (keyPath.length === 1) {
|
||||||
|
updateField(keyPath[0], value)
|
||||||
|
} else {
|
||||||
|
const fullConfig = { ...config } as Record<string, unknown>
|
||||||
|
setNestedValue(fullConfig, keyPath, value)
|
||||||
|
updateField(keyPath[0], fullConfig[keyPath[0]])
|
||||||
|
}
|
||||||
|
|
||||||
|
notify(`Set ${keyPath.join(".")}`, "success")
|
||||||
|
if (exitAfterSave) {
|
||||||
|
exitEdit()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[config, updateField, notify, exitEdit],
|
||||||
|
)
|
||||||
|
|
||||||
|
const enterEdit = useCallback(
|
||||||
|
(entry: FlatEntry) => {
|
||||||
|
const schema = getFieldSchema(entry.keyPath)
|
||||||
|
setEditingEntry(entry)
|
||||||
|
setAddingArrayItem(false)
|
||||||
|
setEditingArrayItemIndex(null)
|
||||||
|
if (schema.type === "boolean") {
|
||||||
|
setHighlightedBoolIndex(Boolean(entry.value) ? 0 : 1)
|
||||||
|
setView("edit-boolean")
|
||||||
|
} else if (schema.type === "enum") {
|
||||||
|
if (schema.enumValues) {
|
||||||
|
const idx = schema.enumValues.indexOf(String(entry.value))
|
||||||
|
setHighlightedEnumIndex(idx >= 0 ? idx : 0)
|
||||||
|
}
|
||||||
|
setView("edit-enum")
|
||||||
|
} else if (schema.type === "array") {
|
||||||
|
setArrayItems(Array.isArray(entry.value) ? entry.value.map(String) : [])
|
||||||
|
setHighlightedArrayIndex(0)
|
||||||
|
setView("edit-array")
|
||||||
|
} else if (schema.type === "color") {
|
||||||
|
setColorError(null)
|
||||||
|
setView("edit-color")
|
||||||
|
} else {
|
||||||
|
setView("edit-string")
|
||||||
|
}
|
||||||
|
onFocusChange(true)
|
||||||
|
},
|
||||||
|
[onFocusChange],
|
||||||
|
)
|
||||||
|
|
||||||
|
useKeyboard((event) => {
|
||||||
|
if (view !== "list") return
|
||||||
|
|
||||||
|
const count = visibleEntries.length
|
||||||
|
if (count === 0) return
|
||||||
|
|
||||||
|
if (event.name === "up") {
|
||||||
|
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : count - 1))
|
||||||
|
}
|
||||||
|
if (event.name === "down") {
|
||||||
|
setHighlightedIndex((prev) => (prev < count - 1 ? prev + 1 : 0))
|
||||||
|
}
|
||||||
|
if (event.name === "return") {
|
||||||
|
const entry = visibleEntries[highlightedIndex]
|
||||||
|
if (!entry) return
|
||||||
|
if (entry.isObject) {
|
||||||
|
const path = entry.keyPath.join(".")
|
||||||
|
setCollapsed((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (next.has(path)) {
|
||||||
|
next.delete(path)
|
||||||
|
} else {
|
||||||
|
next.add(path)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
enterEdit(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useKeyboard((event) => {
|
||||||
|
if (view !== "edit-boolean" || !editingEntry) return
|
||||||
|
if (event.name === "escape") {
|
||||||
|
exitEdit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.name === "up" || event.name === "down") {
|
||||||
|
setHighlightedBoolIndex((prev) => (prev === 0 ? 1 : 0))
|
||||||
|
}
|
||||||
|
if (event.name === "return") {
|
||||||
|
const newVal = highlightedBoolIndex === 0
|
||||||
|
applyValue(editingEntry.keyPath, newVal)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useKeyboard((event) => {
|
||||||
|
if (view !== "edit-enum" || !editingEntry) return
|
||||||
|
const schema = getFieldSchema(editingEntry.keyPath)
|
||||||
|
const enumValues = schema.type === "enum" ? (schema.enumValues ?? []) : []
|
||||||
|
if (event.name === "escape") {
|
||||||
|
exitEdit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.name === "up" || event.name === "down") {
|
||||||
|
const len = enumValues.length
|
||||||
|
if (len === 0) return
|
||||||
|
setHighlightedEnumIndex((prev) =>
|
||||||
|
event.name === "up" ? (prev > 0 ? prev - 1 : len - 1) : prev < len - 1 ? prev + 1 : 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (event.name === "return") {
|
||||||
|
const selected = enumValues[highlightedEnumIndex]
|
||||||
|
if (selected !== undefined) {
|
||||||
|
applyValue(editingEntry.keyPath, selected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useKeyboard((event) => {
|
||||||
|
if (view !== "edit-array" || !editingEntry) return
|
||||||
|
|
||||||
|
if (addingArrayItem || editingArrayItemIndex !== null) {
|
||||||
|
if (event.name === "escape") {
|
||||||
|
setAddingArrayItem(false)
|
||||||
|
setEditingArrayItemIndex(null)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.name === "escape") {
|
||||||
|
exitEdit()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const count = arrayItems.length
|
||||||
|
if (event.name === "up" && count > 0 && !event.shift) {
|
||||||
|
setHighlightedArrayIndex((prev) => (prev > 0 ? prev - 1 : count - 1))
|
||||||
|
}
|
||||||
|
if (event.name === "down" && count > 0 && !event.shift) {
|
||||||
|
setHighlightedArrayIndex((prev) => (prev < count - 1 ? prev + 1 : 0))
|
||||||
|
}
|
||||||
|
if (event.name === "n") {
|
||||||
|
setAddingArrayItem(true)
|
||||||
|
}
|
||||||
|
if (event.name === "return" && count > 0) {
|
||||||
|
setEditingArrayItemIndex(highlightedArrayIndex)
|
||||||
|
}
|
||||||
|
if (event.name === "x" && count > 0) {
|
||||||
|
const nextItems = arrayItems.filter((_, i) => i !== highlightedArrayIndex)
|
||||||
|
setArrayItems(nextItems)
|
||||||
|
setHighlightedArrayIndex(Math.max(0, Math.min(highlightedArrayIndex, nextItems.length - 1)))
|
||||||
|
applyValue(editingEntry.keyPath, nextItems, false)
|
||||||
|
}
|
||||||
|
if (event.name === "up" && event.shift && highlightedArrayIndex > 0) {
|
||||||
|
const nextItems = [...arrayItems]
|
||||||
|
const idx = highlightedArrayIndex
|
||||||
|
;[nextItems[idx - 1], nextItems[idx]] = [nextItems[idx], nextItems[idx - 1]]
|
||||||
|
setArrayItems(nextItems)
|
||||||
|
setHighlightedArrayIndex(idx - 1)
|
||||||
|
applyValue(editingEntry.keyPath, nextItems, false)
|
||||||
|
}
|
||||||
|
if (event.name === "down" && event.shift && highlightedArrayIndex < count - 1) {
|
||||||
|
const nextItems = [...arrayItems]
|
||||||
|
const idx = highlightedArrayIndex
|
||||||
|
;[nextItems[idx], nextItems[idx + 1]] = [nextItems[idx + 1], nextItems[idx]]
|
||||||
|
setArrayItems(nextItems)
|
||||||
|
setHighlightedArrayIndex(idx + 1)
|
||||||
|
applyValue(editingEntry.keyPath, nextItems, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
useKeyboard((event) => {
|
||||||
|
if (view !== "edit-string" && view !== "edit-color") return
|
||||||
|
if (event.name === "escape") exitEdit()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return (
|
||||||
|
<box padding={1}>
|
||||||
|
<text>
|
||||||
|
<span fg="#888888">No configuration found. Run `quartz create` first.</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderTree = (dimmed: boolean) => {
|
||||||
|
const baseFg = dimmed ? "#666666" : "#888888"
|
||||||
|
const highlightFg = dimmed ? "#AAAAAA" : "#FFFFFF"
|
||||||
|
|
||||||
|
const renderedEntries = visibleEntries.map((entry, idx) => {
|
||||||
|
const indent = " ".repeat(entry.depth)
|
||||||
|
const isHighlighted = idx === highlightedIndex
|
||||||
|
const marker = isHighlighted ? "▸ " : " "
|
||||||
|
|
||||||
|
if (entry.isObject) {
|
||||||
|
const isCollapsed = collapsed.has(entry.keyPath.join("."))
|
||||||
|
const arrow = isCollapsed ? "▸" : "▾"
|
||||||
|
return (
|
||||||
|
<text key={entry.keyPath.join(".")}>
|
||||||
|
{isHighlighted ? (
|
||||||
|
<span fg={highlightFg}>
|
||||||
|
<strong>
|
||||||
|
{marker}
|
||||||
|
{indent}
|
||||||
|
{entry.displayKey}: {arrow}
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span fg={baseFg}>
|
||||||
|
{marker}
|
||||||
|
{indent}
|
||||||
|
{entry.displayKey}: {arrow}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = getFieldSchema(entry.keyPath)
|
||||||
|
let valueText = ""
|
||||||
|
let valueFg: string | null = null
|
||||||
|
let swatchColor: string | null = null
|
||||||
|
|
||||||
|
if (schema.type === "boolean") {
|
||||||
|
const enabled = Boolean(entry.value)
|
||||||
|
valueText = enabled ? "● true" : "○ false"
|
||||||
|
valueFg = enabled ? "green" : "red"
|
||||||
|
} else if (schema.type === "enum") {
|
||||||
|
valueText = String(entry.value ?? "")
|
||||||
|
} else if (schema.type === "array") {
|
||||||
|
const length = Array.isArray(entry.value) ? entry.value.length : 0
|
||||||
|
valueText = `[${length} items]`
|
||||||
|
} else if (schema.type === "color") {
|
||||||
|
const colorValue = String(entry.value ?? "")
|
||||||
|
valueText = colorValue
|
||||||
|
swatchColor = isValidColorValue(colorValue) ? colorValue : null
|
||||||
|
} else {
|
||||||
|
valueText = formatStringValue(entry.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayFg = isHighlighted ? (valueFg ?? highlightFg) : (valueFg ?? baseFg)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<text key={entry.keyPath.join(".")}>
|
||||||
|
{isHighlighted ? (
|
||||||
|
<span fg={highlightFg}>
|
||||||
|
<strong>
|
||||||
|
{marker}
|
||||||
|
{indent}
|
||||||
|
{entry.displayKey}:
|
||||||
|
</strong>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span fg={baseFg}>
|
||||||
|
{marker}
|
||||||
|
{indent}
|
||||||
|
{entry.displayKey}:
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{swatchColor ? <span fg={swatchColor}>█ </span> : null}
|
||||||
|
<span fg={displayFg}>{isHighlighted ? <strong>{valueText}</strong> : valueText}</span>
|
||||||
|
</text>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<scrollbox>
|
||||||
|
<box flexDirection="column">{renderedEntries}</box>
|
||||||
|
</scrollbox>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderEditPanel = () => {
|
||||||
|
if (!editingEntry) return null
|
||||||
|
const schema = getFieldSchema(editingEntry.keyPath)
|
||||||
|
const pathLabel = editingEntry.keyPath.join(".")
|
||||||
|
|
||||||
|
if (view === "edit-boolean") {
|
||||||
|
const boolItems = [
|
||||||
|
{ label: "true", isCurrent: Boolean(editingEntry.value) === true },
|
||||||
|
{ label: "false", isCurrent: Boolean(editingEntry.value) === false },
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
<box flexDirection="column" paddingX={1}>
|
||||||
|
<text>
|
||||||
|
<strong>Edit: {pathLabel}</strong>
|
||||||
|
</text>
|
||||||
|
<box flexDirection="column" marginTop={1}>
|
||||||
|
{boolItems.map((item, i) => {
|
||||||
|
const isHighlighted = i === highlightedBoolIndex
|
||||||
|
const fg = isHighlighted ? "#FFFFFF" : "#888888"
|
||||||
|
const marker = isHighlighted ? "▸ " : " "
|
||||||
|
return (
|
||||||
|
<text key={item.label}>
|
||||||
|
<span fg={fg}>
|
||||||
|
{marker}
|
||||||
|
{item.label}
|
||||||
|
{item.isCurrent ? " (current)" : ""}
|
||||||
|
</span>
|
||||||
|
</text>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</box>
|
||||||
|
<text>
|
||||||
|
<span fg="#888888">↑↓: toggle │ Enter: select │ Esc: back</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view === "edit-enum" && schema.type === "enum") {
|
||||||
|
const enumValues = schema.enumValues ?? []
|
||||||
|
return (
|
||||||
|
<box flexDirection="column" paddingX={1}>
|
||||||
|
<text>
|
||||||
|
<strong>Edit: {pathLabel}</strong>
|
||||||
|
</text>
|
||||||
|
<box flexDirection="column" marginTop={1}>
|
||||||
|
{enumValues.map((value, i) => {
|
||||||
|
const isHighlighted = i === highlightedEnumIndex
|
||||||
|
const fg = isHighlighted ? "#FFFFFF" : "#888888"
|
||||||
|
const marker = isHighlighted ? "▸ " : " "
|
||||||
|
const currentTag = value === String(editingEntry.value) ? " (current)" : ""
|
||||||
|
return (
|
||||||
|
<text key={value}>
|
||||||
|
<span fg={fg}>
|
||||||
|
{marker}
|
||||||
|
{value}
|
||||||
|
{currentTag}
|
||||||
|
</span>
|
||||||
|
</text>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</box>
|
||||||
|
<text>
|
||||||
|
<span fg="#888888">↑↓: navigate │ Enter: select │ Esc: back</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view === "edit-array") {
|
||||||
|
const hasItems = arrayItems.length > 0
|
||||||
|
const controlsLabel = "n: add │ x: delete │ Shift+↑/↓: reorder │ Esc: back"
|
||||||
|
const editingLabel = editingArrayItemIndex !== null ? "Edit item:" : "New item:"
|
||||||
|
const placeholder =
|
||||||
|
editingArrayItemIndex !== null ? (arrayItems[editingArrayItemIndex] ?? "") : "pattern"
|
||||||
|
const showInput = addingArrayItem || editingArrayItemIndex !== null
|
||||||
|
|
||||||
|
const arrayLines = hasItems
|
||||||
|
? arrayItems.map((value, index) => {
|
||||||
|
const isHighlighted = index === highlightedArrayIndex
|
||||||
|
const fg = isHighlighted ? "#FFFFFF" : "#888888"
|
||||||
|
const marker = isHighlighted ? "▸ " : " "
|
||||||
|
return (
|
||||||
|
<text key={`${index}-${value}`}>
|
||||||
|
<span fg={fg}>
|
||||||
|
{marker}
|
||||||
|
{JSON.stringify(value)}
|
||||||
|
</span>
|
||||||
|
</text>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
: [
|
||||||
|
<text key="__empty__">
|
||||||
|
<span fg="#888888">(no items)</span>
|
||||||
|
</text>,
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box flexDirection="column" paddingX={1}>
|
||||||
|
<text>
|
||||||
|
<strong>Edit: {pathLabel}</strong>
|
||||||
|
</text>
|
||||||
|
<scrollbox>
|
||||||
|
<box flexDirection="column">{arrayLines}</box>
|
||||||
|
</scrollbox>
|
||||||
|
<box marginTop={1}>
|
||||||
|
<text>{editingLabel} </text>
|
||||||
|
<box border borderStyle="single" height={3} flexGrow={1}>
|
||||||
|
<input
|
||||||
|
placeholder={placeholder}
|
||||||
|
focused={showInput}
|
||||||
|
onSubmit={(value: string) => {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!trimmed) {
|
||||||
|
setAddingArrayItem(false)
|
||||||
|
setEditingArrayItemIndex(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (editingArrayItemIndex !== null) {
|
||||||
|
const nextItems = [...arrayItems]
|
||||||
|
nextItems[editingArrayItemIndex] = trimmed
|
||||||
|
setArrayItems(nextItems)
|
||||||
|
applyValue(editingEntry.keyPath, nextItems, false)
|
||||||
|
setEditingArrayItemIndex(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const nextItems = [...arrayItems, trimmed]
|
||||||
|
setArrayItems(nextItems)
|
||||||
|
setHighlightedArrayIndex(Math.max(0, nextItems.length - 1))
|
||||||
|
applyValue(editingEntry.keyPath, nextItems, false)
|
||||||
|
setAddingArrayItem(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
<text>
|
||||||
|
<span fg="#888888">{controlsLabel}</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view === "edit-color") {
|
||||||
|
const currentValue = String(editingEntry.value ?? "")
|
||||||
|
const showSwatch = isValidColorValue(currentValue)
|
||||||
|
const errorText = colorError ?? ""
|
||||||
|
return (
|
||||||
|
<box flexDirection="column" paddingX={1}>
|
||||||
|
<text>
|
||||||
|
<strong>Edit: {pathLabel}</strong>
|
||||||
|
</text>
|
||||||
|
<text>
|
||||||
|
<span fg="#888888">Current: {currentValue}</span>
|
||||||
|
{showSwatch ? <span fg={currentValue}> █</span> : null}
|
||||||
|
</text>
|
||||||
|
<box marginTop={1}>
|
||||||
|
<text>Value: </text>
|
||||||
|
<box border borderStyle="single" height={3} flexGrow={1}>
|
||||||
|
<input
|
||||||
|
placeholder={currentValue}
|
||||||
|
focused
|
||||||
|
onSubmit={(value: string) => {
|
||||||
|
const trimmed = value.trim()
|
||||||
|
if (!isValidColorValue(trimmed)) {
|
||||||
|
setColorError(
|
||||||
|
"Invalid color. Use #RGB, #RRGGBB, #RRGGBBAA, or a CSS color function like rgba(...)",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setColorError(null)
|
||||||
|
applyValue(editingEntry.keyPath, trimmed)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
<text>
|
||||||
|
<span fg={errorText ? "red" : "#888888"}>{errorText}</span>
|
||||||
|
</text>
|
||||||
|
<text>
|
||||||
|
<span fg="#888888">Enter: save │ Esc: cancel</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view === "edit-string") {
|
||||||
|
const currentLabel = formatStringValue(editingEntry.value)
|
||||||
|
return (
|
||||||
|
<box flexDirection="column" paddingX={1}>
|
||||||
|
<text>
|
||||||
|
<strong>Edit: {pathLabel}</strong>
|
||||||
|
</text>
|
||||||
|
<text>
|
||||||
|
<span fg="#888888">Current: {currentLabel}</span>
|
||||||
|
</text>
|
||||||
|
<box marginTop={1} flexDirection="row">
|
||||||
|
<text>Value: </text>
|
||||||
|
<box border borderStyle="single" height={3} flexGrow={1}>
|
||||||
|
<input
|
||||||
|
placeholder={currentLabel}
|
||||||
|
focused
|
||||||
|
onSubmit={(value: string) => {
|
||||||
|
const parsed = parseJsonOrString(value)
|
||||||
|
applyValue(editingEntry.keyPath, parsed)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
<text>
|
||||||
|
<span fg="#888888">Enter: save │ Esc: cancel</span>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (view !== "list" && editingEntry) {
|
||||||
|
return (
|
||||||
|
<box flexDirection="row" paddingX={1} gap={1} flexGrow={1}>
|
||||||
|
<box flexDirection="column" flexGrow={1}>
|
||||||
|
<text>
|
||||||
|
<strong>Global Configuration</strong>
|
||||||
|
</text>
|
||||||
|
<box
|
||||||
|
flexDirection="column"
|
||||||
|
border
|
||||||
|
borderStyle="single"
|
||||||
|
paddingX={1}
|
||||||
|
marginTop={1}
|
||||||
|
flexGrow={1}
|
||||||
|
>
|
||||||
|
{renderTree(true)}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
<box flexDirection="column" border borderStyle="single" paddingX={1} flexGrow={1}>
|
||||||
|
{renderEditPanel()}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<box flexDirection="column" paddingX={1} flexGrow={1}>
|
||||||
|
<text>
|
||||||
|
<strong>Global Configuration</strong>
|
||||||
|
</text>
|
||||||
|
<box
|
||||||
|
flexDirection="column"
|
||||||
|
border
|
||||||
|
borderStyle="single"
|
||||||
|
paddingX={1}
|
||||||
|
marginTop={1}
|
||||||
|
flexGrow={1}
|
||||||
|
>
|
||||||
|
{renderTree(false)}
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
19
quartz/cli/tui/tsconfig.json
Normal file
19
quartz/cli/tui/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "@opentui/react",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "..",
|
||||||
|
"declaration": false,
|
||||||
|
"types": ["node", "react"]
|
||||||
|
},
|
||||||
|
"include": ["*.tsx", "**/*.tsx", "**/*.ts", "cli-modules.d.ts"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user