Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 48 additions & 22 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Commands

```bash
# Run in development (no build step needed)
npm run dev
# Development
npm run dev # run TUI (no build needed)
npm run dev -- init # pass init subcommand through npm
npm run dev -- --help # pass --help through npm

# Build to dist/
npm run build
# Build
npm run build # esbuild -> dist/tmuxtui.js

# Run the CLI directly
npx tsx bin/tmuxtui.ts
npx tsx bin/tmuxtui.ts init # register current dir as a new tmux session
# Test the built output
npm run dev:build # build + run the compiled CLI
npm run dev:pack # build + npm pack + extract (pre-publish check)
```

There are no tests or linter configured.
Expand All @@ -22,22 +24,46 @@ There are no tests or linter configured.

**tmuxtui** is an Ink (React-for-terminals) TUI that manages tmux sessions. It wraps tmux CLI commands and renders an interactive session picker.

### Data flow
### Entry and CLI dispatch

1. `bin/tmuxtui.ts` — shebang entry; re-exports `src/index.tsx`
2. `src/index.tsx` — two modes:
- `init` subcommand: calls `createSession()` with `cwd` name/path and exits
- TUI mode: renders the Ink `<App>`, awaits exit, then calls `tmux attach-session` (outside tmux) or `tmux switch-client` (inside tmux) based on `process.env.TMUX`
3. `src/components/App.tsx` — single `SessionView` component; all UI state lives here:
- `mode`: `'list' | 'new' | 'rename' | 'confirm-kill'`
- Keyboard input is gated on `process.stdin.isTTY` (passed as `interactive` prop)
- Session mutations call parent callbacks (onCreate/onKill/onRename/onDetach) then `exit()` or `refresh()`
4. `src/services/tmuxService.ts` — thin wrappers around `execSync` tmux commands; all session names/paths are single-quote-escaped before shell interpolation
5. `src/types.ts` — `TmuxSession` interface (name, windows, created, attached, sessionId, path, lastAttached)
`src/index.tsx` handles all CLI subcommands before entering TUI mode:

- `init` / `-i` — create a detached tmux session named `basename(cwd)` and exit
- `last` / `-l` — attach to the most recently used session (no TUI)
- `update` — fetch latest version from npm and `npm install -g`
- `version` / `-v` — print `__APP_VERSION__` (injected by esbuild at build time, `'dev'` when running via tsx)
- `help` / `-h` — print usage text
- `--favorites` / `-F` — flag passed into TUI to filter to starred sessions only
- No args → render the interactive TUI, then `tmux attach-session` (outside tmux) or `tmux switch-client` (inside tmux)

`bin/tmuxtui.ts` is a shebang entry that re-exports `src/index.tsx`.

### Build

`build.mjs` uses esbuild to bundle everything into a single ESM file at `dist/tmuxtui.js`. It injects `__APP_VERSION__` from `package.json` and strips `react-devtools-core` via a plugin. The `declare const __APP_VERSION__` at the top of `src/index.tsx` provides the TypeScript type.

### Component structure

`src/components/App.tsx` is a single `SessionView` component holding all UI state:

- **Mode union**: `'list' | 'new' | 'rename' | 'confirm-kill' | 'config' | 'search' | 'detail' | 'confirm-batch-kill' | 'help'`
- **Config sub-modes**: `'list' | 'new' | 'rename' | 'confirm-delete' | 'init-panes' | 'move'` (window management within a session)
- Keyboard input is gated on `process.stdin.isTTY` (passed as `interactive` prop)
- All rendering is done inline in the component — no separate sub-components

### Services

- **`src/services/tmuxService.ts`** — `execSync` wrappers for tmux commands (session, window, pane CRUD). All names/paths are single-quote-escaped before shell interpolation. Contains `PANE_LAYOUTS` (9 preset layouts with preview ASCII art and split instructions) and `initPanes()` which executes sequential `split-window` commands. Also exports `formatTime()` for epoch-to-readable conversion.
- **`src/services/favoritesService.ts`** — persists a `Set<string>` of favorited session names to `~/.config/tmuxtui/favorites.json`.

### Types

`src/types.ts` defines `TmuxSession`, `TmuxWindow`, and `TmuxPane` interfaces matching tmux's `list-sessions`, `list-windows`, and `list-panes` output formats.

### Key behaviors

- Sessions are listed sorted by `lastAttached` (most recent first)
- The `init` subcommand uses `basename(cwd)` as the session name and `cwd` as the start path
- `~` in path input is expanded via `String.replace(/^~/, HOME)` at creation time
- The app uses ESM (`"type": "module"`); imports use `.js` extensions even for `.tsx` source files (TypeScript bundler resolution)
- Sessions sorted by `lastAttached` (most recent first), with attached sessions pinned to top
- `~` in path input expanded via `String.replace(/^~/, HOME)` at creation time
- Fuzzy search (`/` key) uses a simple subsequence matcher, not regex
- Batch operations: `Tab` to mark sessions, `X` batch-detach, `D` batch-kill
- ESM (`"type": "module"`); imports use `.js` extensions even for `.tsx` source files (TypeScript bundler resolution)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tmux-tui",
"version": "1.3.0",
"version": "1.3.1",
"description": "Terminal UI for tmux session management",
"type": "module",
"bin": {
Expand Down
3 changes: 2 additions & 1 deletion src/services/tmuxService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export function listSessions(): TmuxSession[] {
attached: attached === '1',
sessionId,
path: path || '',
lastAttached: parseInt(lastAttached, 10),
lastAttached: parseInt(lastAttached, 10) || 0,
};
});

Expand Down Expand Up @@ -242,6 +242,7 @@ export function moveWindow(srcSession: string, windowIndex: number, dstSession:
}

export function formatTime(epoch: number): string {
if (!epoch) return '—';
const d = new Date(epoch * 1000);
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const h = d.getHours().toString().padStart(2, '0');
Expand Down
Loading