diff --git a/package.json b/package.json index 72d37ce96..ea34c0b5f 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,6 @@ "check": "tsc --noEmit && npx prettier . --check", "format": "npx prettier . --write", "test": "tsx --test", - "build:tui": "node quartz/cli/build-tui.mjs", "profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1", "install-plugins": "npx tsx ./quartz/plugins/loader/install-plugins.ts", "prebuild": "npm run install-plugins" @@ -42,8 +41,6 @@ "@floating-ui/dom": "^1.7.4", "@myriaddreamin/rehype-typst": "^0.6.0", "@napi-rs/simple-git": "0.1.22", - "@opentui/core": "^0.1.80", - "@opentui/react": "^0.1.80", "ansi-truncate": "^1.4.0", "async-mutex": "^0.5.0", "chokidar": "^5.0.0", @@ -68,7 +65,6 @@ "preact-render-to-string": "^6.6.5", "pretty-bytes": "^7.1.0", "pretty-time": "^1.1.0", - "react": "^19.2.4", "reading-time": "^1.5.0", "rehype-autolink-headings": "^7.1.0", "rehype-citation": "^2.3.1", @@ -106,7 +102,6 @@ "@types/js-yaml": "^4.0.9", "@types/node": "^25.0.10", "@types/pretty-time": "^1.1.5", - "@types/react": "^19.2.14", "@types/source-map-support": "^0.5.10", "@types/ws": "^8.18.1", "@types/yargs": "^17.0.35", diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs index d45fe3910..6c5e50a07 100755 --- a/quartz/bootstrap-cli.mjs +++ b/quartz/bootstrap-cli.mjs @@ -33,17 +33,17 @@ import { 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") + const tuiPath = join(process.cwd(), ".quartz", "plugins", "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.") + console.error( + "TUI plugin not installed. Install with:\n" + + " npx quartz plugin add github:quartz-community/tui\n" + ) + process.exit(1) } // OpenTUI requires Bun runtime (uses bun:ffi for Zig renderer) diff --git a/quartz/cli/build-tui.mjs b/quartz/cli/build-tui.mjs deleted file mode 100644 index 3811f5dcd..000000000 --- a/quartz/cli/build-tui.mjs +++ /dev/null @@ -1,21 +0,0 @@ -#!/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" }, -}) diff --git a/quartz/cli/tui/App.tsx b/quartz/cli/tui/App.tsx deleted file mode 100644 index c76422258..000000000 --- a/quartz/cli/tui/App.tsx +++ /dev/null @@ -1,114 +0,0 @@ -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("Plugins") - const [notification, setNotification] = useState(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 ( - - - - - {` Quartz v${version} Plugin Manager `} - - - - - - { - setHasConfig(true) - notify("Configuration created", "success") - }} - /> - - - {notification && } - - ) - } - - return ( - - - - - {` Quartz v${version} Plugin Manager `} - - - - - - {TABS.map((tab) => ( - - {tab === activeTab ? ( - - [ {tab} ] - - ) : ( - {` ${tab} `} - )} - - ))} - - - - {activeTab === "Plugins" && ( - - )} - {activeTab === "Layout" && } - {activeTab === "Settings" && ( - - )} - - - {notification && } - - - - ) -} - -const renderer = await createCliRenderer({ exitOnCtrlC: true }) -createRoot(renderer).render() diff --git a/quartz/cli/tui/async-plugin-ops.ts b/quartz/cli/tui/async-plugin-ops.ts deleted file mode 100644 index 6a4a5c911..000000000 --- a/quartz/cli/tui/async-plugin-ops.ts +++ /dev/null @@ -1,572 +0,0 @@ -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 -} - -interface PluginsJsonEntry { - source: string - enabled?: boolean - options?: Record - order?: number - layout?: { - position: string - priority?: number - display?: string - condition?: string - group?: string - groupOptions?: Record - } -} - -interface PluginsJson { - plugins?: PluginsJsonEntry[] -} - -interface PluginManifestComponent { - defaultPosition?: string - defaultPriority?: number -} - -interface PluginManifest { - defaultEnabled?: boolean - defaultOptions?: Record - defaultOrder?: number - components?: Record -} - -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 { - 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 { - const lockfile = readLockfile() as Lockfile | null - - if (!lockfile) { - const message = "⚠ No quartz.lock.json found. Run 'npx quartz plugin add ' 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 { - 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) - } - - 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 { - 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) - } - - 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 { - 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 { - 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 ' 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 } -} diff --git a/quartz/cli/tui/cli-modules.d.ts b/quartz/cli/tui/cli-modules.d.ts deleted file mode 100644 index ca84add04..000000000 --- a/quartz/cli/tui/cli-modules.d.ts +++ /dev/null @@ -1,175 +0,0 @@ -declare module "../plugin-data.js" { - export function configExists(): boolean - export function createConfigFromDefault(): Record -} - -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 -} - -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 | null - export function writePluginsJson(data: Record): void - export function readDefaultPluginsJson(): Record | null - export function readLockfile(): Record | null - export function writeLockfile(lockfile: Record): void - export function extractPluginName(source: string): string - export function readManifestFromPackageJson(pluginDir: string): Record | 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 - order: number - layout: { - position: string - priority: number - display: string - condition?: string - group?: string - groupOptions?: Record - } | null - category: string | string[] - installed: boolean - locked: { - source: string - resolved: string - commit: string - installedAt: string - } | null - manifest: Record | null - currentCommit: string | null - modified: boolean - }> - export function getLayoutConfig(): Record | null - export function getGlobalConfig(): Record | null - export function updatePluginEntry(index: number, updates: Record): boolean - export function updateGlobalConfig(updates: Record): boolean - export function updateLayoutConfig(layout: Record): boolean - export function reorderPlugin(fromIndex: number, toIndex: number): boolean - export function removePluginEntry(index: number): boolean - export function addPluginEntry(entry: Record): boolean - export function configExists(): boolean - export function createConfigFromDefault(): Record - 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 - export function handlePluginAdd(sources: string[]): Promise - export function handlePluginRemove(names: string[]): Promise - export function handlePluginUpdate(names?: string[]): Promise - export function handlePluginRestore(): Promise - export function handlePluginList(): Promise - export function handlePluginEnable(names: string[]): Promise - export function handlePluginDisable(names: string[]): Promise - export function handlePluginConfig(name: string, options?: { set?: string }): Promise - export function handlePluginCheck(): Promise -} - -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 - export function tuiPluginInstall(onProgress?: ProgressCallback): Promise - export function tuiPluginRestore(onProgress?: ProgressCallback): Promise - export function tuiPluginAdd( - sources: string[], - onProgress?: ProgressCallback, - ): Promise - export function tuiPluginRemove( - names: string[], - onProgress?: ProgressCallback, - ): Promise -} - -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 - export function tuiPluginInstall(onProgress?: ProgressCallback): Promise - export function tuiPluginRestore(onProgress?: ProgressCallback): Promise - export function tuiPluginAdd( - sources: string[], - onProgress?: ProgressCallback, - ): Promise - export function tuiPluginRemove( - names: string[], - onProgress?: ProgressCallback, - ): Promise -} - -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 -} diff --git a/quartz/cli/tui/components/Notification.tsx b/quartz/cli/tui/components/Notification.tsx deleted file mode 100644 index 129a7adba..000000000 --- a/quartz/cli/tui/components/Notification.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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 ( - - - {message.message} - - - ) -} diff --git a/quartz/cli/tui/components/SetupWizard.tsx b/quartz/cli/tui/components/SetupWizard.tsx deleted file mode 100644 index d13a64fa1..000000000 --- a/quartz/cli/tui/components/SetupWizard.tsx +++ /dev/null @@ -1,116 +0,0 @@ -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.config.default.yaml 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 ( - - - - - No configuration found - - - - - quartz.config.yaml does not exist yet. How would you like to set up your configuration? - - - - - { - const num = parseInt(value, 10) - if (!isNaN(num)) { - const arrIdx = findPluginArrayIndex(selectedComp.pluginIndex) - if (arrIdx >= 0) { - const plugin = plugins[arrIdx] - if (plugin.layout) { - updateLayout(arrIdx, { ...plugin.layout, priority: num }) - notify(`Priority set to ${num}`, "success") - } - } - } - exitView() - }} - /> - - - - ) - } - - if (view === "edit-display" && selectedComp) { - return ( - { - const arrIdx = findPluginArrayIndex(selectedComp.pluginIndex) - if (arrIdx >= 0) { - const plugin = plugins[arrIdx] - if (plugin.layout) { - updateLayout(arrIdx, { ...plugin.layout, display: mode }) - notify(`Display set to ${mode}`, "success") - } - } - exitView() - }} - onCancel={exitView} - /> - ) - } - - if (view === "edit-condition" && selectedComp) { - return ( - - - - {currentZone} › {selectedComp.displayName} › Condition - - - - Set Condition: {selectedComp.displayName} - - - - Available: not-index, has-tags, has-backlinks, has-toc (or empty to remove) - - - - Condition: - - { - const arrIdx = findPluginArrayIndex(selectedComp.pluginIndex) - if (arrIdx >= 0) { - const plugin = plugins[arrIdx] - if (plugin.layout) { - const newLayout = { ...plugin.layout } - if (value.trim()) { - newLayout.condition = value.trim() - } else { - delete newLayout.condition - } - updateLayout(arrIdx, newLayout) - notify( - value.trim() ? `Condition set to ${value.trim()}` : "Condition removed", - "success", - ) - } - } - exitView() - }} - /> - - - - ) - } - - if (view === "confirm-remove-layout" && selectedComp) { - return ( - - - - {currentZone} › {selectedComp.displayName} - - - - - Remove {selectedComp.displayName} from layout? - - - - The plugin will remain installed but won't appear in the layout. - - - Confirm removal? - { - const arrIdx = findPluginArrayIndex(selectedComp.pluginIndex) - if (arrIdx >= 0) { - updateLayout(arrIdx, null) - setSelectedComponent(Math.max(0, selectedComponent - 1)) - notify(`Removed ${selectedComp.displayName} from layout`, "success") - } - exitView() - }} - onCancel={() => exitView()} - /> - - - ) - } - - if (view === "groups") { - return ( - { - saveLayout(newLayout) - refreshPlugins() - notify("Groups updated", "success") - exitView() - }} - onCancel={exitView} - /> - ) - } - - if (view === "page-types") { - return ( - { - saveLayout(newLayout) - }} - onCancel={exitView} - /> - ) - } - - const renderZoneBox = (zone: Zone) => { - const comps = zoneComponents[zone] - const isFocused = getActiveZone() === zone - const isDrilledIn = isFocused && drillMode - const maxItems = maxItemsForZone(zone) - const visibleComps = comps.slice(0, maxItems) - const overflowCount = comps.length - visibleComps.length - - return ( - <> - - {isFocused ? ( - - - {zone} ({comps.length}) - - - ) : ( - - {zone} ({comps.length}) - - )} - - {isDrilledIn ? ( - comps.length === 0 ? ( - - Empty zone - - ) : ( - { - const zone = option?.value as Zone | undefined - if (zone) onSelect(zone) - }} - showDescription={false} - /> - - Enter: select │ Esc: cancel - - - ) -} - -interface DisplayModeViewProps { - component: ZoneComponent - fromZone: Zone - onSelect: (mode: string) => void - onCancel: () => void -} - -const DISPLAY_MODES = ["all", "desktop-only", "mobile-only"] as const - -function DisplayModeView({ component, fromZone, onSelect, onCancel }: DisplayModeViewProps) { - useKeyboard((event) => { - if (event.name === "escape") onCancel() - }) - - const modeOptions: SelectOption[] = DISPLAY_MODES.map((mode) => ({ - name: mode, - description: mode === component.display ? "(current)" : "", - value: mode, - })) - - return ( - - - - {fromZone} › {component.displayName} › Display - - - - Display mode for {component.displayName}: - - { - if (value.trim()) { - const newLayout = { ...(layout ?? {}) } - const currentGroups = - ((newLayout as Record).groups as Record< - string, - Record - >) ?? {} - currentGroups[value.trim()] = { direction: "row", gap: "0.5rem" } - ;(newLayout as Record).groups = currentGroups - onSave(newLayout) - } else { - setMode("list") - } - }} - /> - - - - ) - } - - if (mode === "confirm-delete" && selectedGroup) { - return ( - - - - Delete group "{selectedGroup[0]}"? - - - - { - const newLayout = { ...(layout ?? {}) } - const currentGroups = { - ...(((newLayout as Record).groups as Record< - string, - Record - >) ?? {}), - } - delete currentGroups[selectedGroup[0]] - ;(newLayout as Record).groups = currentGroups - onSave(newLayout) - }} - onCancel={() => setMode("list")} - /> - - - ) - } - - if (mode === "edit" && selectedGroup && editField) { - const [groupName, groupConfig] = selectedGroup - const currentValue = String((groupConfig as Record)[editField] ?? "") - - return ( - - - - Edit {groupName} → {editField} - - - - - {editField === "direction" - ? 'Values: "row" or "column"' - : 'Value: CSS gap (e.g. "0.5rem")'} - - - - {editField}: - - { - const newLayout = { ...(layout ?? {}) } - const currentGroups = { - ...(((newLayout as Record).groups as Record< - string, - Record - >) ?? {}), - } - currentGroups[groupName] = { ...currentGroups[groupName], [editField]: value } - ;(newLayout as Record).groups = currentGroups - - if (editField === "direction") { - setEditField("gap") - } else { - onSave(newLayout) - } - }} - /> - - - - ) - } - - const groupOptions: SelectOption[] = groupEntries.map(([name, config]) => { - const cfg = config as Record - return { - name, - description: `direction=${String(cfg.direction ?? "row")} gap=${String(cfg.gap ?? "0")}`, - value: name, - } - }) - - return ( - - - Layout Groups - - {groupEntries.length === 0 ? ( - - No groups defined - - ) : ( - { - const name = value.trim() - if (!name) { - setMode("list") - return - } - - if (byPageType[name]) { - notify(`Page type "${name}" already exists`, "error") - setMode("list") - return - } - - const newLayout = { ...(layout ?? {}) } - const currentByPageType = { - ...(((newLayout as LayoutConfig).byPageType as Record< - string, - PageTypeOverride - >) ?? {}), - } - currentByPageType[name] = {} - ;(newLayout as LayoutConfig).byPageType = currentByPageType - onSave(newLayout) - setSelected(pageTypeEntries.length) - setMode("list") - }} - /> - - - - ) - } - - if (mode === "confirm-delete" && selectedPageType) { - return ( - - - - Delete page type "{selectedPageType}"? - - - - { - const newLayout = { ...(layout ?? {}) } - const currentByPageType = { - ...(((newLayout as LayoutConfig).byPageType as Record) ?? - {}), - } - delete currentByPageType[selectedPageType] - ;(newLayout as LayoutConfig).byPageType = currentByPageType - onSave(newLayout) - setSelected(Math.max(0, selected - 1)) - setMode("list") - }} - onCancel={() => setMode("list")} - /> - - - ) - } - - if (mode === "exclude" && selectedPageType) { - return ( - { - updatePageType(selectedPageType, (current) => ({ ...current, exclude: nextExclude })) - }} - onCancel={() => setMode("detail")} - /> - ) - } - - if (mode === "positions" && selectedPageType) { - return ( - { - updatePageType(selectedPageType, (current) => ({ ...current, positions: nextPositions })) - }} - onCancel={() => setMode("detail")} - /> - ) - } - - if (mode === "detail" && selectedPageType) { - const excludeCount = selectedOverride.exclude?.length ?? 0 - const positionsCount = selectedOverride.positions - ? Object.keys(selectedOverride.positions).length - : 0 - - const options: SelectOption[] = [ - { - name: `Excluded plugins (${excludeCount} excluded)`, - description: "", - value: "exclude", - }, - { - name: `Position overrides (${positionsCount} overrides)`, - description: "", - value: "positions", - }, - ] - - return ( - - - Edit page type "{selectedPageType}" - - setSelected(Math.max(0, index))} - onSelect={() => { - if (!selectedEntry) return - setMode("detail") - }} - showDescription - showScrollIndicator - /> - )} - - - Enter: edit │ n: new │ d: delete │ Esc: back - - - - ) -} - -interface PageTypeExcludeViewProps { - pageType: string - exclude: string[] - plugins: EnrichedPlugin[] - onUpdate: (nextExclude: string[]) => void - onCancel: () => void -} - -function PageTypeExcludeView({ - pageType, - exclude, - plugins, - onUpdate, - onCancel, -}: PageTypeExcludeViewProps) { - const enabledPlugins = plugins.filter((plugin) => plugin.enabled) - - useKeyboard((event) => { - if (event.name === "escape") onCancel() - }) - - const options: SelectOption[] = enabledPlugins.map((plugin) => { - const isExcluded = exclude.includes(plugin.name) - return { - name: plugin.displayName, - description: isExcluded ? "excluded" : "included", - value: plugin.name, - } - }) - - return ( - - - Excluded plugins for "{pageType}": - - {enabledPlugins.length === 0 ? ( - - No enabled plugins - - ) : ( - { - const pluginName = option?.value as string | undefined - if (!pluginName) return - setActivePlugin(pluginName) - setMode("select-position") - }} - showDescription - showScrollIndicator - /> - )} - - - Enter: select │ Esc: back - - - - ) - } - - if (mode === "select-position" && activePlugin) { - const options: SelectOption[] = ZONES.map((zone) => ({ - name: zone, - description: "", - value: zone, - })) - - return ( - - - Select position for "{activePlugin}": - - setSelected(Math.max(0, index))} - onSelect={() => { - const entry = entries[selected] - if (!entry || entry.kind !== "plugin") return - setActivePlugin(entry.pluginName) - setMode("select-position") - }} - showDescription - showScrollIndicator - /> - )} - - - Enter: edit │ n: new │ d: delete │ Esc: back - - - - ) -} diff --git a/quartz/cli/tui/panels/PluginsPanel.tsx b/quartz/cli/tui/panels/PluginsPanel.tsx deleted file mode 100644 index 20c299111..000000000 --- a/quartz/cli/tui/panels/PluginsPanel.tsx +++ /dev/null @@ -1,1578 +0,0 @@ -import { useState, useCallback, useMemo, useEffect } from "react" -import { useKeyboard } from "@opentui/react" -import { usePlugins } from "../hooks/usePlugins.js" - -type SortMode = "config" | "alpha" | "priority" - -const SORT_MODES: SortMode[] = ["config", "alpha", "priority"] - -type View = "list" | "add" | "confirm-remove" | "order" - -interface PluginsPanelProps { - notify: (message: string, type?: "success" | "error" | "info") => void - onFocusChange: (focused: boolean) => void - maxHeight?: number -} - -interface PluginSelectOption { - name: string - description: string - value?: unknown -} - -const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] - -function Spinner({ label }: { label: string }) { - const [frame, setFrame] = useState(0) - - useEffect(() => { - const timer = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80) - return () => clearInterval(timer) - }, []) - - return ( - - {SPINNER_FRAMES[frame]} {label} - - ) -} - -function ConfirmPrompt({ onConfirm, onCancel }: { onConfirm: () => void; onCancel: () => void }) { - useKeyboard((event) => { - if (event.name === "y") onConfirm() - else if (event.name === "n" || event.name === "escape") onCancel() - }) - - return ( - - Confirm? (y/n): - - ) -} - -function parseJsonOrString(value: string): unknown { - try { - return JSON.parse(value) - } catch { - return value - } -} - -function getDefaultForKey( - manifest: Record | null, - keyPath: string[], -): unknown | undefined { - if (!manifest) return undefined - const defaults = manifest.defaultOptions as Record | undefined - if (!defaults) return undefined - let current: unknown = defaults - for (const key of keyPath) { - if (current === null || current === undefined || typeof current !== "object") return undefined - current = (current as Record)[key] - } - return current -} - -interface OptionSchemaEntry { - type: "enum" | "array" - values?: string[] - items?: { type: "enum"; values: string[] } -} - -function getOptionSchema( - manifest: Record | null, - key: string, -): OptionSchemaEntry | null { - if (!manifest) return null - const schema = manifest.optionSchema as Record | undefined - if (!schema) return null - const entry = schema[key] - if (!entry || typeof entry !== "object") return null - return entry as OptionSchemaEntry -} - -function isDefault(current: unknown, defaultVal: unknown): boolean { - if (defaultVal === undefined) return false - return JSON.stringify(current) === JSON.stringify(defaultVal) -} - -function formatValue(value: unknown): string { - if (value === null) return "null" - if (value === undefined) return "undefined" - if (typeof value === "string") return `"${value}"` - if (typeof value === "boolean" || typeof value === "number") return String(value) - if (Array.isArray(value)) return `[${value.map(formatValue).join(", ")}]` - if (typeof value === "object") { - const entries = Object.entries(value as Record) - if (entries.length === 0) return "{}" - return `{${entries.map(([k, v]) => `${k}: ${formatValue(v)}`).join(", ")}}` - } - return String(value) -} - -function isEditableObject(value: unknown): value is Record { - return value !== null && typeof value === "object" && !Array.isArray(value) -} - -function normalizeCategories(category: string | string[] | undefined): string[] { - if (!category) return ["unknown"] - if (Array.isArray(category)) return category.length > 0 ? category : ["unknown"] - return [category] -} - -interface CategorizedEntry { - plugin: ReturnType["plugins"][number] - sortedIndex: number - category: string -} - -export function PluginsPanel({ notify, onFocusChange, maxHeight }: PluginsPanelProps) { - const { plugins, refresh, toggleEnabled, setPluginOrder, setPluginOptions } = usePlugins() - - const [selectedIndex, setSelectedIndex] = useState(0) - const [view, setView] = useState("list") - const [loading, setLoading] = useState(false) - const [progressMessages, setProgressMessages] = useState([]) - const [sortMode, setSortMode] = useState("config") - const [editingKey, setEditingKey] = useState(null) - const [editingSubKey, setEditingSubKey] = useState(null) - const [editingArrayIndex, setEditingArrayIndex] = useState(null) - const [listKey, setListKey] = useState(0) - const [showOptions, setShowOptions] = useState(false) - const [highlightedOptionIndex, setHighlightedOptionIndex] = useState(0) - const [addingObjectKey, setAddingObjectKey] = useState(false) - const [newObjectKeyName, setNewObjectKeyName] = useState(null) - const [highlightedSubKeyIndex, setHighlightedSubKeyIndex] = useState(0) - const [highlightedArrayItemIndex, setHighlightedArrayItemIndex] = useState(0) - const [highlightedBoolIndex, setHighlightedBoolIndex] = useState(0) - const [highlightedEnumIndex, setHighlightedEnumIndex] = useState(0) - - const sortedPlugins = useMemo(() => { - const sorted = [...plugins] - sorted.sort((a, b) => { - const aCat = normalizeCategories(a.category)[0] - const bCat = normalizeCategories(b.category)[0] - const catCmp = aCat.localeCompare(bCat) - if (catCmp !== 0) return catCmp - switch (sortMode) { - case "alpha": - return a.displayName.localeCompare(b.displayName) - case "priority": - return a.order - b.order - case "config": - default: - return a.index - b.index - } - }) - return sorted - }, [plugins, sortMode]) - - const categorizedEntries = useMemo(() => { - const entries: CategorizedEntry[] = [] - for (let i = 0; i < sortedPlugins.length; i++) { - const plugin = sortedPlugins[i] - const cats = normalizeCategories(plugin.category) - for (const cat of cats) { - entries.push({ plugin, sortedIndex: i, category: cat }) - } - } - entries.sort((a, b) => { - const catCmp = a.category.localeCompare(b.category) - if (catCmp !== 0) return catCmp - switch (sortMode) { - case "alpha": - return a.plugin.displayName.localeCompare(b.plugin.displayName) - case "priority": - return a.plugin.order - b.plugin.order - case "config": - default: - return a.plugin.index - b.plugin.index - } - }) - return entries - }, [sortedPlugins, sortMode]) - - const listOptions = useMemo(() => { - const result: PluginSelectOption[] = [] - let lastCategory = "" - for (let i = 0; i < categorizedEntries.length; i++) { - const entry = categorizedEntries[i] - const cat = entry.category - - if (cat !== lastCategory) { - result.push({ - name: `── ${cat.toUpperCase()} ──`, - description: "", - value: { type: "separator" as const }, - }) - lastCategory = cat - } - - const plugin = entry.plugin - const status = plugin.enabled ? "● ON " : "○ OFF" - const configIcon = - plugin.options && - typeof plugin.options === "object" && - Object.keys(plugin.options).length > 0 - ? " ⚙" - : "" - const orderLabel = ` [${plugin.order}]` - const installedLabel = plugin.installed ? "" : " │ NOT INSTALLED" - result.push({ - name: ` ${status} ${plugin.displayName}${configIcon}${orderLabel}`, - description: `${plugin.source}${installedLabel}`, - value: { type: "plugin" as const, sortedIndex: entry.sortedIndex }, - }) - } - return result - }, [categorizedEntries]) - - const resolvePluginIndex = useCallback( - (selectIdx: number): number | null => { - const item = listOptions[selectIdx] - if (!item?.value || typeof item.value !== "object") return null - const val = item.value as { type: string; sortedIndex?: number } - if (val.type === "plugin" && typeof val.sortedIndex === "number") return val.sortedIndex - return null - }, - [listOptions], - ) - - const selectedPlugin = useMemo(() => { - const pluginIdx = resolvePluginIndex(selectedIndex) - return pluginIdx !== null ? (sortedPlugins[pluginIdx] ?? null) : null - }, [selectedIndex, resolvePluginIndex, sortedPlugins]) - - const enterView = useCallback( - (v: View) => { - setView(v) - onFocusChange(true) - }, - [onFocusChange], - ) - - const exitView = useCallback(() => { - setView("list") - setListKey((k) => k + 1) - onFocusChange(false) - }, [onFocusChange]) - - useEffect(() => { - if (selectedIndex >= sortedPlugins.length) { - setSelectedIndex(Math.max(0, sortedPlugins.length - 1)) - } - }, [selectedIndex, sortedPlugins.length]) - - useEffect(() => { - if (showOptions) { - setEditingKey(null) - setEditingSubKey(null) - setEditingArrayIndex(null) - setHighlightedOptionIndex(0) - setHighlightedSubKeyIndex(0) - setHighlightedArrayItemIndex(0) - setHighlightedBoolIndex(0) - setHighlightedEnumIndex(0) - setAddingObjectKey(false) - setNewObjectKeyName(null) - } - }, [showOptions]) - - useEffect(() => { - if (view === "confirm-remove" && !selectedPlugin) { - exitView() - } - }, [view, selectedPlugin, exitView]) - - useEffect(() => { - if (editingKey !== null && selectedPlugin) { - setHighlightedSubKeyIndex(0) - setHighlightedArrayItemIndex(0) - const currentValue = selectedPlugin.options[editingKey] - setHighlightedBoolIndex(currentValue === true ? 0 : 1) - const schema = getOptionSchema( - selectedPlugin.manifest as Record | null, - editingKey, - ) - if (schema?.type === "enum" && schema.values && typeof currentValue === "string") { - const idx = schema.values.indexOf(currentValue) - setHighlightedEnumIndex(idx >= 0 ? idx : 0) - } else { - setHighlightedEnumIndex(0) - } - } - }, [editingKey, selectedPlugin]) - - useEffect(() => { - if (editingArrayIndex !== null && editingKey !== null && selectedPlugin) { - const currentValue = selectedPlugin.options[editingKey] - if (Array.isArray(currentValue)) { - const currentItem = currentValue[editingArrayIndex] - const schema = getOptionSchema( - selectedPlugin.manifest as Record | null, - editingKey, - ) - if ( - schema?.type === "array" && - schema.items?.type === "enum" && - schema.items.values && - typeof currentItem === "string" - ) { - const idx = schema.items.values.indexOf(currentItem) - setHighlightedEnumIndex(idx >= 0 ? idx : 0) - } - } - } - }, [editingArrayIndex, editingKey, selectedPlugin]) - - useEffect(() => { - if (editingSubKey !== null && editingKey !== null && selectedPlugin) { - const parentValue = selectedPlugin.options[editingKey] - if (isEditableObject(parentValue)) { - const subVal = parentValue[editingSubKey] - setHighlightedBoolIndex(subVal === true ? 0 : 1) - } - } - }, [editingSubKey, editingKey, selectedPlugin]) - - useKeyboard((event) => { - if (view !== "list" || loading || showOptions) return - - if (event.name === "s") { - setSortMode((current: SortMode) => { - const nextIdx = (SORT_MODES.indexOf(current) + 1) % SORT_MODES.length - const next = SORT_MODES[nextIdx] - setSelectedIndex(0) - notify(`Sort: ${next}`, "info") - return next - }) - } - - if (event.name === "e" && selectedPlugin) { - if (!selectedPlugin.enabled) { - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0) { - toggleEnabled(origIdx) - notify(`Enabled ${selectedPlugin.displayName}`, "success") - } - } - } - if (event.name === "d" && selectedPlugin) { - if (selectedPlugin.enabled) { - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0) { - toggleEnabled(origIdx) - notify(`Disabled ${selectedPlugin.displayName}`, "success") - } - } - } - - if (event.name === "n") { - enterView("add") - } - - if (event.name === "x" && selectedPlugin) { - enterView("confirm-remove") - } - - if (event.name === "o" && selectedPlugin) { - const hasOptions = - selectedPlugin.options && - typeof selectedPlugin.options === "object" && - Object.keys(selectedPlugin.options).length > 0 - if (hasOptions) { - setShowOptions(true) - onFocusChange(true) - } else { - notify("No options to configure", "info") - } - } - - if (event.name === "O" && selectedPlugin) { - enterView("order") - } - - if (event.name === "u") { - setLoading(true) - setProgressMessages(["→ Updating plugins..."]) - notify("Updating plugins...", "info") - import("../async-plugin-ops.js").then(({ tuiPluginUpdate }) => { - tuiPluginUpdate(undefined, (msg: string) => { - setProgressMessages((prev) => [...prev.slice(-20), msg]) - }) - .then((result) => { - refresh() - if (result.success) { - const count = result.updated?.length ?? 0 - notify(`Updated ${count} plugin(s)`, "success") - } else { - notify("Some updates failed", "error") - } - }) - .catch(() => notify("Update failed", "error")) - .finally(() => { - setLoading(false) - setProgressMessages([]) - }) - }) - } - - if (event.name === "i") { - setLoading(true) - setProgressMessages(["→ Installing plugins from lockfile..."]) - import("../async-plugin-ops.js").then(({ tuiPluginInstall }) => { - tuiPluginInstall((msg: string) => { - setProgressMessages((prev) => [...prev.slice(-20), msg]) - }) - .then((result) => { - refresh() - if (result.success) { - notify(`Installed ${result.installed ?? 0} plugin(s)`, "success") - } else { - notify("Some installs failed", "error") - } - }) - .catch(() => notify("Install failed", "error")) - .finally(() => { - setLoading(false) - setProgressMessages([]) - }) - }) - } - }) - - useKeyboard((event) => { - if (view !== "add") return - if (event.name === "escape") exitView() - }) - - useKeyboard((event) => { - if (view !== "order") return - if (event.name === "escape") exitView() - }) - - useKeyboard((event) => { - if (!showOptions) return - if (event.name === "escape") { - if (addingObjectKey || newObjectKeyName !== null) { - setAddingObjectKey(false) - setNewObjectKeyName(null) - } else if (editingArrayIndex !== null) { - setEditingArrayIndex(null) - } else if (editingSubKey) { - setEditingSubKey(null) - } else if (editingKey) { - setEditingKey(null) - setAddingObjectKey(false) - setNewObjectKeyName(null) - } else { - setShowOptions(false) - onFocusChange(false) - } - } - if (!editingKey && selectedPlugin) { - const optionEntries = Object.entries(selectedPlugin.options) - const count = optionEntries.length - if (count === 0) return - if (event.name === "up") { - setHighlightedOptionIndex((prev) => (prev > 0 ? prev - 1 : count - 1)) - } - if (event.name === "down") { - setHighlightedOptionIndex((prev) => (prev < count - 1 ? prev + 1 : 0)) - } - if (event.name === "return") { - const key = optionEntries[highlightedOptionIndex]?.[0] - if (key) { - setEditingKey(key) - setEditingSubKey(null) - setEditingArrayIndex(null) - } - } - if (event.name === "d" && event.shift) { - const entry = optionEntries[highlightedOptionIndex] - if (!entry) return - const [key] = entry - const defaultVal = getDefaultForKey( - selectedPlugin.manifest as Record | null, - [key], - ) - if (defaultVal === undefined) { - notify("No default available for this option", "info") - return - } - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0) { - setPluginOptions(origIdx, key, defaultVal) - notify(`Restored ${key} to default`, "success") - } - } - } else if (editingKey && selectedPlugin) { - const currentValue = selectedPlugin.options[editingKey] - - if (editingSubKey && isEditableObject(currentValue)) { - const parentValue = currentValue - const currentSubValue = parentValue[editingSubKey] - if (typeof currentSubValue === "boolean") { - if (event.name === "up" || event.name === "down") { - setHighlightedBoolIndex((prev) => (prev === 0 ? 1 : 0)) - } - if (event.name === "return") { - const newVal = highlightedBoolIndex === 0 - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0) { - const updatedParent = { ...parentValue, [editingSubKey]: newVal } - setPluginOptions(origIdx, editingKey, updatedParent) - notify(`Set ${editingKey}.${editingSubKey} = ${newVal}`, "success") - } - setEditingSubKey(null) - } - } - } else if ( - editingArrayIndex === null && - !editingSubKey && - !addingObjectKey && - newObjectKeyName === null - ) { - if (isEditableObject(currentValue)) { - const subEntries = Object.entries(currentValue) - const totalItems = subEntries.length + 1 - if (event.name === "up") { - setHighlightedSubKeyIndex((prev) => (prev > 0 ? prev - 1 : totalItems - 1)) - } - if (event.name === "down") { - setHighlightedSubKeyIndex((prev) => (prev < totalItems - 1 ? prev + 1 : 0)) - } - if (event.name === "return") { - if (highlightedSubKeyIndex === subEntries.length) { - setAddingObjectKey(true) - } else { - const subKey = subEntries[highlightedSubKeyIndex]?.[0] - if (subKey) setEditingSubKey(subKey) - } - } - if (event.name === "x" && highlightedSubKeyIndex < subEntries.length) { - const subKey = subEntries[highlightedSubKeyIndex]?.[0] - if (subKey) { - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0) { - const updated = { ...currentValue } - delete updated[subKey] - setPluginOptions(origIdx, editingKey, updated) - notify(`Removed field ${editingKey}.${subKey}`, "success") - setHighlightedSubKeyIndex((prev) => - prev >= subEntries.length - 1 ? Math.max(0, subEntries.length - 2) : prev, - ) - } - } - } - } else if (Array.isArray(currentValue)) { - const totalItems = currentValue.length + 1 - if (event.name === "up" && !event.shift) { - setHighlightedArrayItemIndex((prev) => (prev > 0 ? prev - 1 : totalItems - 1)) - } - if (event.name === "down" && !event.shift) { - setHighlightedArrayItemIndex((prev) => (prev < totalItems - 1 ? prev + 1 : 0)) - } - if (event.name === "return") { - if (highlightedArrayItemIndex === currentValue.length) { - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0) { - const newArray = [...currentValue, ""] - setPluginOptions(origIdx, editingKey, newArray) - notify(`Added item to ${editingKey}`, "success") - setEditingArrayIndex(newArray.length - 1) - } - } else { - setEditingArrayIndex(highlightedArrayItemIndex) - } - } - if (event.name === "x" && highlightedArrayItemIndex < currentValue.length) { - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0) { - const newArray = [...currentValue] - newArray.splice(highlightedArrayItemIndex, 1) - setPluginOptions(origIdx, editingKey, newArray) - notify(`Removed ${editingKey}[${highlightedArrayItemIndex}]`, "success") - setHighlightedArrayItemIndex((prev) => - prev >= newArray.length ? Math.max(0, newArray.length - 1) : prev, - ) - } - } - if ( - event.name === "up" && - event.shift && - highlightedArrayItemIndex > 0 && - highlightedArrayItemIndex < currentValue.length - ) { - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0) { - const newArray = [...currentValue] - const idx = highlightedArrayItemIndex - ;[newArray[idx - 1], newArray[idx]] = [newArray[idx], newArray[idx - 1]] - setPluginOptions(origIdx, editingKey, newArray) - setHighlightedArrayItemIndex(idx - 1) - } - } - if ( - event.name === "down" && - event.shift && - highlightedArrayItemIndex < currentValue.length - 1 - ) { - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0) { - const newArray = [...currentValue] - const idx = highlightedArrayItemIndex - ;[newArray[idx], newArray[idx + 1]] = [newArray[idx + 1], newArray[idx]] - setPluginOptions(origIdx, editingKey, newArray) - setHighlightedArrayItemIndex(idx + 1) - } - } - } else if (typeof currentValue === "boolean") { - if (event.name === "up" || event.name === "down") { - setHighlightedBoolIndex((prev) => (prev === 0 ? 1 : 0)) - } - if (event.name === "return") { - const newVal = highlightedBoolIndex === 0 - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0) { - setPluginOptions(origIdx, editingKey, newVal) - notify(`Set ${editingKey} = ${newVal}`, "success") - } - setEditingKey(null) - } - } else { - const optSchema = getOptionSchema( - selectedPlugin.manifest as Record | null, - editingKey, - ) - if (optSchema?.type === "enum" && optSchema.values) { - if (event.name === "up" || event.name === "down") { - const len = optSchema.values.length - setHighlightedEnumIndex((prev) => - event.name === "up" - ? prev > 0 - ? prev - 1 - : len - 1 - : prev < len - 1 - ? prev + 1 - : 0, - ) - } - if (event.name === "return") { - const newVal = optSchema.values[highlightedEnumIndex] - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0 && newVal !== undefined) { - setPluginOptions(origIdx, editingKey, newVal) - notify(`Set ${editingKey} = ${newVal}`, "success") - } - setEditingKey(null) - } - } - } - } else if (editingArrayIndex !== null && Array.isArray(currentValue)) { - const optSchema = getOptionSchema( - selectedPlugin.manifest as Record | null, - editingKey, - ) - if ( - optSchema?.type === "array" && - optSchema.items?.type === "enum" && - optSchema.items.values - ) { - const enumValues = optSchema.items.values - if (event.name === "up" || event.name === "down") { - const len = enumValues.length - setHighlightedEnumIndex((prev) => - event.name === "up" ? (prev > 0 ? prev - 1 : len - 1) : prev < len - 1 ? prev + 1 : 0, - ) - } - if (event.name === "return") { - const newVal = enumValues[highlightedEnumIndex] - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0 && newVal !== undefined) { - const newArray = [...currentValue] - newArray[editingArrayIndex] = newVal - setPluginOptions(origIdx, editingKey, newArray) - notify(`Set ${editingKey}[${editingArrayIndex}] = ${newVal}`, "success") - } - setEditingArrayIndex(null) - } - } - } - } - }) - - const optionSummary = useMemo(() => { - if (!selectedPlugin?.options || typeof selectedPlugin.options !== "object") return "none" - const keys = Object.keys(selectedPlugin.options) - if (keys.length === 0) return "none" - const defaults = (selectedPlugin.manifest as Record | null)?.defaultOptions as - | Record - | undefined - const summaryParts = keys.map((key) => { - const val = selectedPlugin.options[key] - const defVal = defaults ? defaults[key] : undefined - const tag = isDefault(val, defVal) ? "" : "*" - return `${tag}${key}` - }) - return summaryParts.join(", ") - }, [selectedPlugin]) - - if (loading) { - const progressHeight = maxHeight ? Math.max(3, maxHeight - 5) : undefined - return ( - - - Progress - - - - - - - {progressMessages.length === 0 ? ( - - Waiting for updates... - - ) : ( - progressMessages.map((message, index) => ( - {message} - )) - )} - - - - ) - } - - if (view === "add") { - return ( - - - Add Plugin - - - Enter a git source (e.g., github:owner/repo) - - - Source: - - { - const source = value.trim() - if (!source) { - exitView() - return - } - exitView() - setLoading(true) - setProgressMessages([`→ Adding ${source}...`]) - import("../async-plugin-ops.js").then(({ tuiPluginAdd }) => { - tuiPluginAdd([source], (msg: string) => { - setProgressMessages((prev) => [...prev.slice(-20), msg]) - }) - .then((result) => { - refresh() - if (result.success) { - notify(`Added ${result.installed ?? 0} plugin(s)`, "success") - } else { - notify("Failed to add plugin", "error") - } - }) - .catch(() => notify("Add failed", "error")) - .finally(() => { - setLoading(false) - setProgressMessages([]) - }) - }) - }} - /> - - - - Enter: confirm │ Esc: cancel - - - ) - } - - if (view === "confirm-remove" && selectedPlugin) { - return ( - - - - Remove {selectedPlugin.displayName}? - - - - Source: {selectedPlugin.source} - - - Confirm removal? - { - const pluginName = selectedPlugin.name - exitView() - setLoading(true) - setProgressMessages([`→ Removing ${selectedPlugin.displayName}...`]) - import("../async-plugin-ops.js").then(({ tuiPluginRemove }) => { - tuiPluginRemove([pluginName], (msg: string) => { - setProgressMessages((prev) => [...prev.slice(-20), msg]) - }) - .then((result) => { - refresh() - setSelectedIndex(Math.max(0, selectedIndex - 1)) - if (result.success) { - notify(`Removed ${selectedPlugin.displayName}`, "success") - } else { - notify("Remove failed", "error") - } - }) - .catch(() => notify("Remove failed", "error")) - .finally(() => { - setLoading(false) - setProgressMessages([]) - }) - }) - }} - onCancel={() => exitView()} - /> - - - ) - } - - if (view === "order" && selectedPlugin) { - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - return ( - - - Set Order for {selectedPlugin.displayName} - - - Current: {selectedPlugin.order} - - - New order: - - { - const num = parseInt(value, 10) - if (!isNaN(num) && origIdx >= 0) { - setPluginOrder(origIdx, num) - notify(`Set order to ${num}`, "success") - } - exitView() - }} - /> - - - - ) - } - - if (view === "confirm-remove" && !selectedPlugin) { - exitView() - } - - const renderOptionsPanel = () => { - if (!selectedPlugin || !showOptions) return null - - const optionEntries = Object.entries(selectedPlugin.options) - const defaults = (selectedPlugin.manifest as Record | null)?.defaultOptions as - | Record - | undefined - - const renderEditPanel = () => { - if (!editingKey) { - // Show preview of highlighted option - const highlightedEntry = optionEntries[highlightedOptionIndex] - if (!highlightedEntry) { - return ( - - - Select an option to edit - - - ) - } - const [hKey, hValue] = highlightedEntry - const hDefault = getDefaultForKey( - selectedPlugin.manifest as Record | null, - [hKey], - ) - const hIsDefault = isDefault(hValue, hDefault) - const typeLabel = isEditableObject(hValue) - ? "object" - : Array.isArray(hValue) - ? "array" - : typeof hValue - return ( - - - {hKey} - ({typeLabel}) - - - Value: - {formatValue(hValue)} - - {hIsDefault ? ( - - ✓ Default value - - ) : hDefault !== undefined ? ( - - Default: {formatValue(hDefault)} - - ) : null} - - - Enter: edit │ D: restore default │ Esc: close options - - - - ) - } - - const currentValue = selectedPlugin.options[editingKey] - const defaultVal = getDefaultForKey( - selectedPlugin.manifest as Record | null, - [editingKey], - ) - const isDefaultVal = isDefault(currentValue, defaultVal) - - if (editingKey && editingSubKey && isEditableObject(currentValue)) { - const parentValue = currentValue - const currentSubValue = parentValue[editingSubKey] - const defaultParent = getDefaultForKey( - selectedPlugin.manifest as Record | null, - [editingKey], - ) - const defaultSubValue = isEditableObject(defaultParent) - ? defaultParent[editingSubKey] - : undefined - const isDefaultSub = isDefault(currentSubValue, defaultSubValue) - - if (typeof currentSubValue === "boolean") { - const boolItems = [ - { label: "true", isCurrent: currentSubValue === true }, - { label: "false", isCurrent: currentSubValue === false }, - ] - return ( - - - - {editingKey} → {editingSubKey} - - (boolean) - - - Current: {String(currentSubValue)} - {isDefaultSub && (default)} - - {defaultSubValue !== undefined && !isDefaultSub && ( - - Default: {formatValue(defaultSubValue)} - - )} - - {boolItems.map((item, i) => { - const isHighlighted = i === highlightedBoolIndex - const fg = isHighlighted ? "#FFFFFF" : "#888888" - const marker = isHighlighted ? "▸ " : " " - const currentTag = item.isCurrent ? " ◀" : "" - return ( - - - {marker} - {item.label} - {currentTag} - - - ) - })} - - - ↑↓: toggle │ Enter: select │ Esc: back - - - ) - } - - return ( - - - - {editingKey} → {editingSubKey} - - - - Current: {formatValue(currentSubValue)} - {isDefaultSub && (default)} - - {defaultSubValue !== undefined && !isDefaultSub && ( - - Default: {formatValue(defaultSubValue)} - - )} - - New value: - - { - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0) { - const updatedParent = { - ...parentValue, - [editingSubKey]: parseJsonOrString(value), - } - setPluginOptions(origIdx, editingKey, updatedParent) - notify(`Set ${editingKey}.${editingSubKey}`, "success") - } - setEditingSubKey(null) - }} - /> - - - - Enter: save │ Esc: back - - - ) - } - - if (editingKey && editingArrayIndex !== null && Array.isArray(currentValue)) { - const currentItem = currentValue[editingArrayIndex] - const defaultArr = Array.isArray(defaultVal) ? defaultVal : undefined - const defaultItem = defaultArr ? defaultArr[editingArrayIndex] : undefined - const isDefaultItem = isDefault(currentItem, defaultItem) - - const optSchema = getOptionSchema( - selectedPlugin.manifest as Record | null, - editingKey, - ) - if ( - optSchema?.type === "array" && - optSchema.items?.type === "enum" && - optSchema.items.values - ) { - const enumValues = optSchema.items.values - return ( - - - - {editingKey} → [{editingArrayIndex}] - - (enum) - - - Current: {formatValue(currentItem)} - {isDefaultItem && (default)} - - {defaultItem !== undefined && !isDefaultItem && ( - - Default: {formatValue(defaultItem)} - - )} - - {enumValues.map((val, i) => { - const isHighlighted = i === highlightedEnumIndex - const isCurrent = currentItem === val - const fg = isHighlighted ? "#FFFFFF" : "#888888" - const marker = isHighlighted ? "▸ " : " " - const currentTag = isCurrent ? " ◀" : "" - return ( - - - {marker} - {val} - {currentTag} - - - ) - })} - - - ↑↓: navigate │ Enter: select │ Esc: back - - - ) - } - - return ( - - - - {editingKey} → [{editingArrayIndex}] - - - - Current: {formatValue(currentItem)} - {isDefaultItem && (default)} - - {defaultItem !== undefined && !isDefaultItem && ( - - Default: {formatValue(defaultItem)} - - )} - - New value: - - { - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0) { - const newArray = [...currentValue] - newArray[editingArrayIndex] = parseJsonOrString(value) - setPluginOptions(origIdx, editingKey, newArray) - notify(`Set ${editingKey}[${editingArrayIndex}]`, "success") - } - setEditingArrayIndex(null) - }} - /> - - - - Enter: save │ Esc: back - - - ) - } - - if (isEditableObject(currentValue)) { - // Adding a new field: step 2 — enter the value for the new key - if (newObjectKeyName !== null) { - return ( - - - - {editingKey} → {newObjectKeyName} - - (new field) - - - Value: - - { - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0) { - const updatedParent = { - ...currentValue, - [newObjectKeyName]: parseJsonOrString(value), - } - setPluginOptions(origIdx, editingKey, updatedParent) - notify(`Added ${editingKey}.${newObjectKeyName}`, "success") - } - setNewObjectKeyName(null) - setAddingObjectKey(false) - }} - /> - - - - Enter: save │ Esc: cancel - - - ) - } - - // Adding a new field: step 1 — enter the key name - if (addingObjectKey) { - return ( - - - {editingKey} - — Add new field - - - Key name: - - { - const keyName = value.trim() - if (!keyName) { - setAddingObjectKey(false) - return - } - if (keyName in currentValue) { - notify(`Field "${keyName}" already exists`, "error") - return - } - setNewObjectKeyName(keyName) - }} - /> - - - - Enter: next │ Esc: cancel - - - ) - } - - const subEntries = Object.entries(currentValue) - const addFieldLabel = "+ Add field" - return ( - - - {editingKey} - {`{…} ${subEntries.length} field(s)`} - {isDefaultVal && (all defaults)} - - - - {subEntries.map(([subKey, subVal], i) => { - const subDefault = isEditableObject(defaultVal) ? defaultVal[subKey] : undefined - const defaultTag = isDefault(subVal, subDefault) ? " ✓" : "" - const isHighlighted = i === highlightedSubKeyIndex - const fg = isHighlighted ? "#FFFFFF" : "#888888" - const marker = isHighlighted ? "▸ " : " " - return ( - - - {marker} - {subKey} - {defaultTag} - - {formatValue(subVal)} - - ) - })} - - - {highlightedSubKeyIndex === subEntries.length ? "▸ " : " "} - {addFieldLabel} - - - - - - ↑↓: navigate │ Enter: edit field │ x: delete │ Esc: back - - - ) - } - - if (Array.isArray(currentValue)) { - const addItemLabel = "+ Add item" - return ( - - - {editingKey} - {`[…] ${currentValue.length} item(s)`} - {isDefaultVal && (all defaults)} - - - - {currentValue.map((item, idx) => { - const defaultArr = Array.isArray(defaultVal) ? defaultVal : undefined - const defaultItem = defaultArr ? defaultArr[idx] : undefined - const defaultTag = isDefault(item, defaultItem) ? " ✓" : "" - const isHighlighted = idx === highlightedArrayItemIndex - const fg = isHighlighted ? "#FFFFFF" : "#888888" - const marker = isHighlighted ? "▸ " : " " - return ( - - - {marker}[{idx}]{defaultTag} - - {formatValue(item)} - - ) - })} - - - {highlightedArrayItemIndex === currentValue.length ? "▸ " : " "} - {addItemLabel} - - - - - - - ↑↓: navigate │ ⇧↑↓: move │ Enter: edit │ x: delete │ Esc: back - - - - ) - } - - if (typeof currentValue === "boolean") { - const boolItems = [ - { label: "true", isCurrent: currentValue === true }, - { label: "false", isCurrent: currentValue === false }, - ] - return ( - - - {editingKey} - (boolean) - - - Current: {String(currentValue)} - {isDefaultVal && (default)} - - {defaultVal !== undefined && !isDefaultVal && ( - - Default: {formatValue(defaultVal)} - - )} - - {boolItems.map((item, i) => { - const isHighlighted = i === highlightedBoolIndex - const fg = isHighlighted ? "#FFFFFF" : "#888888" - const marker = isHighlighted ? "▸ " : " " - const currentTag = item.isCurrent ? " ◀" : "" - return ( - - - {marker} - {item.label} - {currentTag} - - - ) - })} - - - ↑↓: toggle │ Enter: select │ Esc: back - - - ) - } - - const optSchema = getOptionSchema( - selectedPlugin.manifest as Record | null, - editingKey, - ) - if (optSchema?.type === "enum" && optSchema.values) { - const enumValues = optSchema.values - return ( - - - {editingKey} - (enum) - - - Current: {formatValue(currentValue)} - {isDefaultVal && (default)} - - {defaultVal !== undefined && !isDefaultVal && ( - - Default: {formatValue(defaultVal)} - - )} - - {enumValues.map((val, i) => { - const isHighlighted = i === highlightedEnumIndex - const isCurrent = currentValue === val - const fg = isHighlighted ? "#FFFFFF" : "#888888" - const marker = isHighlighted ? "▸ " : " " - const currentTag = isCurrent ? " ◀" : "" - return ( - - - {marker} - {val} - {currentTag} - - - ) - })} - - - ↑↓: navigate │ Enter: select │ Esc: back - - - ) - } - - return ( - - - {editingKey} - - - Current: {formatValue(currentValue)} - {isDefaultVal && (default)} - - {defaultVal !== undefined && !isDefaultVal && ( - - Default: {formatValue(defaultVal)} - - )} - - New value: - - { - const origIdx = plugins.findIndex((p) => p.index === selectedPlugin.index) - if (origIdx >= 0) { - const parsed = parseJsonOrString(value) - if (typeof currentValue === "number" && typeof parsed !== "number") { - notify("Invalid: expected a number", "error") - return - } - setPluginOptions(origIdx, editingKey, parsed) - notify(`Set ${editingKey}`, "success") - } - setEditingKey(null) - }} - /> - - - - Enter: save │ Esc: back - - - ) - } - - const renderOptionsList = () => { - return ( - - - {optionEntries.map(([key, value], i) => { - const defaultVal = defaults ? defaults[key] : undefined - const defaultTag = isDefault(value, defaultVal) ? " ✓" : "" - const typeTag = isEditableObject(value) ? " {…}" : Array.isArray(value) ? " […]" : "" - const isActive = editingKey === key - const isHighlighted = !editingKey && i === highlightedOptionIndex - const fg = isActive ? "#FFFF00" : isHighlighted ? "#FFFFFF" : "#888888" - const marker = isActive ? "▶ " : isHighlighted ? "▸ " : " " - return ( - - - {marker} - {key} - {typeTag} - {defaultTag} - - - ) - })} - - - ) - } - - return ( - - - Options: {selectedPlugin.displayName} - │ Esc: back - - {optionEntries.length === 0 ? ( - - No options configured - - ) : ( - - - {renderOptionsList()} - - - {renderEditPanel()} - - - )} - - ) - } - - return ( - - - { - 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) - }} - /> - - - - {controlsLabel} - - - ) - } - - if (view === "edit-color") { - const currentValue = String(editingEntry.value ?? "") - const showSwatch = isValidColorValue(currentValue) - const errorText = colorError ?? "" - return ( - - - Edit: {pathLabel} - - - Current: {currentValue} - {showSwatch ? : null} - - - Value: - - { - 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) - }} - /> - - - - {errorText} - - - Enter: save │ Esc: cancel - - - ) - } - - if (view === "edit-string") { - const currentLabel = formatStringValue(editingEntry.value) - return ( - - - Edit: {pathLabel} - - - Current: {currentLabel} - - - Value: - - { - const parsed = parseJsonOrString(value) - applyValue(editingEntry.keyPath, parsed) - }} - /> - - - - Enter: save │ Esc: cancel - - - ) - } - - return null - } - - if (view !== "list" && editingEntry) { - return ( - - - - Global Configuration - - - {renderTree(true)} - - - - {renderEditPanel()} - - - ) - } - - return ( - - - Global Configuration - - - {renderTree(false)} - - - ↑↓: navigate │ Enter: edit/expand │ Shift+D: restore default - - - ) -} diff --git a/quartz/cli/tui/tsconfig.json b/quartz/cli/tui/tsconfig.json deleted file mode 100644 index a63a4fa35..000000000 --- a/quartz/cli/tui/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "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"] -}