Files
cladm/CLAUDE.md
Alejandro Gutiérrez fcafe652cf Add TUI launcher implementation and project docs
Source modules for history parsing, git metadata, project scanning,
terminal launching, and OpenTUI component layout. Remove private flag
for publishing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 22:56:54 +00:00

223 lines
7.9 KiB
Markdown

# cladm
Interactive terminal UI for launching Claude Code sessions across project folders. Built with OpenTUI + Bun.
## Project Overview
A rich TUI app that replaces `~/Desktop/launch-claude.sh`. Scans `~/.claude/history.jsonl` to discover projects, displays them in an interactive multi-select list with metadata (git branch, last commit, dirty status, Claude session stats), and launches selected projects in new Terminal windows with `claude --dangerously-skip-permissions`.
## Tech Stack
- **Runtime**: Bun (>=1.3.0) — use `bun` exclusively, never node/npm/npx
- **UI Framework**: @opentui/core (imperative API, not React/Solid bindings)
- **Language**: TypeScript
## Architecture
```
src/
index.ts — Entry point: createCliRenderer, compose layout, input loop
data/
history.ts — Parse ~/.claude/history.jsonl → project list with sessions/msgs/timestamps
git.ts — Git metadata: branch, last commit, dirty status (staged/unstaged/untracked)
scanner.ts — Filesystem fallback scanner (recursive, skips node_modules etc.)
components/
project-list.ts — ScrollBox + selectable rows with checkbox toggles
header.ts — Title bar with selected count, sort mode, source label
footer.ts — Keybinding hints
row.ts — Single project row: checkbox, name, branch, commit, dirty, claude stats, stack tags
actions/
launcher.ts — osascript Terminal.app integration to open claude sessions
sorter.ts — Sort logic: recent claude, name, last commit, most sessions
lib/
colors.ts — Theme colors and highlight helpers
time.ts — Relative time formatting (time_ago)
```
## Run
```sh
bun run src/index.ts
```
## Key Behaviors
1. **Default mode**: reads `~/.claude/history.jsonl` to discover all projects (sorted by most recently used)
2. **Fallback**: recursive filesystem scan from `~/Desktop` if no history
3. **Interactive picker**: arrow keys navigate, space toggles, `a` all, `n` none, `s` cycles sort, enter launches, `q` quits
4. **Per-row metadata**: project name (relative path), git branch, last commit age + message, dirty status (+staged ~modified ?untracked), last Claude use, session count, message count, stack tags
5. **Launch**: each selected project opens a new Terminal.app window via osascript running `cd <path> && claude --dangerously-skip-permissions`
## OpenTUI Reference
### Quick Start Pattern
```typescript
import { createCliRenderer, Box, Text, ScrollBox, Select, Input } from "@opentui/core"
const renderer = await createCliRenderer({ exitOnCtrlC: true })
renderer.root.add(
Box({ flexDirection: "column", width: "100%", height: "100%" },
// children...
)
)
```
### Core Concepts
- **Constructs** are factory functions: `Box(props, ...children)` → returns VNode
- **Renderables** are class instances: `new BoxRenderable(renderer, options)`
- Use constructs for declarative composition, renderables when you need imperative control
- `renderer.root.add(vnode)` to mount, `.remove(id)` to unmount
- `requestRender()` to trigger re-draw after state changes
### Layout (Yoga/Flexbox)
All components accept flexbox props:
- `flexDirection: "row" | "column" | "row-reverse" | "column-reverse"`
- `justifyContent: "flex-start" | "flex-end" | "center" | "space-between" | "space-around" | "space-evenly"`
- `alignItems: "flex-start" | "flex-end" | "center" | "stretch" | "baseline"`
- `width/height`: number (chars) or string ("100%", "50%")
- `padding`, `paddingTop/Right/Bottom/Left`, `margin*`, `gap`, `rowGap`, `columnGap`
- `position: "relative" | "absolute"` with `top/right/bottom/left`
- `overflow: "visible" | "hidden" | "scroll"`
- `flexGrow`, `flexShrink`, `flexBasis`
### Box
```typescript
Box({
borderStyle: "single" | "double" | "rounded" | "heavy",
border: boolean | ("top" | "right" | "bottom" | "left")[],
borderColor: string | RGBA,
backgroundColor: string | RGBA,
focusedBorderColor: ColorInput,
title: string,
titleAlignment: "left" | "center" | "right",
shouldFill: boolean,
focusable: boolean,
gap: number | `${number}%`,
}, ...children)
```
### Text + Styled Text
```typescript
import { Text, t, bold, italic, underline, dim, fg, bg, strikethrough } from "@opentui/core"
Text({
content: "plain" | t`${bold(fg("#00FFFF")("styled"))}`,
fg: string | RGBA,
bg: string | RGBA,
wrapMode: "none" | "char" | "word",
truncate: boolean,
selectable: boolean,
attributes: number, // TextAttributes bitmask
})
```
Color helpers: `red()`, `green()`, `blue()`, `yellow()`, `cyan()`, `magenta()`, `white()`, `gray()`, `brightRed()`, etc.
Background: `bgRed()`, `bgGreen()`, `bgBlue()`, `bgYellow()`, etc.
### Input
```typescript
Input({
placeholder: string,
maxLength: number, // default 1000
value: string, // initial value
// Inherits TextareaOptions for colors, keybindings
})
// Events: "input" (each char), "change" (on blur), "enter" (on Enter key)
// Methods: .value, .focus(), .blur(), .submit()
```
### Select
```typescript
Select({
options: [{ name: string, description: string, value?: any }],
backgroundColor / textColor / focusedBackgroundColor / focusedTextColor,
selectedBackgroundColor / selectedTextColor,
descriptionColor / selectedDescriptionColor,
showScrollIndicator: boolean,
wrapSelection: boolean,
showDescription: boolean,
itemSpacing: number,
fastScrollStep: number,
keyBindings: SelectKeyBinding[],
})
// Events: "selectionChanged", "itemSelected"
// Methods: .getSelectedOption(), .getSelectedIndex(), .moveUp/Down(), .selectCurrent()
```
### ScrollBox
```typescript
ScrollBox({
scrollX: boolean, // default false
scrollY: boolean, // default true
stickyScroll: boolean,
viewportCulling: boolean, // default true
// Inherits BoxOptions (border, bg, etc.)
})
// Methods: .scrollBy(delta), .scrollTo(pos), .scrollTop/.scrollLeft
// Children added via .add() are delegated to internal content container
```
### Keyboard Handling
```typescript
// Global input handler
renderer.addInputHandler((key: KeyEvent) => {
key.name // "a", "return", "escape", "up", "down", etc.
key.ctrl // boolean
key.meta // boolean (Cmd on mac)
key.shift // boolean
key.preventDefault()
key.stopPropagation()
})
// Per-component
Box({ onKeyDown: (key) => { ... }, focusable: true })
```
### Colors (RGBA)
```typescript
import { RGBA } from "@opentui/core"
RGBA.fromHex("#FF0000")
RGBA.fromInts(255, 0, 0, 255)
RGBA.fromValues(1.0, 0.0, 0.0, 1.0)
```
### Renderer Lifecycle
```typescript
const renderer = await createCliRenderer({
exitOnCtrlC: true,
targetFps: 30,
useMouse: true,
useAlternateScreen: true,
onDestroy: () => { /* cleanup */ },
})
// renderer.start() — auto-called
// renderer.destroy() — cleanup and exit
```
### Animation
```typescript
import { createTimeline } from "@opentui/core"
const tl = createTimeline()
tl.add({ targets: myRenderable, duration: 500, ease: "outQuad", onUpdate: (anim) => { ... } })
```
### Key Patterns for This Project
- Use `Box` with `flexDirection: "column"` for the main layout (header, list, footer)
- Use `ScrollBox` for the project list (handles overflow + scrolling)
- Build each row as a `Box` with `flexDirection: "row"` containing `Text` elements
- Track selection state in a `Map<number, boolean>` or `boolean[]`
- Use `renderer.addInputHandler()` for global keybinds (q, a, n, s, enter, space, arrows)
- Launch via `Bun.$\`osascript -e '...'\`` or `Bun.spawn()`
- Parse history.jsonl with `Bun.file().text()` + line-by-line JSON.parse
- Git data via `Bun.spawn(["git", "-C", path, ...])`
## Conventions
- Use Bun exclusively (no node, npm, npx)
- Bun auto-loads .env — no dotenv
- Prefer `Bun.file()` over `node:fs`
- Use `Bun.$` for shell commands, `Bun.spawn()` for processes
- No JSDoc comments, no excessive documentation
- camelCase for variables/functions, PascalCase for types/classes
- Keep it simple — this is a launcher, not a framework