UNPKG

tuix

Version:

A performant TUI framework for Bun with JSX and reactive state management

363 lines (320 loc) 10.9 kB
/** * Custom Help Screen for CLI-KIT * * A beautiful, interactive help screen showcasing our TUI framework capabilities */ import { Effect, Stream } from "effect" import { runApp } from "../src/core/runtime.ts" import type { Cmd, View, AppServices } from "../src/core/types.ts" import { vstack, hstack, text, styledText } from "../src/core/view.ts" import { style, Colors, Borders } from "../src/styling/index.ts" import { styledBox } from "../src/layout/box.ts" import { spacer } from "../src/layout/spacer.ts" import { largeAnimatedGradientText, colorPalettes } from "../src/components/LargeText.ts" import { LiveServices } from "../src/services/impl/index.ts" import { TerminalService } from "../src/services/index.ts" import type { HelpSection } from "../src/components/Help.ts" // ============================================================================= // Types // ============================================================================= interface HelpModel { time: number selectedSection: number expandedSection: boolean terminalWidth: number terminalHeight: number } type HelpMsg = | { _tag: "Tick"; time: number } | { _tag: "KeyPress"; key: string } | { _tag: "SelectSection"; index: number } | { _tag: "ToggleExpand" } | { _tag: "Quit" } // ============================================================================= // Help Content // ============================================================================= const helpSections: HelpSection[] = [ { title: "📸 Screenshot Commands", description: "Capture and manage CLI screenshots", bindings: [ { key: "screenshot list", description: "List all saved screenshots" }, { key: "screenshot quick <cmd>", description: "Quick capture with auto-generated name" }, { key: "screenshot create <name>", description: "Create named screenshot" }, { key: "screenshot show <name>", description: "Display a saved screenshot" }, { key: "screenshot delete <name>", description: "Delete a screenshot" }, { key: "screenshot export <name> <file>", description: "Export to file (json/text/ansi)" }, { key: "screenshot multi", description: "Batch capture multiple commands" } ] }, { title: "🎨 Screenshot Options", description: "Fine-tune your captures", bindings: [ { key: "--pty", description: "Use PTY mode for interactive commands" }, { key: "--duration <ms>", description: "Set capture duration for PTY mode" }, { key: "--raw", description: "Include raw ANSI codes in capture" }, { key: "-s, --show", description: "Show screenshot immediately after capture" }, { key: "-d, --description", description: "Add description to screenshot" }, { key: "-f, --format", description: "Export format: json, text, or ansi" } ] }, { title: "🚀 Getting Started", description: "Quick examples to try", bindings: [ { key: "cli-kit ss quick 'ls -la'", description: "Capture directory listing" }, { key: "cli-kit ss create demo --command 'echo Hello' --show", description: "Named capture with preview" }, { key: "cli-kit ss multi --examples", description: "Capture CLI-KIT examples" }, { key: "cli-kit create my-app", description: "Create new CLI-KIT app (coming soon!)" } ] }, { title: "⌨️ Keyboard Shortcuts", description: "Navigate this help screen", bindings: [ { key: "↑/↓ or j/k", description: "Navigate sections" }, { key: "Enter", description: "Expand/collapse section" }, { key: "q or Escape", description: "Exit help" }, { key: "?", description: "Show this help" } ] } ] // ============================================================================= // View Functions // ============================================================================= function renderHeader(model: HelpModel): View { const gradient = { colors: [ colorPalettes.neon.colors[0], colorPalettes.neon.colors[1], colorPalettes.neon.colors[2], colorPalettes.purple.colors[0], colorPalettes.purple.colors[1] ], direction: 'horizontal' as const } const animatedTitle = largeAnimatedGradientText({ text: "CLI-KIT", font: 'ansiShadow', gradient, time: model.time, animationSpeed: 0.05, mode: 'outlined' }) const subtitle = styledText( "The Ultimate Terminal UI Framework", style().foreground(Colors.brightCyan).italic() ) const version = styledText( "v1.0.0", style().foreground(Colors.gray) ) // Center by adding spacers const centerView = (view: View, width: number): View => { return hstack(spacer(), view, spacer()) } return vstack( text(""), centerView(animatedTitle, model.terminalWidth), centerView(subtitle, model.terminalWidth), centerView(version, model.terminalWidth), text("") ) } function renderSection(section: HelpSection, index: number, isSelected: boolean, isExpanded: boolean): View { const headerStyle = isSelected ? style().foreground(Colors.brightYellow).bold() : style().foreground(Colors.cyan).bold() const arrow = isExpanded ? "▼" : "▶" const header = styledText( `${arrow} ${section.title}`, headerStyle ) const description = section.description ? styledText(` ${section.description}`, style().foreground(Colors.gray).italic()) : text("") if (!isExpanded) { return vstack(header, description) } const bindings = section.bindings.map(binding => { const keyStyle = style().foreground(Colors.yellow) const descStyle = style().foreground(Colors.white) const separator = styledText(" • ", style().foreground(Colors.gray)) return hstack( text(" "), styledText(binding.key.padEnd(35), keyStyle), separator, styledText(binding.description, descStyle) ) }) return vstack( header, description, text(""), ...bindings, text("") ) } function renderFooter(model: HelpModel): View { const footerStyle = style().foreground(Colors.gray) const keyStyle = style().foreground(Colors.yellow).bold() return styledBox( hstack( styledText("Navigate: ", footerStyle), styledText("↑↓", keyStyle), styledText(" • Expand: ", footerStyle), styledText("Enter", keyStyle), styledText(" • Exit: ", footerStyle), styledText("q", keyStyle), styledText(" • ", footerStyle), styledText("Built with ❤️ using CLI-KIT", style().foreground(Colors.magenta)) ), { border: Borders.Single, padding: { top: 0, right: 1, bottom: 0, left: 1 }, style: style().foreground(Colors.gray) } ) } // ============================================================================= // Update Function // ============================================================================= function update(msg: HelpMsg, model: HelpModel): Effect.Effect<[HelpModel, Cmd<HelpMsg>[]], never, AppServices> { switch (msg._tag) { case "Tick": return Effect.succeed([{ ...model, time: msg.time }, []]) case "KeyPress": switch (msg.key) { case 'q': case 'escape': return Effect.succeed([model, [{ type: "exit" }]]) case 'up': case 'k': return Effect.succeed([ { ...model, selectedSection: Math.max(0, model.selectedSection - 1), expandedSection: false }, [] ]) case 'down': case 'j': return Effect.succeed([ { ...model, selectedSection: Math.min(helpSections.length - 1, model.selectedSection + 1), expandedSection: false }, [] ]) case 'enter': case ' ': return Effect.succeed([ { ...model, expandedSection: !model.expandedSection }, [] ]) default: return Effect.succeed([model, []]) } case "SelectSection": return Effect.succeed([ { ...model, selectedSection: msg.index, expandedSection: model.selectedSection === msg.index ? !model.expandedSection : true }, [] ]) case "ToggleExpand": return Effect.succeed([ { ...model, expandedSection: !model.expandedSection }, [] ]) case "Quit": return Effect.succeed([model, [{ type: "exit" }]]) } } // ============================================================================= // View Function // ============================================================================= function view(model: HelpModel): View { const header = renderHeader(model) const sections = helpSections.map((section, index) => renderSection( section, index, index === model.selectedSection, index === model.selectedSection && model.expandedSection ) ) const content = styledBox( vstack(...sections), { border: Borders.Rounded, padding: { top: 1, right: 2, bottom: 1, left: 2 }, style: style().foreground(Colors.white) } ) const footer = renderFooter(model) // Add left padding with spaces const paddedContent = hstack( text(" "), // 4 spaces for padding content ) // Center footer const centeredFooter = hstack(spacer(), footer, spacer()) return vstack( header, paddedContent, text(""), centeredFooter ) } // ============================================================================= // Subscriptions // ============================================================================= function subscriptions(_model: HelpModel): Cmd<HelpMsg>[] { return [ { type: "tick", fps: 10, handler: (time) => ({ _tag: "Tick", time }) }, { type: "keypress", handler: (key) => ({ _tag: "KeyPress", key: key.key }) } ] } // ============================================================================= // Main Function // ============================================================================= export function showHelp(): Effect.Effect<void, never, AppServices> { return runApp({ init: Effect.gen(function* () { const terminal = yield* TerminalService const size = yield* terminal.size const initialModel: HelpModel = { time: 0, selectedSection: 0, expandedSection: true, // Start with first section expanded terminalWidth: size.columns, terminalHeight: size.rows } return [initialModel, []] }), update, view, subscriptions }) } // Run if called directly if (import.meta.main) { await Effect.runPromise( showHelp().pipe( Effect.provide(LiveServices) ) ) }