Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:08:16 +08:00
commit fc569e5620
38 changed files with 4997 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
---
name: github-pr-workflow
description: Working with GitHub Pull Requests using the gh CLI. Use for fetching PR details, review comments, CI status, and understanding the difference between PR-level comments vs inline code review comments.
---
# GitHub PR Workflow
## Key Concepts
### Comment Types
GitHub PRs have **two different types of comments**:
1. **PR-level comments** - General discussion on the PR (shown via `gh pr view --comments`)
2. **Inline code review comments** - Comments attached to specific lines of code (requires API)
**Important**: `gh pr view --comments` does NOT show inline code review comments!
## Scripts
| Script | Purpose |
|--------|---------|
| `gh-pr-review-comments <PR>` | Get inline code review comments (the ones `gh` misses!) |
| `gh-pr-summary <PR>` | PR title, description, state, branches |
| `gh-pr-reviews <PR>` | Review decisions (approved/changes requested) |
| `gh-pr-checks <PR>` | CI check status |
All scripts auto-detect the repo from git remote, or accept `[REPO]` as second arg.
## Common Commands
```bash
# Basic PR info
gh pr view <PR> # Overview
gh pr view <PR> --comments # PR-level comments only (NOT inline!)
gh pr diff <PR> # View the diff
# Review comments (inline) - USE THE SCRIPT
gh-pr-review-comments <PR> # ✅ Gets inline code review comments
# Or manually via API
gh api repos/OWNER/REPO/pulls/PR/comments | jq '.[] | {path, line, body}'
# Reviews (approve/request changes)
gh pr review <PR> --approve
gh pr review <PR> --request-changes --body "Please fix X"
gh pr review <PR> --comment --body "Looks good overall"
# Checks
gh pr checks <PR> # CI status
gh run view <RUN_ID> --log-failed # Failed job logs
```
## API Endpoints Reference
When `gh` commands don't expose what you need, use the API:
```bash
# Inline review comments
gh api repos/OWNER/REPO/pulls/PR/comments
# PR-level comments (issue comments)
gh api repos/OWNER/REPO/issues/PR/comments
# Review submissions
gh api repos/OWNER/REPO/pulls/PR/reviews
# Commits in PR
gh api repos/OWNER/REPO/pulls/PR/commits
# Files changed
gh api repos/OWNER/REPO/pulls/PR/files
```
## Workflow: Addressing Review Comments
1. **Get the comments**: `gh-pr-review-comments <PR>`
2. **Make fixes** in your local branch
3. **Push** (if using JJ: `jj git push`)
4. **Reply to comments** on GitHub or via API
5. **Re-request review** if needed: `gh pr edit <PR> --add-reviewer <USER>`

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Get CI check status for a PR
# Usage: gh-pr-checks <PR_NUMBER> [REPO]
set -euo pipefail
PR_NUMBER="${1:?Usage: gh-pr-checks <PR_NUMBER> [REPO]}"
REPO="${2:-}"
if [[ -z "$REPO" ]]; then
REPO=$(gh repo view --json nameWithOwner -q '.nameWithOwner' 2>/dev/null || true)
if [[ -z "$REPO" ]]; then
echo "Error: Could not detect repo. Provide REPO as second argument" >&2
exit 1
fi
fi
# Get the head SHA first
HEAD_SHA=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha')
gh api "repos/${REPO}/commits/${HEAD_SHA}/check-runs" | jq -r '
.check_runs[] |
"\(if .conclusion == "success" then "✓" elif .conclusion == "failure" then "✗" elif .status == "in_progress" then "⋯" else "?" end) \(.name): \(.conclusion // .status)"'

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Get inline code review comments from a GitHub PR
# Usage: gh-pr-review-comments <PR_NUMBER> [REPO]
#
# If REPO is not provided, uses the current git remote origin
set -euo pipefail
PR_NUMBER="${1:?Usage: gh-pr-review-comments <PR_NUMBER> [REPO]}"
REPO="${2:-}"
# If no repo provided, try to detect from git remote
if [[ -z "$REPO" ]]; then
REPO=$(gh repo view --json nameWithOwner -q '.nameWithOwner' 2>/dev/null || true)
if [[ -z "$REPO" ]]; then
echo "Error: Could not detect repo. Provide REPO as second argument (e.g., owner/repo)" >&2
exit 1
fi
fi
gh api "repos/${REPO}/pulls/${PR_NUMBER}/comments" | jq -r '
.[] |
"───────────────────────────────────────────────────────────────────────
File: \(.path):\(.line // .original_line // "N/A")
Author: \(.user.login) (\(.created_at | split("T")[0]))
───────────────────────────────────────────────────────────────────────
\(.body)
"'

View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Get review decisions (approved, changes_requested, commented) for a PR
# Usage: gh-pr-reviews <PR_NUMBER> [REPO]
set -euo pipefail
PR_NUMBER="${1:?Usage: gh-pr-reviews <PR_NUMBER> [REPO]}"
REPO="${2:-}"
if [[ -z "$REPO" ]]; then
REPO=$(gh repo view --json nameWithOwner -q '.nameWithOwner' 2>/dev/null || true)
if [[ -z "$REPO" ]]; then
echo "Error: Could not detect repo. Provide REPO as second argument" >&2
exit 1
fi
fi
gh api "repos/${REPO}/pulls/${PR_NUMBER}/reviews" | jq -r '
.[] |
select(.state != "PENDING") |
"\(.user.login): \(.state) (\(.submitted_at | split("T")[0]))\(if .body != "" then "\n \(.body)" else "" end)"'

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
# Get a summary of a PR: title, body, state, and review status
# Usage: gh-pr-summary <PR_NUMBER> [REPO]
set -euo pipefail
PR_NUMBER="${1:?Usage: gh-pr-summary <PR_NUMBER> [REPO]}"
REPO="${2:-}"
if [[ -z "$REPO" ]]; then
REPO=$(gh repo view --json nameWithOwner -q '.nameWithOwner' 2>/dev/null || true)
if [[ -z "$REPO" ]]; then
echo "Error: Could not detect repo. Provide REPO as second argument" >&2
exit 1
fi
fi
gh api "repos/${REPO}/pulls/${PR_NUMBER}" | jq -r '
"PR #\(.number): \(.title)
State: \(.state) | Mergeable: \(.mergeable // "unknown") | Draft: \(.draft)
Author: \(.user.login)
Branch: \(.head.ref) → \(.base.ref)
Created: \(.created_at | split("T")[0])
URL: \(.html_url)
─── Description ───
\(.body // "(no description)")"'

View File

@@ -0,0 +1,590 @@
---
name: package-npm-nix
description: Package npm/TypeScript/Bun CLI tools for Nix. Use when creating Nix derivations for JavaScript/TypeScript tools from npm registry or GitHub sources, handling pre-built packages or source builds with dependency management.
---
<objective>
Create Nix packages for npm-based CLI tools, covering both pre-built packages from npm registry and source builds with proper dependency management. This skill provides patterns for fetching, building, and packaging JavaScript/TypeScript/Bun tools in Nix environments.
</objective>
<quick_start>
<pre_built_from_npm>
For tools already built and published to npm (fastest approach):
```nix
{
lib,
stdenv,
fetchzip,
nodejs,
}:
stdenv.mkDerivation rec {
pname = "tool-name";
version = "1.0.0";
src = fetchzip {
url = "https://registry.npmjs.org/${pname}/-/${pname}-${version}.tgz";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
nativeBuildInputs = [ nodejs ];
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp $src/dist/cli.js $out/bin/tool-name
chmod +x $out/bin/tool-name
# Fix shebang
substituteInPlace $out/bin/tool-name \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
runHook postInstall
'';
meta = with lib; {
description = "Tool description";
homepage = "https://github.com/org/repo";
license = licenses.mit;
sourceProvenance = with lib.sourceTypes; [ binaryBytecode ];
maintainers = with maintainers; [ ];
mainProgram = "tool-name";
platforms = platforms.all;
};
}
```
Get the hash:
```bash
nix-prefetch-url --unpack https://registry.npmjs.org/tool-name/-/tool-name-1.0.0.tgz
# Convert to SRI format:
nix hash convert --to sri --hash-algo sha256 <hash-output>
```
</pre_built_from_npm>
<source_build_with_bun>
For tools that need to be built from source using Bun:
```nix
{
lib,
stdenv,
stdenvNoCC,
fetchFromGitHub,
bun,
makeBinaryWrapper,
nodejs,
autoPatchelfHook,
}:
let
fetchBunDeps =
{ src, hash, ... }@args:
stdenvNoCC.mkDerivation {
pname = args.pname or "${src.name or "source"}-bun-deps";
version = args.version or src.version or "unknown";
inherit src;
nativeBuildInputs = [ bun ];
buildPhase = ''
export HOME=$TMPDIR
export npm_config_ignore_scripts=true
bun install --no-progress --frozen-lockfile --ignore-scripts
'';
installPhase = ''
mkdir -p $out
cp -R ./node_modules $out
cp ./bun.lock $out/
'';
dontFixup = true;
outputHash = hash;
outputHashAlgo = "sha256";
outputHashMode = "recursive";
};
version = "1.0.0";
src = fetchFromGitHub {
owner = "org";
repo = "repo";
rev = "v${version}";
hash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
};
node_modules = fetchBunDeps {
pname = "tool-name-bun-deps";
inherit version src;
hash = "sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=";
};
in
stdenv.mkDerivation rec {
pname = "tool-name";
inherit version src;
nativeBuildInputs = [
bun
nodejs
makeBinaryWrapper
autoPatchelfHook
];
buildInputs = [
stdenv.cc.cc.lib
];
buildPhase = ''
# Verify lockfile match
diff -q ./bun.lock ${node_modules}/bun.lock || exit 1
# Copy and patch node_modules
cp -R ${node_modules}/node_modules .
chmod -R u+w node_modules
patchShebangs node_modules
autoPatchelf node_modules
export HOME=$TMPDIR
export npm_config_ignore_scripts=true
bun run build
'';
installPhase = ''
mkdir -p $out/bin
cp dist/tool-name $out/bin/tool-name
chmod +x $out/bin/tool-name
'';
dontStrip = true;
meta = with lib; {
description = "Tool description";
homepage = "https://github.com/org/repo";
license = licenses.mit;
sourceProvenance = with lib.sourceTypes; [ fromSource ];
maintainers = with maintainers; [ ];
mainProgram = "tool-name";
platforms = [ "x86_64-linux" ];
};
}
```
</source_build_with_bun>
</quick_start>
<workflow>
<step_1_identify_package_type>
**Determine build approach**:
Check the npm package:
```bash
# Download and inspect
nix-prefetch-url --unpack https://registry.npmjs.org/package/-/package-1.0.0.tgz
ls -la /nix/store/<hash>-package-1.0.0.tgz/
```
If `dist/` directory exists with built files:
→ Use pre-built approach (simpler, faster)
If only `src/` exists or package.json has build scripts:
→ Use source build approach
Check package.json for:
- `"bin"` field: Shows what executables are provided
- `"type": "module"`: ES modules (common in modern packages)
- `"scripts"`: Build commands (indicates source build needed)
- Runtime: Look for bun, node, or specific version requirements
</step_1_identify_package_type>
<step_2_fetch_hashes>
**Get source and dependency hashes**:
For pre-built packages:
```bash
# Fetch npm tarball
nix-prefetch-url --unpack https://registry.npmjs.org/pkg/-/pkg-1.0.0.tgz
# Output: 1abc... (base32 format)
# Convert to SRI format
nix hash convert --to sri --hash-algo sha256 1abc...
# Output: sha256-xyz...
```
For source builds:
```bash
# Get GitHub source hash
nix-prefetch-url --unpack https://github.com/org/repo/archive/v1.0.0.tar.gz
# Get dependencies hash (requires iteration):
# 1. Use lib.fakeHash in fetchBunDeps
# 2. Try to build
# 3. Nix will show expected hash in error
# 4. Update hash and rebuild
```
</step_2_fetch_hashes>
<step_3_create_package_files>
**Create package structure**:
```bash
mkdir -p packages/tool-name
```
Create `packages/tool-name/package.nix` with full derivation (see quick_start).
Create `packages/tool-name/default.nix`:
```nix
{ pkgs }: pkgs.callPackage ./package.nix { }
```
This two-file pattern allows the package to be used standalone or integrated into a flake.
</step_3_create_package_files>
<step_4_handle_special_cases>
**Common additional requirements**:
**WASM files or other assets**:
```nix
installPhase = ''
mkdir -p $out/bin
cp $src/dist/cli.js $out/bin/tool
cp $src/dist/*.wasm $out/bin/ # Copy WASM alongside
chmod +x $out/bin/tool
substituteInPlace $out/bin/tool \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
'';
```
**Multiple executables**:
```nix
# package.json might have:
# "bin": {
# "tool": "dist/cli.js",
# "tool-admin": "dist/admin.js"
# }
installPhase = ''
mkdir -p $out/bin
for exe in tool tool-admin; do
cp $src/dist/$exe.js $out/bin/$exe
chmod +x $out/bin/$exe
substituteInPlace $out/bin/$exe \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
done
'';
meta.mainProgram = "tool"; # Primary command
```
**Platform-specific binaries**:
```nix
meta = {
platforms = [ "x86_64-linux" ]; # Bun-compiled binaries often Linux-only
# or
platforms = platforms.all; # Pure JS works everywhere
};
```
</step_4_handle_special_cases>
<step_5_test_build>
**Build and test**:
```bash
# Build
nix build .#tool-name
# Test the binary
./result/bin/tool-name --version
./result/bin/tool-name --help
# Check dependencies (Linux)
ldd ./result/bin/tool-name # Should show all deps resolved
# Format
nix fmt
# Run flake checks
nix flake check
```
</step_5_test_build>
</workflow>
<metadata_requirements>
<essential_fields>
Every package must have complete metadata:
```nix
meta = with lib; {
description = "Clear, concise description";
homepage = "https://project-homepage.com";
changelog = "https://github.com/org/repo/releases"; # Optional but nice
license = licenses.mit; # or licenses.unfree for proprietary
sourceProvenance = with lib.sourceTypes; [
fromSource # Built from source
# or
binaryBytecode # Pre-built JS/TS (npm dist/)
# or
binaryNativeCode # Compiled binaries
];
maintainers = with maintainers; [ ]; # Empty OK for community packages
mainProgram = "binary-name";
platforms = platforms.all; # or specific: [ "x86_64-linux" ]
};
```
</essential_fields>
<source_provenance_guide>
**Choose based on what you're packaging**:
- `fromSource`: Built from TypeScript/source during derivation
- `binaryBytecode`: Pre-compiled JS from npm registry
- `binaryNativeCode`: Native binaries (Rust, Go, Bun-compiled)
This affects security auditing and reproducibility expectations.
</source_provenance_guide>
</metadata_requirements>
<common_patterns>
<shebang_replacement>
**Always replace shebangs** for reproducibility:
```nix
# Single file
substituteInPlace $out/bin/tool \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
# Multiple files
find $out/bin -type f -exec substituteInPlace {} \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node" \;
```
The `--replace-quiet` flag suppresses warnings if pattern not found.
</shebang_replacement>
<native_dependencies>
**Handle native modules** (like sqlite, sharp):
```nix
nativeBuildInputs = [
bun
nodejs
makeBinaryWrapper
autoPatchelfHook # Linux: patches ELF binaries
];
buildInputs = [
stdenv.cc.cc.lib # Provides libgcc_s.so.1, libstdc++.so.6
];
autoPatchelfIgnoreMissingDeps = [
"libc.musl-x86_64.so.1" # Ignore musl if not available
];
```
`autoPatchelf` runs automatically on Linux, fixing RPATH for .so files.
</native_dependencies>
<bun_compiled_binaries>
**Don't strip Bun-compiled executables**:
```nix
# Bun embeds JavaScript in the binary
dontStrip = true;
```
Stripping would remove the embedded JS, breaking the program.
</bun_compiled_binaries>
<checking_tarball_contents>
**Inspect npm package structure**:
```bash
# After nix-prefetch-url
ls -la /nix/store/*-pkg-1.0.0.tgz/
# Common layouts:
# dist/cli.js → Pre-built, use directly
# dist/index.js → Main entry, check package.json "bin"
# src/index.ts → Source only, need to build
# lib/ → Built CommonJS
# esm/ → Built ES modules
```
Check package.json to find the correct entry point.
</checking_tarball_contents>
</common_patterns>
<anti_patterns>
<avoid_these>
**Don't do this**:
❌ Hardcode node paths:
```nix
# Bad
"#!/usr/bin/node" # Won't work on NixOS
```
✅ Use substituteInPlace:
```nix
# Good
substituteInPlace $out/bin/tool \
--replace-quiet "#!/usr/bin/env node" "#!${nodejs}/bin/node"
```
❌ Skip hash verification:
```nix
# Bad - insecure
hash = lib.fakeHash;
```
✅ Get real hash:
```nix
# Good - reproducible and secure
hash = "sha256-actual-hash-here";
```
❌ Forget to make executable:
```nix
# Bad - won't run
cp $src/dist/cli.js $out/bin/tool
```
✅ Set executable bit:
```nix
# Good
cp $src/dist/cli.js $out/bin/tool
chmod +x $out/bin/tool
```
❌ Strip Bun binaries:
```nix
# Bad - breaks Bun-compiled executables
# (default behavior strips binaries)
```
✅ Disable stripping:
```nix
# Good
dontStrip = true;
```
</avoid_these>
</anti_patterns>
<troubleshooting>
<hash_mismatch>
**Error: "hash mismatch in fixed-output derivation"**
The hash you provided doesn't match what Nix fetched.
Solution:
1. Nix error shows "got: sha256-XYZ..."
2. Copy that hash into your derivation
3. Rebuild
For `fetchBunDeps`, this is expected the first time—use the error output to get the correct hash.
</hash_mismatch>
<missing_executable>
**Error: Binary not found after build**
Check:
```bash
# List what was actually built
ls -R result/
# Check package.json "bin" field
cat /nix/store/*-source/package.json | jq .bin
# Check build output location
cat /nix/store/*-source/package.json | jq .scripts.build
```
The build might output to a different directory than expected.
</missing_executable>
<elf_interpreter_error>
**Error: "No such file or directory" when running binary (Linux)**
The binary needs ELF patching for native dependencies.
Solution:
```nix
nativeBuildInputs = [
autoPatchelfHook
];
buildInputs = [
stdenv.cc.cc.lib
];
```
For node_modules with native addons:
```nix
buildPhase = ''
cp -R ${node_modules}/node_modules .
chmod -R u+w node_modules
autoPatchelf node_modules # Patch .node files
'';
```
</elf_interpreter_error>
<bun_lock_mismatch>
**Error: "bun.lock mismatch"**
The lockfile in your source doesn't match the cached dependencies.
This happens when:
- Source version updated but dependency hash not updated
- Source repo has uncommitted lockfile changes
Solution:
1. Update source hash to match new version
2. Set dependency hash to `lib.fakeHash`
3. Build to get correct dependency hash
4. Update dependency hash
5. Rebuild
</bun_lock_mismatch>
</troubleshooting>
<validation>
<build_checklist>
Before considering the package done:
- [ ] `nix build .#package-name` succeeds
- [ ] `./result/bin/tool --version` works
- [ ] `./result/bin/tool --help` works
- [ ] `nix flake check` passes
- [ ] `meta.description` is clear and concise
- [ ] `meta.homepage` points to project site
- [ ] `meta.license` is correct
- [ ] `meta.sourceProvenance` matches what you packaged
- [ ] `meta.mainProgram` is set
- [ ] `meta.platforms` is appropriate for the tool
- [ ] All hashes are real (no `lib.fakeHash`)
- [ ] Shebangs use Nix store paths, not /usr/bin
- [ ] File is formatted with `nix fmt`
</build_checklist>
<testing_on_other_platforms>
If you only have Linux but package claims `platforms.all`:
Consider asking maintainers with macOS/ARM to test, or:
- Mark platforms conservatively based on what you can test
- Note in package that other platforms are untested
- Let CI or other contributors expand platform support
</testing_on_other_platforms>
</validation>
<success_criteria>
A well-packaged npm tool has:
- Clean build with no warnings or errors
- Working executable in `result/bin/`
- Complete and accurate metadata
- Proper source provenance classification
- All dependencies resolved (no missing libraries)
- Reproducible builds (real hashes, no network access during build)
- Follows Nix packaging conventions (shebang patching, proper phases)
</success_criteria>

View File

@@ -0,0 +1,175 @@
---
name: textual-builder
description: Build Text User Interface (TUI) applications using the Textual Python framework (v0.86.0+). Use when creating terminal-based applications, prototyping card games or interactive CLIs, or when the user mentions Textual, TUI, or terminal UI. Includes comprehensive reference documentation, card game starter template, and styling guides.
---
# Textual Builder
## Overview
This skill helps you build sophisticated Text User Interfaces (TUIs) using Textual, a Python framework for creating terminal and browser-based applications with a modern web-inspired API. It includes reference documentation, a card game template, and best practices for Textual development.
## Quick Start
### Basic Textual App
```python
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Label
class MyApp(App):
def compose(self) -> ComposeResult:
yield Header()
yield Label("Hello, Textual!")
yield Footer()
if __name__ == "__main__":
app = MyApp()
app.run()
```
### Card Game Template
For card game prototyping, copy the template:
```bash
cp -r assets/card-game-template/* ./my-game/
cd my-game
python app.py
```
The template includes:
- Interactive Card widget with face-up/down states
- Hand containers for player cards
- Play area with turn management
- Key bindings for card selection and playing
- Customizable styling
See `assets/card-game-template/README.md` for customization guide.
## When to Read Reference Documentation
This skill includes comprehensive reference files. Load them based on your task:
### references/basics.md
**Read when:** Setting up app structure, using reactive attributes, handling mounting, querying widgets, or working with messages/events.
**Covers:**
- App structure and compose method
- Reactive attributes and watchers
- Mounting and dynamic widget creation
- Widget querying
- Messages, events, and custom messages
### references/widgets.md
**Read when:** Adding UI elements like buttons, inputs, labels, data tables, or creating custom widgets.
**Covers:**
- Display widgets (Label, Static, Placeholder)
- Input widgets (Button, Input, TextArea, Switch)
- DataTable for tabular data
- Layout containers (Container, Grid, Horizontal, Vertical)
- Custom widget creation
- Header/Footer
### references/layout.md
**Read when:** Designing layouts, positioning widgets, using grid systems, or handling responsive sizing.
**Covers:**
- Layout types (vertical, horizontal, grid)
- Grid configuration (cell spanning, row/column sizing)
- Alignment and content positioning
- Docking widgets to screen edges
- Sizing (fixed, relative, fractional, auto)
- Spacing (margin, padding)
- Scrolling
### references/styling.md
**Read when:** Applying CSS styles, theming, adding borders, or customizing widget appearance.
**Covers:**
- CSS files and selectors
- Colors (named, hex, RGB, theme variables)
- Borders and border styling
- Text styling and alignment
- Opacity and tinting
- Rich markup for styled text
- Pseudo-classes (:hover, :focus, etc.)
### references/interactivity.md
**Read when:** Implementing keyboard shortcuts, handling mouse events, responding to user actions, or creating interactive behaviors.
**Covers:**
- Key bindings and actions
- Dynamic binding updates
- Mouse events (click, hover, enter, leave)
- Keyboard events
- Focus management
- Widget-specific messages
- Custom messages
- Notifications and timers
## Common Workflows
### Creating a New TUI App
1. Start with basic app structure (see Quick Start)
2. Design layout (read `references/layout.md`)
3. Add widgets (read `references/widgets.md`)
4. Style with CSS (read `references/styling.md`)
5. Add interactivity (read `references/interactivity.md`)
### Prototyping a Card Game
1. Copy the card game template
2. Customize the Card widget for your game's card properties
3. Modify game logic in action methods
4. Add game-specific rules in message handlers
5. Style cards and layout in `app.tcss`
### Adding Interactive Features
1. Define key bindings in `BINDINGS`
2. Implement action methods (`action_*`)
3. Handle widget messages (`on_button_pressed`, etc.)
4. Use reactive attributes for state management
5. Update UI in watchers
## Best Practices
- **Progressive Development**: Start simple, add complexity incrementally
- **Reactive State**: Use `reactive()` for state that affects UI
- **CSS Separation**: Keep styling in `.tcss` files, not inline
- **Widget Reuse**: Create custom widgets for repeated components
- **Message Bubbling**: Use `event.stop()` to control message propagation
- **Type Hints**: Use proper type hints for better IDE support
- **IDs and Classes**: Use semantic IDs/classes for querying and styling
## Installation
```bash
pip install textual
# or
uv pip install textual
```
Current version: v0.86.0+ (as of November 2025, latest is v6.6.0)
## Resources
### references/
Comprehensive documentation loaded on-demand:
- `basics.md` - Core concepts and app structure
- `widgets.md` - Widget catalog and usage
- `layout.md` - Layout systems and positioning
- `styling.md` - CSS and theming
- `interactivity.md` - Events, bindings, and actions
### assets/
- `card-game-template/` - Complete starter template for card games with interactive cards, hands, and turn management
## Official Documentation
For topics not covered in this skill, consult:
- https://textual.textualize.io/ (official docs)
- https://github.com/Textualize/textual (GitHub repo)

View File

@@ -0,0 +1,77 @@
# Card Game Template
A starter template for building turn-based card games with Textual.
## Features
- **Card Widget**: Customizable playing cards with suit, rank, face-up/down state
- **Hand Container**: Display and manage player hands
- **Play Area**: Central area for cards in play
- **Turn System**: Basic turn management
- **Interactivity**: Card selection and playing with keyboard shortcuts
## Running the Template
```bash
python app.py
```
## Key Bindings
- `d` - Draw a card
- `space` - Select/deselect card
- `p` - Play selected card
- `n` - Next turn
- `q` - Quit
## Customization
### Card Values
Modify the `Card` class to add game-specific properties:
```python
class Card(Widget):
def __init__(self, rank: str, suit: str, power: int = 0, special_ability: str = ""):
self.power = power
self.special_ability = special_ability
# ...
```
### Game Rules
Implement game logic in the `CardGameApp` methods:
- `on_card_played()` - Validate and process card plays
- `action_draw_card()` - Implement deck management
- `action_next_turn()` - Add turn-based game logic
### Card Appearance
Edit `Card.compose()` or the CSS in `app.tcss` to change card styling.
### Deck Management
Add a `Deck` class to manage card shuffling and drawing:
```python
class Deck:
def __init__(self):
self.cards = []
self.shuffle()
def shuffle(self):
import random
random.shuffle(self.cards)
def draw(self) -> Card | None:
return self.cards.pop() if self.cards else None
```
## Structure
```
card-game-template/
├── app.py # Main application
├── app.tcss # Styles
└── README.md # This file
```

View File

@@ -0,0 +1,324 @@
"""
Card Game Template for Textual
A starter template for building turn-based card games with Textual.
Includes:
- Card widget with customizable suit, rank, and face-up/down state
- Hand container for displaying player hands
- Play area for cards in play
- Turn management system
- Action system with key bindings
Customize this template for your specific card game rules.
"""
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical
from textual.widgets import Header, Footer, Label, Static
from textual.widget import Widget
from textual.reactive import reactive
from textual.message import Message
from textual.binding import Binding
class Card(Widget):
"""A playing card widget."""
DEFAULT_CSS = """
Card {
width: 12;
height: 10;
border: round white;
background: $panel;
content-align: center middle;
}
Card:hover {
background: $boost;
border: heavy $primary;
}
Card.selected {
border: double cyan;
background: $accent;
}
Card.face-down {
background: $surface;
color: $text-muted;
}
Card.disabled {
opacity: 0.5;
}
.card-rank {
text-style: bold;
text-align: center;
}
.card-suit {
text-align: center;
}
"""
class Selected(Message):
"""Posted when card is selected."""
def __init__(self, card: "Card") -> None:
super().__init__()
self.card = card
class Played(Message):
"""Posted when card is played."""
def __init__(self, card: "Card") -> None:
super().__init__()
self.card = card
face_up = reactive(True)
selectable = reactive(True)
def __init__(
self,
rank: str,
suit: str,
value: int = 0,
face_up: bool = True,
card_id: str | None = None,
) -> None:
super().__init__(id=card_id)
self.rank = rank
self.suit = suit
self.value = value
self.face_up = face_up
def compose(self) -> ComposeResult:
if self.face_up:
yield Label(self.rank, classes="card-rank")
yield Label(self.suit, classes="card-suit")
else:
yield Label("🂠", classes="card-back")
def watch_face_up(self, face_up: bool) -> None:
"""Update display when card is flipped."""
if face_up:
self.remove_class("face-down")
else:
self.add_class("face-down")
# Refresh the card content
self.recompose()
def on_click(self) -> None:
"""Handle card click."""
if self.selectable:
self.post_message(self.Selected(self))
def flip(self) -> None:
"""Flip the card."""
self.face_up = not self.face_up
def play(self) -> None:
"""Play this card."""
if self.selectable:
self.post_message(self.Played(self))
class Hand(Container):
"""Container for a player's hand of cards."""
DEFAULT_CSS = """
Hand {
layout: horizontal;
height: auto;
width: 100%;
align: center middle;
}
Hand > Card {
margin: 0 1;
}
"""
def __init__(self, player_name: str, **kwargs) -> None:
super().__init__(**kwargs)
self.player_name = player_name
def add_card(self, card: Card) -> None:
"""Add a card to this hand."""
self.mount(card)
def remove_card(self, card: Card) -> None:
"""Remove a card from this hand."""
card.remove()
def get_cards(self) -> list[Card]:
"""Get all cards in this hand."""
return list(self.query(Card))
class PlayArea(Container):
"""Central area where cards are played."""
DEFAULT_CSS = """
PlayArea {
height: 1fr;
border: round $primary;
background: $surface;
align: center middle;
}
PlayArea > .play-area-label {
color: $text-muted;
}
"""
def compose(self) -> ComposeResult:
yield Label("Play Area", classes="play-area-label")
class GameState(Static):
"""Display current game state."""
DEFAULT_CSS = """
GameState {
dock: top;
height: 3;
background: $boost;
content-align: center middle;
text-style: bold;
}
"""
current_player = reactive("Player 1")
turn = reactive(1)
def render(self) -> str:
return f"Turn {self.turn} | Current Player: {self.current_player}"
class CardGameApp(App):
"""A card game application."""
CSS_PATH = "app.tcss"
BINDINGS = [
("q", "quit", "Quit"),
("n", "next_turn", "Next Turn"),
("d", "draw_card", "Draw Card"),
("p", "play_selected", "Play Card"),
Binding("space", "toggle_select", "Select/Deselect", show=True),
]
selected_card: Card | None = None
def compose(self) -> ComposeResult:
"""Create child widgets."""
yield Header()
yield GameState(id="game-state")
with Vertical(id="game-container"):
# Opponent's hand (face down)
with Container(id="opponent-area"):
yield Label("Opponent", id="opponent-label")
yield Hand("Opponent", id="opponent-hand")
# Play area
yield PlayArea(id="play-area")
# Player's hand (face up)
with Container(id="player-area"):
yield Hand("Player", id="player-hand")
yield Label("Your Hand", id="player-label")
yield Footer()
def on_mount(self) -> None:
"""Initialize the game when app starts."""
# Deal initial cards (example)
player_hand = self.query_one("#player-hand", Hand)
opponent_hand = self.query_one("#opponent-hand", Hand)
# Example: Deal 5 cards to each player
suits = ["", "", "", ""]
ranks = ["A", "2", "3", "4", "5"]
for i, rank in enumerate(ranks):
# Player cards (face up)
player_hand.add_card(Card(rank, suits[i % 4], face_up=True, card_id=f"player-{i}"))
# Opponent cards (face down)
opponent_hand.add_card(Card(rank, suits[i % 4], face_up=False, card_id=f"opp-{i}"))
def on_card_selected(self, event: Card.Selected) -> None:
"""Handle card selection."""
# Deselect previous
if self.selected_card:
self.selected_card.remove_class("selected")
# Select new card
self.selected_card = event.card
event.card.add_class("selected")
self.notify(f"Selected {event.card.rank} of {event.card.suit}")
def on_card_played(self, event: Card.Played) -> None:
"""Handle card being played."""
play_area = self.query_one("#play-area", PlayArea)
card = event.card
# Remove from hand
hand = card.parent
if isinstance(hand, Hand):
hand.remove_card(card)
# Move to play area
play_area.mount(card)
self.notify(f"Played {card.rank} of {card.suit}")
# Deselect
if self.selected_card == card:
self.selected_card = None
def action_next_turn(self) -> None:
"""Advance to next turn."""
game_state = self.query_one(GameState)
game_state.turn += 1
# Toggle current player
if game_state.current_player == "Player 1":
game_state.current_player = "Player 2"
else:
game_state.current_player = "Player 1"
self.notify(f"Turn {game_state.turn}")
def action_draw_card(self) -> None:
"""Draw a card (example)."""
player_hand = self.query_one("#player-hand", Hand)
# Example: Draw a random card
import random
suits = ["", "", "", ""]
ranks = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
card = Card(random.choice(ranks), random.choice(suits), face_up=True)
player_hand.add_card(card)
self.notify("Drew a card")
def action_play_selected(self) -> None:
"""Play the currently selected card."""
if self.selected_card:
self.selected_card.play()
else:
self.notify("No card selected", severity="warning")
def action_toggle_select(self) -> None:
"""Select/deselect hovered card."""
# This is a simplified version - in practice you'd track the hovered card
if self.selected_card:
self.selected_card.remove_class("selected")
self.selected_card = None
if __name__ == "__main__":
app = CardGameApp()
app.run()

View File

@@ -0,0 +1,51 @@
/* Card Game Template Styles */
Screen {
background: $background;
}
#game-container {
height: 100%;
width: 100%;
layout: vertical;
}
/* Opponent Area */
#opponent-area {
height: auto;
padding: 1 2;
background: $surface;
}
#opponent-label {
text-align: center;
text-style: bold;
color: $text-muted;
}
#opponent-hand {
padding: 1 0;
}
/* Play Area */
#play-area {
height: 1fr;
margin: 1 2;
}
/* Player Area */
#player-area {
height: auto;
padding: 1 2;
background: $boost;
}
#player-label {
text-align: center;
text-style: bold;
color: $text;
}
#player-hand {
padding: 1 0;
}

View File

@@ -0,0 +1,182 @@
# Textual Basics
## App Structure
Every Textual app follows this pattern:
```python
from textual.app import App, ComposeResult
from textual.widgets import Widget
class MyApp(App):
"""Docstring describing the app."""
# Optional: Link to external CSS file
CSS_PATH = "app.tcss"
# Optional: Inline CSS
CSS = """
Screen {
align: center middle;
}
"""
def compose(self) -> ComposeResult:
"""Create child widgets."""
yield Widget()
def on_mount(self) -> None:
"""Called when app is mounted and ready."""
pass
if __name__ == "__main__":
app = MyApp()
app.run()
```
## Compose Method
The `compose()` method yields widgets to add to the app. It's called once during initialization:
```python
def compose(self) -> ComposeResult:
yield Header()
yield ContentWidget()
yield Footer()
```
## Mounting
- `on_mount()`: Called when the app/widget is fully mounted and ready
- `mount()`: Dynamically add widgets after app starts (returns a coroutine)
```python
async def on_key(self) -> None:
# Must await when modifying mounted widgets
await self.mount(NewWidget())
self.query_one(Button).label = "Modified!"
```
## Reactive Attributes
Reactive attributes automatically update the UI when changed:
```python
from textual.reactive import reactive
class Counter(Widget):
count = reactive(0) # Initial value
def watch_count(self, new_value: int) -> None:
"""Called automatically when count changes."""
self.query_one(Label).update(f"Count: {new_value}")
def increment(self) -> None:
self.count += 1 # Triggers watch_count
```
### Reactive with Bindings
Set `bindings=True` to auto-refresh footer bindings when reactive changes:
```python
class MyApp(App):
page = reactive(0, bindings=True)
def check_action(self, action: str, parameters) -> bool | None:
"""Return None to disable action."""
if action == "next" and self.page == MAX_PAGES:
return None # Dims the key in footer
return True
```
## Querying Widgets
Find widgets in the DOM:
```python
# Get one widget (raises if not found)
button = self.query_one(Button)
button = self.query_one("#my-id")
# Get multiple widgets
all_buttons = self.query(Button)
for button in all_buttons:
pass
# Get with CSS selector
widget = self.query_one("#container .special-class")
```
## Messages and Events
### Built-in Events
Handle with `on_<event>` methods:
```python
def on_mount(self) -> None:
"""When mounted."""
pass
def on_key(self, event: events.Key) -> None:
"""Key pressed."""
if event.key == "escape":
self.exit()
```
### Widget Messages
Handle messages from child widgets:
```python
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Button was clicked."""
self.notify(f"Button {event.button.id} clicked!")
def on_input_changed(self, event: Input.Changed) -> None:
"""Input text changed."""
self.value = event.value
```
### Custom Messages
Define custom messages in your widgets:
```python
from textual.message import Message
class MyWidget(Widget):
class ValueChanged(Message):
"""Posted when value changes."""
def __init__(self, value: int) -> None:
super().__init__()
self.value = value
def update_value(self, new_value: int) -> None:
self.value = new_value
self.post_message(self.ValueChanged(new_value))
# Handle in parent
def on_my_widget_value_changed(self, event: MyWidget.ValueChanged) -> None:
self.notify(f"New value: {event.value}")
```
## Preventing Message Propagation
Stop messages from bubbling to parent:
```python
def on_switch_changed(self, event: Switch.Changed) -> None:
event.stop() # Don't propagate to parent
# Handle here
```
## Preventing Reactive Watchers
Temporarily prevent reactive watchers from firing:
```python
with self.prevent(MyWidget.ValueChanged):
self.value = new_value # Won't trigger watch_value or post message
```

View File

@@ -0,0 +1,346 @@
# Interactivity: Events, Bindings, and Actions
## Key Bindings
Define keyboard shortcuts:
```python
from textual.app import App
from textual.binding import Binding
class MyApp(App):
BINDINGS = [
("q", "quit", "Quit"), # key, action, description
("s", "save", "Save"),
("ctrl+c", "copy", "Copy"),
Binding("f1", "help", "Help", show=True, priority=True),
]
def action_save(self) -> None:
"""Actions are methods prefixed with 'action_'."""
self.notify("Saved!")
def action_copy(self) -> None:
self.notify("Copied!")
def action_help(self) -> None:
self.notify("Help content...")
```
### Binding Options
```python
Binding(
key="f1",
action="help",
description="Help",
show=True, # Show in footer (default: True)
priority=True, # Prioritize over widget bindings
)
```
### Dynamic Bindings
Refresh bindings when state changes:
```python
class MyApp(App):
page = reactive(0, bindings=True) # Auto-refresh bindings
def check_action(self, action: str, parameters) -> bool | None:
"""Control action availability."""
if action == "next" and self.page >= MAX_PAGES:
return None # Disables and dims the key
if action == "previous" and self.page == 0:
return None
return True # Enabled
```
Or manually refresh:
```python
def update_state(self):
self.state = "new_state"
self.refresh_bindings() # Update footer
```
## Mouse Events
Handle mouse interactions:
```python
from textual import events
class MyWidget(Widget):
def on_click(self, event: events.Click) -> None:
"""Widget was clicked."""
self.notify(f"Clicked at {event.x}, {event.y}")
def on_mouse_move(self, event: events.MouseMove) -> None:
"""Mouse moved over widget."""
pass
def on_enter(self, event: events.Enter) -> None:
"""Mouse entered widget."""
self.add_class("hover")
def on_leave(self, event: events.Leave) -> None:
"""Mouse left widget."""
self.remove_class("hover")
```
## Keyboard Events
Handle key presses:
```python
from textual import events
class MyApp(App):
def on_key(self, event: events.Key) -> None:
"""Any key pressed."""
if event.key == "escape":
self.exit()
elif event.key == "space":
self.toggle_pause()
def key_r(self, event: events.Key) -> None:
"""Specific key handler (press 'r')."""
self.reset()
```
## Focus Events
Track focus changes:
```python
def on_focus(self, event: events.Focus) -> None:
"""Widget gained focus."""
self.border_title = "Focused"
def on_blur(self, event: events.Blur) -> None:
"""Widget lost focus."""
self.border_title = ""
```
Programmatic focus:
```python
widget.focus() # Give focus to widget
widget.can_focus = True # Enable focusing (default for inputs)
```
## Widget Messages
Handle messages from specific widgets:
```python
from textual.widgets import Button, Input, Switch
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Any button pressed."""
button_id = event.button.id
self.notify(f"Button {button_id} pressed")
def on_input_changed(self, event: Input.Changed) -> None:
"""Input text changed."""
self.update_preview(event.value)
def on_input_submitted(self, event: Input.Submitted) -> None:
"""User pressed Enter in input."""
self.process(event.value)
def on_switch_changed(self, event: Switch.Changed) -> None:
"""Switch toggled."""
self.feature_enabled = event.value
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
"""Row in table selected."""
row_key = event.row_key
```
### Message Naming Convention
Handler method: `on_{widget_type}_{message_name}`
- Converts to snake_case
- Example: `Button.Pressed``on_button_pressed`
- Custom widget: `MyWidget.ValueChanged``on_my_widget_value_changed`
## Custom Messages
Define custom messages for your widgets:
```python
from textual.message import Message
from textual.widget import Widget
class Card(Widget):
class Selected(Message):
"""Posted when card is selected."""
def __init__(self, card_id: str, value: int) -> None:
super().__init__()
self.card_id = card_id
self.value = value
def on_click(self) -> None:
self.post_message(self.Selected(self.id, self.value))
# Handle in parent
def on_card_selected(self, event: Card.Selected) -> None:
self.notify(f"Card {event.card_id} (value: {event.value}) selected")
```
## Message Control
### Stop Propagation
Prevent message from bubbling to parent:
```python
def on_button_pressed(self, event: Button.Pressed) -> None:
event.stop() # Don't propagate to parent
# Handle locally
```
### Prevent Messages
Temporarily suppress messages:
```python
with widget.prevent(Switch.Changed):
widget.value = True # Won't emit Changed message
```
Useful when programmatically updating to avoid infinite loops.
## Actions
Actions are methods that can be triggered by bindings or programmatically:
```python
class MyApp(App):
BINDINGS = [
("n", "next_page", "Next"),
("p", "prev_page", "Previous"),
]
def action_next_page(self) -> None:
self.page += 1
self.refresh_view()
def action_prev_page(self) -> None:
self.page -= 1
self.refresh_view()
```
### Parameterized Actions
Pass parameters to actions:
```python
BINDINGS = [
("r", "add_color('red')", "Red"),
("g", "add_color('green')", "Green"),
("b", "add_color('blue')", "Blue"),
]
def action_add_color(self, color: str) -> None:
self.add_widget(ColorBar(color))
```
### Programmatic Action Calls
```python
self.run_action("save") # Trigger action by name
```
## Notifications
Show temporary messages to user:
```python
self.notify("File saved successfully!")
self.notify("Error occurred", severity="error")
self.notify("Warning!", severity="warning")
self.notify("Info message", severity="information", timeout=5)
```
## Timers
Schedule repeated actions:
```python
def on_mount(self) -> None:
self.set_interval(1.0, self.update_timer) # Every 1 second
def update_timer(self) -> None:
self.elapsed += 1
self.query_one("#timer").update(str(self.elapsed))
```
One-time delayed action:
```python
self.set_timer(2.0, self.delayed_action) # After 2 seconds
def delayed_action(self) -> None:
self.notify("Timer complete!")
```
## Example: Interactive Card Selection
```python
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.widget import Widget
from textual.widgets import Label, Static
from textual.message import Message
class Card(Widget):
DEFAULT_CSS = """
Card {
width: 12;
height: 10;
border: round white;
background: $panel;
}
Card:hover {
background: $boost;
}
Card.selected {
border: double cyan;
background: $accent;
}
"""
class Selected(Message):
def __init__(self, card: "Card") -> None:
super().__init__()
self.card = card
def __init__(self, suit: str, value: str) -> None:
super().__init__()
self.suit = suit
self.value = value
def compose(self) -> ComposeResult:
yield Label(f"{self.value}\n{self.suit}")
def on_click(self) -> None:
self.post_message(self.Selected(self))
class CardGame(App):
def compose(self) -> ComposeResult:
with Horizontal(id="hand"):
yield Card("", "A")
yield Card("", "K")
yield Card("", "Q")
def on_card_selected(self, event: Card.Selected) -> None:
# Deselect all
for card in self.query(Card):
card.remove_class("selected")
# Select clicked
event.card.add_class("selected")
self.notify(f"Selected {event.card.value} of {event.card.suit}")
```

View File

@@ -0,0 +1,298 @@
# Layout and Positioning
## Layout Types
### Vertical (Default)
Stacks widgets vertically:
```css
Container {
layout: vertical;
}
```
### Horizontal
Arranges widgets side-by-side:
```css
Container {
layout: horizontal;
}
```
### Grid
Grid layout with rows and columns:
```css
Grid {
layout: grid;
grid-size: 3 2; /* 3 columns, 2 rows */
grid-gutter: 1 2; /* vertical horizontal spacing */
}
```
#### Grid Cell Spanning
Make widgets span multiple cells:
```css
#header {
column-span: 3; /* Span 3 columns */
}
#sidebar {
row-span: 2; /* Span 2 rows */
}
```
#### Grid Rows and Columns
Define row heights and column widths:
```css
Grid {
grid-size: 2 3;
grid-rows: 1fr 6 25%; /* Flexible, fixed 6, 25% */
grid-columns: 1fr 2fr; /* 1:2 ratio */
}
```
## Alignment
### Screen/Container Alignment
Center content within screen:
```css
Screen {
align: center middle; /* horizontal vertical */
}
```
Options: `left`, `center`, `right` × `top`, `middle`, `bottom`
### Content Alignment
Align content within a widget:
```css
MyWidget {
content-align: center middle;
text-align: center;
}
```
## Docking
Pin widgets to screen edges:
```css
#header {
dock: top;
height: 3;
}
#sidebar {
dock: left;
width: 20;
}
#footer {
dock: bottom;
}
```
Docking order matters - earlier docked widgets take priority.
## Sizing
### Fixed Sizes
```css
Widget {
width: 50; /* 50 cells */
height: 10; /* 10 rows */
}
```
### Relative Sizes
```css
Widget {
width: 50%; /* 50% of parent */
height: 100%;
}
```
### Fractional Units
Share available space proportionally:
```css
#left {
width: 1fr; /* Gets 1 part */
}
#right {
width: 2fr; /* Gets 2 parts (twice as wide) */
}
```
### Auto Sizing
Fit content:
```css
Widget {
width: auto;
height: auto;
}
```
### Min/Max Constraints
```css
Widget {
min-width: 20;
max-width: 80;
min-height: 5;
max-height: 30;
}
```
## Spacing
### Margin
Space outside widget border:
```css
Widget {
margin: 1; /* All sides */
margin: 1 2; /* vertical horizontal */
margin: 1 2 3 4; /* top right bottom left */
}
```
### Padding
Space inside widget border:
```css
Widget {
padding: 1; /* All sides */
padding: 1 2; /* vertical horizontal */
}
```
## Visibility
### Display
Show or hide widgets:
```css
#hidden {
display: none;
}
#visible {
display: block;
}
```
Toggle in Python:
```python
widget.display = False # Hide
widget.display = True # Show
```
### Visibility
Similar to display but reserves space:
```css
Widget {
visibility: hidden; /* Hidden but takes space */
visibility: visible;
}
```
## Layers
Control stacking order:
```css
#background {
layer: below;
}
#popup {
layer: above;
}
```
## Scrolling
### Enable Scrolling
```css
Container {
overflow-x: auto; /* Horizontal scrolling */
overflow-y: auto; /* Vertical scrolling */
overflow: auto auto; /* Both */
}
```
### Programmatic Scrolling
```python
# Scroll to specific position
container.scroll_to(x=0, y=100)
# Scroll widget into view
widget.scroll_visible()
# Scroll to end
self.screen.scroll_end(animate=True)
```
## Example: Card Game Layout
```css
Screen {
layout: vertical;
}
#opponent-hand {
dock: top;
height: 12;
layout: horizontal;
align: center top;
}
#play-area {
height: 1fr;
layout: grid;
grid-size: 5 3;
align: center middle;
}
#player-hand {
dock: bottom;
height: 15;
layout: horizontal;
align: center bottom;
padding: 1;
}
.card {
width: 12;
height: 10;
margin: 0 1;
}
```

View File

@@ -0,0 +1,323 @@
# Styling with CSS
## CSS Files
Link external CSS file:
```python
class MyApp(App):
CSS_PATH = "app.tcss" # Textual CSS file
```
Or inline CSS:
```python
class MyApp(App):
CSS = """
Screen {
background: $background;
}
"""
```
## Selectors
### Type Selectors
Target all widgets of a type:
```css
Button {
width: 100%;
}
Label {
color: cyan;
}
```
### ID Selectors
Target specific widget:
```css
#my-button {
background: red;
}
#header {
dock: top;
}
```
### Class Selectors
Target widgets with specific class:
```css
.card {
border: round white;
padding: 1;
}
.selected {
background: yellow;
}
```
Add classes in Python:
```python
widget = Label("Text", classes="card selected")
# or
widget.add_class("highlighted")
widget.remove_class("selected")
widget.toggle_class("active")
```
### Pseudo-classes
Style based on state:
```css
Button:hover {
background: $accent;
}
Button:focus {
border: double green;
}
Input:disabled {
opacity: 0.5;
}
```
Common pseudo-classes: `:hover`, `:focus`, `:focus-within`, `:disabled`, `:enabled`
### Combinators
```css
/* Direct children */
Container > Label {
color: white;
}
/* Descendants */
Container Label {
margin: 1;
}
/* Class and type */
Label.card {
border: round;
}
```
## Colors
### Named Colors
```css
Widget {
color: red;
background: blue;
border: green;
}
```
### Hex Colors
```css
Widget {
color: #ff0000;
background: #00ff0088; /* With alpha */
}
```
### RGB/RGBA
```css
Widget {
color: rgb(255, 0, 0);
background: rgba(0, 255, 0, 0.5);
}
```
### Theme Variables
Use built-in theme colors:
```css
Widget {
background: $background;
color: $text;
border: $primary;
}
```
Common theme variables:
- `$background` - Main background
- `$surface` - Surface color
- `$panel` - Panel background
- `$boost` - Highlighted background
- `$primary` - Primary accent
- `$secondary` - Secondary accent
- `$accent` - Accent color
- `$text` - Main text color
- `$text-muted` - Muted text
- `$foreground-muted` - Dimmed foreground
## Borders
### Border Styles
```css
Widget {
border: solid red; /* Style and color */
border: round cyan; /* Rounded border */
border: double white; /* Double line */
border: dashed yellow; /* Dashed */
border: heavy green; /* Heavy/thick */
border: tall blue; /* Tall characters */
}
```
### Border Sides
```css
Widget {
border-top: solid red;
border-bottom: round blue;
border-left: double green;
border-right: dashed yellow;
}
```
### Border Title
```css
Widget {
border: round white;
border-title-align: center;
}
```
Set title in Python:
```python
widget.border_title = "My Widget"
```
## Text Styling
### Text Properties
```css
Label {
text-style: bold;
text-style: italic;
text-style: bold italic;
text-style: underline;
text-style: strike;
}
```
### Text Alignment
```css
Static {
text-align: left;
text-align: center;
text-align: right;
}
```
## Keylines
Add separators between grid cells or flex items:
```css
Grid {
keyline: thin green;
keyline: thick $primary;
}
```
Note: Must be on a container with a layout.
## Opacity
```css
Widget {
opacity: 0.5; /* 50% transparent */
opacity: 0; /* Fully transparent */
opacity: 1; /* Fully opaque */
}
```
## Tint
Apply color overlay:
```css
Widget {
tint: rgba(255, 0, 0, 0.3); /* Red tint */
}
```
## Rich Markup
Use Rich markup in text:
```python
label = Label("[bold cyan]Hello[/bold cyan] [red]World[/red]")
label.update("[underline]Updated[/underline]")
```
Common markup:
- `[bold]...[/bold]` - Bold
- `[italic]...[/italic]` - Italic
- `[color]...[/color]` - Colored (e.g., `[red]`, `[#ff0000]`)
- `[underline]...[/underline]` - Underline
- `[strike]...[/strike]` - Strikethrough
- `[link=...]...[/link]` - Link
## Example: Card Styling
```css
.card {
width: 12;
height: 10;
border: round $secondary;
background: $panel;
padding: 1;
content-align: center middle;
}
.card:hover {
background: $boost;
border: heavy $primary;
}
.card.selected {
background: $accent;
border: double $primary;
}
.card.disabled {
opacity: 0.5;
tint: rgba(0, 0, 0, 0.5);
}
.card-title {
text-style: bold;
text-align: center;
color: $text;
}
.card-value {
text-align: center;
color: $text-muted;
}
```

View File

@@ -0,0 +1,241 @@
# Common Widgets
## Display Widgets
### Label / Static
Display static or updatable text:
```python
from textual.widgets import Label, Static
# Label is just an alias for Static
label = Label("Hello World")
static = Static("Initial text")
# Update later
static.update("New text")
static.update("[bold]Rich markup[/bold]")
```
### Placeholder
Useful for prototyping layouts:
```python
from textual.widgets import Placeholder
# Shows widget ID and size info
yield Placeholder("Custom label", id="p1")
yield Placeholder(variant="size") # Shows dimensions
yield Placeholder(variant="text") # Shows placeholder text
```
## Input Widgets
### Button
```python
from textual.widgets import Button
yield Button("Click Me", id="my-button")
yield Button("Disabled", disabled=True)
# Handle click
def on_button_pressed(self, event: Button.Pressed) -> None:
button_id = event.button.id
self.notify(f"{button_id} clicked!")
```
### Input
Single-line text input:
```python
from textual.widgets import Input
yield Input(placeholder="Enter text...", id="name-input")
def on_input_changed(self, event: Input.Changed) -> None:
self.text_value = event.value
def on_input_submitted(self, event: Input.Submitted) -> None:
# User pressed Enter
self.process_input(event.value)
```
### TextArea
Multi-line text editor:
```python
from textual.widgets import TextArea
text_area = TextArea()
text_area.load_text("Initial content")
# Get content
content = text_area.text
```
### Switch
Toggle switch (like checkbox):
```python
from textual.widgets import Switch
yield Switch(value=True) # Initially on
def on_switch_changed(self, event: Switch.Changed) -> None:
is_on = event.value
self.toggle_feature(is_on)
```
## Data Display
### DataTable
Display tabular data:
```python
from textual.widgets import DataTable
table = DataTable()
# Add columns
table.add_columns("Name", "Age", "Country")
# Add rows
table.add_row("Alice", 30, "USA")
table.add_row("Bob", 25, "UK")
# Add row with custom label
from rich.text import Text
label = Text("1", style="bold cyan")
table.add_row("Charlie", 35, "Canada", label=label)
# Configuration
table.zebra_stripes = True # Alternating row colors
table.cursor_type = "row" # "cell", "row", "column", or "none"
table.show_header = True
table.show_row_labels = True
# Handle selection
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
row_key = event.row_key
row_data = table.get_row(row_key)
def on_data_table_cell_selected(self, event: DataTable.CellSelected) -> None:
value = event.value
coordinate = event.coordinate
```
## Layout Containers
### Container
Generic container for grouping widgets:
```python
from textual.containers import Container
with Container(id="sidebar"):
yield Label("Title")
yield Button("Action")
```
### Vertical / Horizontal / VerticalScroll / HorizontalScroll
Directional containers:
```python
from textual.containers import Vertical, Horizontal, VerticalScroll
with Horizontal():
yield Button("Left")
yield Button("Right")
with VerticalScroll():
for i in range(100):
yield Label(f"Item {i}")
```
### Grid
Grid layout container:
```python
from textual.containers import Grid
with Grid(id="my-grid"):
yield Label("A")
yield Label("B")
yield Label("C")
yield Label("D")
# Style in CSS:
# Grid {
# grid-size: 2 2; /* 2 columns, 2 rows */
# }
```
## App Widgets
### Header / Footer
Standard app chrome:
```python
from textual.widgets import Header, Footer
def compose(self) -> ComposeResult:
yield Header()
# ... content ...
yield Footer()
```
Footer automatically shows key bindings defined in BINDINGS.
## Custom Widgets
Create reusable components:
```python
from textual.widget import Widget
from textual.widgets import Label, Button
class Card(Widget):
"""A card widget with title and content."""
DEFAULT_CSS = """
Card {
width: 30;
height: 15;
border: round white;
padding: 1;
}
"""
def __init__(self, title: str, content: str) -> None:
super().__init__()
self.title = title
self.content = content
def compose(self) -> ComposeResult:
yield Label(self.title, classes="card-title")
yield Label(self.content, classes="card-content")
yield Button("Select", id=f"select-{self.title}")
```
### Render Method
For simple custom widgets that just render text:
```python
from textual.widget import Widget
class FizzBuzz(Widget):
def render(self) -> str:
return "FizzBuzz!"
```

View File

@@ -0,0 +1,271 @@
---
name: typst-writer
description: Write correct and idiomatic Typst code for document typesetting. Use when creating or editing Typst (.typ) files, working with Typst markup, or answering questions about Typst syntax and features. Focuses on avoiding common syntax confusion (arrays vs content blocks, proper function definitions, state management).
---
# Typst Writer
This skill provides guidance for writing correct Typst code, with emphasis on avoiding common syntax errors from conflating Typst with other languages.
## Core Principles
1. **Never assume syntax from other languages applies** - Typst has its own semantics, especially for data structures
2. **Verify uncertain syntax** - When unsure, check official documentation
3. **Use idiomatic patterns** - Follow Typst conventions for clean, maintainable code
## Quick Syntax Reference
**Critical distinctions:**
- **Arrays**: `(item1, item2)` - parentheses
- **Dictionaries**: `(key: value, key2: value2)` - parentheses with colons
- **Content blocks**: `[markup content]` - square brackets
- **NO tuples** - Typst only has arrays
**For detailed syntax rules and common patterns**, see [references/syntax.md](references/syntax.md).
## Documentation Resources
### Official Documentation
- **Core language reference**: https://typst.app/docs/reference/
- **Package search**: https://typst.app/universe/search?kind=packages&q=QUERY
- **Template search**: https://typst.app/universe/search?kind=templates&q=QUERY
### When to Consult Documentation
- Uncertain about function signatures or parameters
- Need to verify syntax for less common features
- Looking for built-in functions or methods
- Exploring available packages (e.g., `cetz` for diagrams, `drafting` for margin notes, `tablex` for advanced tables)
**Use WebFetch when needed** to retrieve current documentation for verification.
## Workflow
1. **Before writing**: If syntax is unclear, consult [references/syntax.md](references/syntax.md) or documentation
2. **While writing**:
- Use proper data structure syntax (arrays with `()`, content with `[]`)
- Define functions with `#let name(params) = { ... }`
- Use `context` blocks when accessing state
3. **After writing**: Review for Python/other language syntax leaking in
## Common Mistakes to Avoid
- ❌ Calling things "tuples" (Typst only has arrays)
- ❌ Using `[]` for arrays (use `()` instead)
- ❌ Accessing array elements with `arr[0]` (use `arr.at(0)`)
- ❌ Forgetting `#` prefix for code in markup context
- ❌ Mixing up content blocks `[]` with code blocks `{}`
## Example Workflow
```typst
// Define custom functions for document elements
#let important(body) = {
box(
fill: red.lighten(80%),
stroke: red + 1pt,
inset: 8pt,
body
)
}
// Use state for counters
#let example-counter = state("examples", 1)
#let example(body) = context {
let num = example-counter.get()
important[Example #num: #body]
example-counter.update(x => x + 1)
}
// Arrays for data
#let factions = (
(name: "Merchants", color: blue),
(name: "Artisans", color: green)
)
// Iterate and render
#for faction in factions [
- #text(fill: faction.color, faction.name)
]
```
## Reading contents from a Typst file
Besides compiling, the `typst` CLI command can also run queries against a Typst file with `typst query`, using Typst selectors,
and get the result as JSON.
For instance, `typst query the_document.typ "heading.where(level: 1)" | jq ".[].body.text"` will list all the level-1 section titles present in
the document. Sadly, it will not tell you their exact positions in the file, but Typst file are easy to grep.
See [https://typst.app/docs/reference/introspection/query/#command-line-queries](the online docs about `query`) for more info.
## Package Usage
When needing specialized functionality:
1. Search for packages at https://typst.app/universe/
2. Import with `#import "@preview/package:version"`
3. Consult package documentation for API
**Popular packages**: `cetz` (diagrams), `drafting` (annotations), `tablex` (tables), `codelst` (code listings)
## Working with Templates
Typst Universe hosts many pre-built templates for reports, papers, CVs, presentations, and more. Templates provide complete document styling and structure.
### Finding Templates
- Browse templates: https://typst.app/universe/search/?kind=templates
- Filter by category: report, paper, thesis, cv, etc.
- Check the template's documentation for parameters and examples
### Using a Template
1. Import the template: `#import "@preview/template-name:version": *`
2. Apply it with `#show: template-name.with(param: value, ...)`
3. Consult template documentation for required and optional parameters
**Example:**
```typst
#import "@preview/bubble:0.2.2": *
#show: bubble.with(
title: "My Report",
subtitle: "A detailed analysis",
author: "Your Name",
affiliation: "Your Organization",
date: datetime.today(),
main-color: rgb("#FF6B35"),
color-words: ("important", "key", "critical"),
)
// Your content follows
= Introduction
...
```
**Key differences from packages:**
- Templates typically use `#show:` to apply styling to the entire document
- Packages provide functions/components you call explicitly
- Templates often have a title page and document structure built-in
**Popular templates**: `charged-ieee` (IEEE papers), `bubble` (colorful reports), `modern-cv` (CVs), `polylux` (presentations)
## Bibliographies and Citations
Typst supports citations and bibliographies using BibTeX (.bib) or Hayagriva (.yml) format files.
See [references/bibliography.md](references/bibliography.md)
## Complete Document Structure Patterns
### Academic Paper / Report
```typst
#set document(
title: "Document Title",
author: "Your Name",
date: auto,
)
#set page(
paper: "a4",
margin: (x: 2.5cm, y: 2.5cm),
numbering: "1",
)
#set text(size: 11pt)
#set par(justify: true)
#set heading(numbering: "1.")
// Title page
#align(center)[
#text(size: 24pt, weight: "bold")[Document Title]
#v(1cm)
#text(size: 14pt)[Your Name]
#v(0.5cm)
#datetime.today().display()
]
#pagebreak()
// Table of contents
#outline(title: "Table of Contents", indent: auto)
#pagebreak()
// Main content
= Introduction
Your introduction text...
= Methods
...
= Results
...
= Conclusion
...
#pagebreak()
// Bibliography
#bibliography("refs.bib", title: "References", style: "ieee")
```
### Using Templates (Simpler)
```typst
#import "@preview/charged-ieee:0.1.4": *
#show: ieee.with(
title: "Your Paper Title",
authors: (
(name: "Author One", email: "author1@example.com"),
(name: "Author Two", email: "author2@example.com"),
),
abstract: [
Your abstract text here...
],
index-terms: ("keyword1", "keyword2", "keyword3"),
bibliography: bibliography("refs.bib"),
)
// Content starts immediately
= Introduction
Your paper content...
```
## Troubleshooting
### Missing Font Warnings
If you see "unknown font family" warnings, remove the font specification to use system defaults
**Note**: Font warnings don't prevent compilation; the document will use fallback fonts
### Template/Package Not Found
If import fails with "package not found":
- Verify exact package name and version on Typst Universe
- Check for typos in `@preview/package:version` syntax
- Ensure network connectivity (packages download on first use)
- **Remember**: Typst uses fully qualified imports with specific versions - there's no package cache to update
### Compilation Errors
Common fixes:
- **"expected content, found ..."**: You're using code where markup is expected - wrap in `#{ }` or use proper syntax
- **"expected expression, found ..."**: Missing `#` prefix in markup context
- **"unknown variable"**: Check spelling, ensure imports are correct
- **Array/dictionary errors**: Review syntax - use `()` for both, dictionaries need `key: value`
### Performance Issues
For large documents:
- Use `#include "file.typ"` to split into multiple files
- Compile specific pages: `typst compile doc.typ output.pdf --pages 1-5`
- Profile compilation (experimental): `typst compile --timings doc.typ`

View File

@@ -0,0 +1,57 @@
# Basic Setup
```typst
// In your document, cite with @label
Some text with a citation @smith2023.
Multiple citations @jones2024 @doe2025.
// At the end of your document
#bibliography("references.bib", title: "References", style: "ieee")
```
# Citation Styles
- `"ieee"` - IEEE numeric style [1], [2], [3]
- `"apa"` - APA author-year style (Smith, 2023)
- `"chicago-author-date"` - Chicago author-date style
- `"chicago-notes"` - Chicago notes and bibliography style
- `"mla"` - MLA style
# BibTeX Format (.bib files)
```bibtex
@article{smith2023,
title = {Title of the Article},
author = {Smith, John and Doe, Jane},
journal = {Journal Name},
year = {2023},
volume = {10},
pages = {123--145},
}
@online{webresource,
title = {Web Page Title},
author = {Author Name},
year = {2024},
url = {https://example.com},
urldate = {2024-11-19},
}
@book{doe2025,
title = {Book Title},
author = {Doe, Jane},
publisher = {Publisher Name},
year = {2025},
}
```
# Bibliography Function Parameters
```typst
#bibliography(
"file.bib", // Path to bibliography file
title: "References", // Section title (or none for no title)
style: "ieee", // Citation style
full: false, // Show all entries or only cited ones
)
```

View File

@@ -0,0 +1,154 @@
# Typst Syntax Reference
## Critical Syntax Distinctions
### Data Structures
- **Arrays**: Use `()` parentheses
```typst
#let colors = (red, blue, green)
#let mixed = (1, "text", true)
```
- **Dictionaries**: Use `()` with key-value pairs
```typst
#let config = (name: "value", count: 5)
#let palette = (primary: red, secondary: blue)
```
- **Content blocks**: Use `[]` square brackets
```typst
#let heading = [== My Title]
#let paragraph = [This is some *bold* text]
```
**IMPORTANT**: Typst does NOT have tuples. Only arrays (with parentheses).
### Function Definitions
```typst
// Basic function
#let greet(name) = [Hello, #name!]
// With default parameters
#let box-style(fill: none, stroke: 1pt) = { ... }
// With variadic arguments
#let items(..args) = {
for item in args.pos() { ... }
}
```
### Conditionals and Loops
```typst
// If-else
#if condition {
[true branch]
} else {
[false branch]
}
// For loop
#for item in array {
[Processing #item]
}
```
### String Interpolation
```typst
#let name = "World"
[Hello #name] // Content context
#("Hello " + name) // String concatenation
```
## Common Patterns
### Custom Styling Functions
```typst
#let highlight(color, body) = {
box(fill: color.lighten(80%), inset: 3pt, body)
}
#highlight(red)[Important text]
```
### State Management
```typst
#let counter = state("my-counter", 0)
#counter.update(x => x + 1)
#context counter.get()
```
### Layout Helpers
```typst
// Stack (vertical by default)
#stack(
spacing: 1em,
[First item],
[Second item]
)
// Grid
#grid(
columns: (1fr, 2fr),
rows: auto,
gutter: 10pt,
[Cell 1], [Cell 2]
)
// Box with styling
#box(
fill: gray.lighten(90%),
stroke: 1pt,
inset: 8pt,
radius: 4pt,
[Content]
)
```
### Color Manipulation
```typst
#let base = rgb("#3366ff")
#let lighter = base.lighten(40%)
#let darker = base.darken(20%)
#let transparent = base.transparentize(50%)
#let mixed = red.mix(blue, 30%)
```
## Common Gotchas
1. **Array access**: Use `.at()` method, not `[]`
```typst
#let arr = (1, 2, 3)
#arr.at(0) // Correct
// arr[0] // WRONG - [] is for content
```
2. **Method chaining on arrays**:
```typst
#items.map(x => x * 2).filter(x => x > 5).join(", ")
```
3. **Context blocks**: Required for accessing state
```typst
#context {
let val = my-state.get()
[The value is #val]
}
```
4. **Assignment in code blocks**: Use `let`, not `=` alone
```typst
#{
let x = 5 // Correct
// x = 5 // WRONG
}
```

View File

@@ -0,0 +1,112 @@
---
name: working-with-jj
description: Expert guidance for using JJ (Jujutsu) version control system. Use when working with JJ, whatever the subject. Operations, revsets, templates, TODO commit workflows, debugging change evolution, etc. Covers JJ commands, template system, evolog, operations log, and specific JJ workflows.
---
# JJ (Jujutsu) Version Control Helper
## Core Principles
- **Change IDs** (immutable) vs **Commit IDs** (content-based hashes that change
on edit)
- **Operations log** - every operation can be undone with `jj op restore`
- **No staging area** - working copy auto-snapshots
- **Conflicts don't block** - resolve later
- **Commits are lightweight** - edit freely
## Essential Commands
```bash
jj log -r <revset> # View history
jj evolog -r <rev> --git # Change evolution (with diffs)
jj new <base> # Create revision and edit it
jj new --no-edit <base> # Create without switching (for TODOs)
jj edit <rev> # Switch to editing revision
jj desc -r <rev> -m "text" # Set description
jj diff # Changes in @
jj diff -r <rev> # Changes in revision
jj restore <path> # Discard changes to file
jj restore --from <rev> <path> # Restore from another revision
jj split -r <rev> # Split into multiple commits
jj squash # Squash @ into parent
jj squash --into <dest> # Squash @ into specific revision
jj absorb # Auto-squash into right ancestors
jj rebase -s <src> -d <dest> # Rebase with descendants
jj rebase -r <rev> -d <dest> # Rebase single revision only
```
## Quick Revset Reference
```bash
@, @-, @-- # Working copy, parent, grandparent
::@ # Ancestors
mine() # Your changes
conflict() # Has conflicts
description(substring:"text") # Match description
A | B, A & B, A ~ B # Union, intersection, difference
```
See `references/revsets.md` for comprehensive revset patterns.
## Common Pitfalls
### 1. Use `-r` not `--revisions`
```bash
jj log -r xyz # ✅
jj log --revisions xyz # ❌
```
### 2. Use `--no-edit` for parallel branches
```bash
jj new parent -m "A"; jj new parent -m "B" # ❌ B is child of A!
jj new --no-edit parent -m "A"; jj new --no-edit parent -m "B" # ✅ Both children of parent
```
### 3. Quote revsets in shell
```bash
jj log -r 'description(substring:"[todo]")' # ✅
```
## Scripts
Helper scripts in `scripts/`. Add to PATH or invoke directly.
| Script | Purpose |
| ----------------------------------------- | -------------------------------------- |
| `jj-show-desc [REV]` | Get description only |
| `jj-show-detailed [REV]` | Detailed info with git diff |
| `jj-todo-create <PARENT> <TITLE> [DESC]` | Create TODO (stays on @) |
| `jj-todo-done [NEXT_REV]` | Complete current TODO, start next |
| `jj-flag-update <REV> <TO_FLAG>` | Update status flag (auto-detects current) |
| `jj-find-flagged [FLAG]` | Find flagged revisions |
| `jj-parallel-todos <PARENT> <T1> <T2>...` | Create parallel TODOs |
| `jj-desc-transform <REV> <CMD...>` | Pipe description through command |
| `jj-batch-desc <SED_FILE> <REV...>` | Batch transform descriptions |
| `jj-checkpoint [NAME]` | Record op ID before risky operations |
These scripts are notably useful if you are working using a _"TODO Commit
Workflow"_: see `references/todo-workflow.md` for structured TODO planning,
parallel task DAGs, and AI-assisted workflows.
## Recovery
```bash
jj op log # Find operation before problem
jj op restore <op-id> # Restore to that state
```
## References
- `references/todo-workflow.md` - Structured TODO planning, parallel DAGs, AI
workflows
- `references/revsets.md` - Full revset syntax and patterns
- `references/templates.md` - Template language and custom output
- `references/git-remotes.md` - Bookmarks, push/fetch, remote workflows
- `references/command-syntax.md` - Command flag details
- `references/batch-operations.md` - Complex batch transformations

View File

@@ -0,0 +1,165 @@
# Batch Operations on Multiple Revisions
## Problem
When you need to update descriptions for multiple revisions (e.g., replacing line number references with labels), bash syntax and piping can be tricky.
## Anti-Pattern
```bash
# ❌ This will fail with syntax errors
for rev in unxn mktt stnq; do
jj log -r $rev | sed 's/L123/label/' | jj desc -r $rev --stdin
done
# Issues:
# 1. Missing -n1 --no-graph -T description (gets full log output)
# 2. Unquoted variables ($rev) can break with special chars
# 3. Complex pipes in one-liners are fragile
```
## Pattern 1: Intermediate Files (Recommended)
```bash
# ✅ Robust pattern using temporary files
for rev in unxn mktt stnq rwyq roww; do
# Extract description to file
jj log -r "$rev" -n1 --no-graph -T description > /tmp/desc_${rev}_old.txt
# Transform using sed/awk/etc
sed -f /tmp/replacements.sed /tmp/desc_${rev}_old.txt > /tmp/desc_${rev}_new.txt
# Apply back to revision
jj desc -r "$rev" --stdin < /tmp/desc_${rev}_new.txt
done
echo "✅ All descriptions updated"
```
**Benefits:**
- Each step is visible and debuggable
- Can inspect intermediate files if something goes wrong
- Easy to retry individual revisions
- Works with complex transformations
## Pattern 2: One Command at a Time
```bash
# ✅ Alternative: Sequential approach
jj log -r unxn -n1 --no-graph -T description | \
sed 's/L123/@label/' > /tmp/desc_unxn.txt
jj desc -r unxn --stdin < /tmp/desc_unxn.txt
jj log -r mktt -n1 --no-graph -T description | \
sed 's/L123/@label/' > /tmp/desc_mktt.txt
jj desc -r mktt --stdin < /tmp/desc_mktt.txt
# etc.
```
**Benefits:**
- Even more explicit
- Easy to stop/resume
- Perfect for copy-paste execution
## Pattern 3: Using sed Script File
```bash
# Create reusable sed script
cat > /tmp/replacements.sed << 'EOF'
s/L596-617/@types-de-cartes/g
s/L1242-1253/@carte-eglise/g
s/L659-665/@couts-marche/g
EOF
# Apply to all revisions
for rev in unxn mktt stnq; do
jj log -r "$rev" -n1 --no-graph -T description | \
sed -f /tmp/replacements.sed | \
jj desc -r "$rev" --stdin
done
```
**Benefits:**
- Reusable transformation logic
- Easy to test sed script independently
- Cleaner loop body
## Common Mistakes
### 1. Missing Template Specification
```bash
# ❌ Wrong: gets formatted log output
jj log -r xyz | sed 's/old/new/'
# ✅ Correct: extract just description
jj log -r xyz -n1 --no-graph -T description | sed 's/old/new/'
```
### 2. Unquoted Variables
```bash
# ❌ Breaks with special characters in rev names
for rev in a b c; do
jj log -r $rev # Unquoted
done
# ✅ Always quote
for rev in a b c; do
jj log -r "$rev" # Quoted
done
```
### 3. Fragile One-Liners
```bash
# ❌ Hard to debug, fragile
for rev in a b c; do jj log -r $rev -n1 --no-graph -T description | sed 's/x/y/' | jj desc -r $rev --stdin; done
# ✅ Readable, debuggable
for rev in a b c; do
jj log -r "$rev" -n1 --no-graph -T description | \
sed 's/x/y/' > /tmp/desc_${rev}.txt
jj desc -r "$rev" --stdin < /tmp/desc_${rev}.txt
done
```
## Real-World Example
Replacing all line number references with Typst labels across 10 revisions:
```bash
# 1. Create sed replacement script
cat > /tmp/sed_replacements.txt << 'EOF'
s/5F\.typ L596-617/5F.typ @types-de-cartes/g
s/5F\.typ L1242-1253/5F.typ @carte-eglise-en-pierre/g
s/5F\.typ L659-665/5F.typ @couts-marche/g
# ... etc
EOF
# 2. Process each revision
for rev in unxn mktt stnq rwyq roww wltq syun zkru mszz ovrv; do
jj log -r "$rev" -n1 --no-graph -T description | \
sed -f /tmp/sed_replacements.txt > "/tmp/desc_${rev}_new.txt"
jj desc -r "$rev" --stdin < "/tmp/desc_${rev}_new.txt"
done
# 3. Verify one result
jj log -r mktt -n1 --no-graph -T description | head -5
```
## Verification
Always verify the results after batch operations:
```bash
# Quick check: first line of each description
for rev in unxn mktt stnq; do
echo "=== $rev ==="
jj log -r "$rev" -n1 --no-graph -T description | head -3
done
# Or use jj log with custom template
jj log -r 'unxn | mktt | stnq' -T 'change_id.shortest(4) ++ " " ++ description.first_line() ++ "\n"'
```

View File

@@ -0,0 +1,211 @@
# JJ Command Syntax Reference
## The `-r` Flag Confusion
JJ commands are **inconsistent** with flag naming, which can be confusing:
### Commands Using `-r` (Most Common)
```bash
jj log -r <revset> # ✅ Short form only
jj desc -r <revset> # ✅ Short form only
jj show -r <revset> # ✅ Short form only
jj rebase -r <revset> # ✅ Short form only
jj edit -r <revset> # ✅ Short form only (no --revision)
```
**Rule:** For most commands, use `-r` and **never** `--revisions` or `--revision`.
### Why This Matters
```bash
# ❌ Common mistake: trying long form
jj desc --revisions xyz
# Error: unexpected argument '--revisions' found
jj log --revision xyz
# Error: unexpected argument '--revision' found
# ✅ Always use short form
jj desc -r xyz
jj log -r xyz
```
## Command Patterns
### Reading Revision Info
```bash
# Get description only (for processing)
jj log -r <rev> -n1 --no-graph -T description
# Get detailed info (human-readable)
jj log -r <rev> -n1 --no-graph -T builtin_log_detailed
# Get compact one-liner
jj log -r <rev> -T 'change_id.shortest(4) ++ " " ++ description.first_line()'
```
**Key flags:**
- `-n1`: Limit to 1 revision
- `--no-graph`: No ASCII art graph
- `-T <template>`: Output template
- `-r <revset>`: Which revision(s)
### Modifying Revisions
```bash
# Change description from string
jj desc -r <rev> -m "New description"
# Change description from stdin (for scripts)
echo "New description" | jj desc -r <rev> --stdin
# Change description from file
jj desc -r <rev> --stdin < /path/to/description.txt
# Pipeline pattern
jj log -r <rev> -n1 --no-graph -T description | \
sed 's/old/new/' | \
jj desc -r <rev> --stdin
```
**Key insight:** `--stdin` is essential for scripted modifications.
### Creating Revisions
```bash
# Create and edit immediately (moves @)
jj new <parent> -m "Description"
# Create without editing (@ stays put) - IMPORTANT for parallel branches
jj new --no-edit <parent> -m "Description"
# Create with multiple parents (merge)
jj new --no-edit <parent1> <parent2> -m "Merge point"
```
**Critical distinction:**
- Without `--no-edit`: Your working copy (@) moves to the new revision
- With `--no-edit`: New revision created, but @ stays where it was
## Revset Syntax
### Basic Revsets
```bash
@ # Working copy
<change-id> # Specific revision (e.g., abc, unxn)
<commit-id> # By commit hash
```
### Operators
```bash
<rev>::<rev> # Range (from..to, inclusive)
<rev>.. # All descendants
..<rev> # All ancestors
# Examples
zyxu::@ # All revisions from zyxu to current
roww:: # roww and all its descendants
::@ # All ancestors of @
```
### Functions
```bash
description(glob:"pattern") # Match description
description(exact:"text") # Exact match
mine() # Your commits
```
### Combining
```bash
# Union (OR)
rev1 | rev2
# Intersection (AND)
rev1 & rev2
# Example: Your changes in current branch
mine() & ::@
```
## Shell Quoting
Revsets often need quoting because they contain special characters:
```bash
# ❌ Shell interprets glob
jj log -r description(glob:"[todo]*")
# ✅ Single quotes (safest)
jj log -r 'description(glob:"[todo]*")'
# ✅ Double quotes with escaping
jj log -r "description(glob:\"[todo]*\")"
```
**Rule:** When in doubt, use single quotes around revsets.
## Common Patterns
### Update Multiple Revisions
```bash
# Pattern: Extract → Transform → Apply
for rev in a b c; do
jj log -r "$rev" -n1 --no-graph -T description > /tmp/desc.txt
# ... transform /tmp/desc.txt ...
jj desc -r "$rev" --stdin < /tmp/desc.txt
done
```
### Find and Update
```bash
# Find all [todo] revisions
jj log -r 'description(glob:"[todo]*")'
# Update specific one
jj log -r xyz -n1 --no-graph -T description | \
sed 's/\[todo\]/[wip]/' | \
jj desc -r xyz --stdin
```
### Create Parallel Branches
```bash
# All branch from same parent
parent=xyz
jj new --no-edit "$parent" -m "[todo] Branch A"
jj new --no-edit "$parent" -m "[todo] Branch B"
jj new --no-edit "$parent" -m "[todo] Branch C"
```
## Debugging
```bash
# Did my command work?
jj log -r <rev> -T 'change_id ++ " " ++ description.first_line()'
# View full description
jj log -r <rev> -n1 --no-graph -T description
# Check revision graph
jj log -r '<parent>::' -T builtin_log_compact
```
## Quick Reference Card
| Task | Command |
|------|---------|
| View description | `jj log -r <rev> -n1 --no-graph -T description` |
| Set description | `jj desc -r <rev> -m "text"` |
| Set from stdin | `jj desc -r <rev> --stdin` |
| Create (edit) | `jj new <parent> -m "text"` |
| Create (no edit) | `jj new --no-edit <parent> -m "text"` |
| Range query | `jj log -r '<from>::<to>'` |
| Find pattern | `jj log -r 'description(glob:"pat*")'` |

View File

@@ -0,0 +1,168 @@
# Working with Git Remotes
JJ coexists with Git. The `.git` directory is the source of truth for remotes.
## Basic Workflow
```bash
# 1. Fetch latest from remotes
jj git fetch
# 2. Rebase your work onto updated main
jj rebase -d 'main@origin'
# 3. Make changes...
# 4. Point a bookmark at your work
jj bookmark set my-feature -r @
# 5. Push to remote
jj git push --bookmark my-feature
```
## Bookmarks vs Git Branches
JJ "bookmarks" = Git "branches". They're just named pointers to revisions.
```bash
jj bookmark list # List all bookmarks
jj bookmark create <name> -r <rev> # Create new bookmark
jj bookmark set <name> -r <rev> # Move existing bookmark
jj bookmark delete <name> # Delete bookmark
```
**Key insight:** Unlike Git, you don't need to "be on a branch" to work. Just edit any revision directly.
## Remote Bookmarks
Remote bookmarks have the form `name@remote`:
```bash
main@origin # Remote main on origin
feature@upstream # Remote feature on upstream
```
### Tracking
```bash
jj bookmark track main@origin # Start tracking remote bookmark
jj bookmark untrack main@origin # Stop tracking
```
Tracked bookmarks automatically update on `jj git fetch`.
### Local vs Remote
After fetch, `main` (local) and `main@origin` (remote) may differ:
```bash
# See the difference
jj log -r '::main ~ ::main@origin' # Local commits not in remote
jj log -r '::main@origin ~ ::main' # Remote commits not in local
# Update local to match remote
jj bookmark set main -r 'main@origin'
```
## Pushing
```bash
jj git push --bookmark <name> # Push specific bookmark
jj git push --all # Push all bookmarks
jj git push --change <rev> # Create/push bookmark for revision
```
### Push Errors
**"bookmark moved unexpectedly"**: Someone else pushed. Fetch and rebase:
```bash
jj git fetch
jj rebase -d 'main@origin'
jj git push --bookmark my-feature
```
**"would delete remote bookmark"**: Remote has bookmark you don't. Either:
```bash
jj git push --bookmark <name> --allow-delete # Delete remote
# or
jj bookmark track <name>@origin # Keep tracking it
```
## Fetching
```bash
jj git fetch # Fetch all remotes
jj git fetch --remote origin # Fetch specific remote
```
After fetch, rebase onto updated remote:
```bash
jj rebase -d 'main@origin'
```
## Cloning
```bash
jj git clone <url> [path] # Clone Git repo into JJ
jj git clone --colocate <url> # Colocated: .git + .jj together
```
### Colocated Repos
Colocated repos have both `.git` and `.jj` at the root. Git and JJ see the same history.
```bash
# Convert existing Git repo to colocated JJ
cd existing-git-repo
jj git init --colocate
```
## Import/Export
JJ auto-imports from Git on most operations. Manual control:
```bash
jj git import # Import Git refs → JJ
jj git export # Export JJ bookmarks → Git refs
```
## Common Patterns
### Start feature from latest main
```bash
jj git fetch
jj new 'main@origin' -m "Start feature X"
```
### Rebase feature onto updated main
```bash
jj git fetch
jj rebase -s <feature-root> -d 'main@origin'
```
### Push new feature for review
```bash
jj bookmark create my-feature -r @
jj git push --bookmark my-feature
```
### Update PR after review
```bash
# Make changes...
jj bookmark set my-feature -r @
jj git push --bookmark my-feature
```
### Delete remote branch after merge
```bash
jj bookmark delete my-feature
jj git push --bookmark my-feature --allow-delete
```

View File

@@ -0,0 +1,143 @@
# Revsets Reference
Revsets are JJ's query language for selecting revisions.
```bash
jj help -k revsets # Official documentation
```
## Basic Selectors
```bash
@ # Working copy
@- # Parent of @
@-- # Grandparent
root() # Root commit (empty ancestor)
<change-id> # By change ID (e.g., abc, xyzmno)
<commit-id> # By commit hash
```
## Ancestry Operators
```bash
::@ # All ancestors of @ (inclusive)
@:: # All descendants of @ (inclusive)
@-:: # Descendants of parent (siblings and their children)
<from>::<to> # Range from..to (inclusive both ends)
# Exclusive variants
@- # Immediate parents only
@+ # Immediate children only
```
## Filter Functions
```bash
mine() # Your changes (by author)
heads(all()) # All head revisions (no children)
roots(<revset>) # Roots of given revset
empty() # Empty revisions (no diff)
conflict() # Revisions with unresolved conflicts
immutable() # Immutable revisions (usually main, tags)
mutable() # Mutable revisions
# Text matching
description(substring:"text") # Match in description
description(exact:"text") # Exact description match
author(substring:"name") # Match author name/email
committer(substring:"name") # Match committer
# File-based
file("path/to/file") # Revisions that modified this file
file("glob:src/*.rs") # Glob pattern matching
```
## Set Operations
```bash
A | B # Union: revisions in A OR B
A & B # Intersection: revisions in A AND B
A ~ B # Difference: revisions in A but NOT in B
~A # Complement: all revisions NOT in A
```
## Useful Patterns
### Working with branches
```bash
# Your work on current line
mine() & ::@
# What's on this branch but not in main
::@ ~ ::main
# Heads of your work (tips)
heads(mine())
# All your unmerged work
mine() ~ ::main
```
### Finding specific changes
```bash
# Changes to a specific file
file("src/lib.rs")
# Your changes to src/ directory
file("src/") & mine()
# Empty TODO commits
empty() & description(substring:"[todo]")
# Commits with conflicts
conflict()
```
### Navigation
```bash
# Recent commits (last 10 by default in log)
@ | @- | @-- | @---
# All siblings (same parent as @)
@-:: ~ @::
# Common ancestor of two revisions
heads(::A & ::B)
```
### Remote tracking
```bash
# Remote main
main@origin
# What's in remote but not local
::main@origin ~ ::main
# What's local but not pushed
::main ~ ::main@origin
```
## Quoting in Shell
Revsets with special characters need shell quoting:
```bash
# ❌ Shell interprets parentheses and quotes
jj log -r description(substring:"[todo]")
# ✅ Single quotes protect everything
jj log -r 'description(substring:"[todo]")'
# ✅ Double quotes with escaping
jj log -r "description(substring:\"[todo]\")"
# ✅ Simple revsets don't need quotes
jj log -r mine
jj log -r @-
```
**Rule:** When in doubt, wrap the entire revset in single quotes.

View File

@@ -0,0 +1,177 @@
# Templates Reference
Templates control JJ's output formatting.
```bash
jj help -k templates # Official documentation
```
## Built-in Templates
```bash
# For jj log
builtin_log_compact # Default compact view
builtin_log_detailed # Full details with diff
builtin_log_oneline # Single line per revision
# For jj evolog
builtin_evolog_compact # Default evolution log
# For jj op log
builtin_op_log_compact # Operation log
```
## Context Separation
**Critical:** Different commands have different template contexts.
### Log Templates (revision context)
```bash
# Direct access to revision properties
change_id # Full change ID
change_id.short() # Short form (default 12 chars)
change_id.shortest() # Shortest unique prefix
commit_id # Git commit hash
description # Full description
description.first_line() # First line only
author # Author info
author.name() # Author name
author.email() # Author email
author.timestamp() # Author timestamp
committer # Committer info (same methods)
empty # Boolean: is empty?
conflict # Boolean: has conflicts?
```
### Evolog Templates (commit context)
```bash
# Must go through commit object
commit.change_id()
commit.commit_id()
commit.description()
commit.author()
# etc.
```
### Op Log Templates (operation context)
```bash
self.id() # Operation ID
self.id().short(12) # Short operation ID
self.description() # What the operation did
self.time() # When it happened
self.user() # Who did it
```
## Template Language
### String Concatenation
```bash
# Use ++ to join strings
change_id.shortest(8) ++ " " ++ description.first_line()
```
### Conditionals
```bash
# if(condition, then, else)
if(conflict, "⚠️ ", "")
if(empty, "(empty)", description.first_line())
```
### Methods
```bash
# Strings
description.first_line()
description.lines() # List of lines
"text".contains("x")
"text".starts_with("x")
# IDs
change_id.short() # Default length
change_id.short(6) # Specific length
change_id.shortest() # Minimum unique
change_id.shortest(4) # Minimum 4 chars
# Timestamps
timestamp.ago() # "2 hours ago"
timestamp.format("%Y-%m-%d") # Custom format
```
### Special Output
```bash
# JSON output (for scripting)
jj log -T "json(self)"
# Diff statistics
diff.stat(72) # Stat with max width
# Labels for coloring
label("keyword", "text")
```
## Useful Custom Templates
### Compact one-liner
```bash
jj log -T 'change_id.shortest(8) ++ " " ++ description.first_line() ++ "\n"'
```
### With status indicators
```bash
jj log -T '
change_id.shortest(8)
++ if(conflict, " ⚠️", "")
++ if(empty, " ∅", "")
++ " " ++ description.first_line()
++ "\n"
'
```
### Files changed
```bash
jj log -T 'change_id.shortest(8) ++ "\n" ++ diff.stat(72)'
```
### For scripting (parseable)
```bash
# Tab-separated
jj log -T 'change_id.short() ++ "\t" ++ description.first_line() ++ "\n"' --no-graph
# JSON
jj log -T "json(self)" --no-graph
```
### Operation IDs for checkpoints
```bash
jj op log -T 'self.id().short(12) ++ " " ++ self.description() ++ "\n"' --no-graph -n5
```
## Config File Templates
Define reusable templates in `~/.jjconfig.toml`:
```toml
[templates]
log = 'change_id.shortest(8) ++ " " ++ description.first_line()'
[template-aliases]
'format_short_id(id)' = 'id.shortest(8)'
```
Then use with:
```bash
jj log -T log
# or reference alias in other templates
```

View File

@@ -0,0 +1,294 @@
# TODO Commit Workflow
Empty revisions as TODO markers enable structured development with clear milestones. Descriptions act as specifications for what to implement.
## Core Concept
```bash
# Create empty TODO (stays on current @)
jj-todo-create @ "Feature X" "Detailed specs of what to implement"
# Later, work on it
jj edit <todo-change-id>
# Update status as you progress
jj-flag-update @ wip
```
## Status Flags
Use description prefixes to track status at a glance:
| Flag | Meaning |
|------|---------|
| `[todo]` | Not started, empty revision |
| `[wip]` | Work in progress |
| `[untested]` | Implementation done, tests missing |
| `[broken]` | Tests failing, needs fixing |
| `[review]` | Needs review (tricky code, design choice) |
| (none) | Complete |
### Updating Flags
```bash
# Using script (auto-detects current flag)
jj-flag-update @ wip
jj-flag-update @ untested
jj-flag-update @ done # "done" removes the flag
# Manual (what the script does)
jj log -r @ -n1 --no-graph -T description | sed 's/\[todo\]/[wip]/' | jj desc -r @ --stdin
```
### Finding Flagged Revisions
```bash
jj-find-flagged # All flagged
jj-find-flagged todo # Only [todo]
jj-find-flagged wip # Only [wip]
# Manual
jj log -r 'description(substring:"[todo]")'
```
## Basic Workflow
### 1. Plan: Create TODO Chain
```bash
# Create linear chain of tasks
jj-todo-create @ "Task 1: Setup data model"
jj-todo-create <T1-id> "Task 2: Implement core logic"
jj-todo-create <T2-id> "Task 3: Add API endpoints"
jj-todo-create <T3-id> "Task 4: Write tests"
```
### 2. Work: Edit Each TODO
```bash
# Read the specs
jj-show-desc <task-id>
# Start working on it
jj edit <task-id>
jj-flag-update @ wip
# ... implement ...
# Mark progress
jj-flag-update @ untested
```
### 3. Complete and Move to Next
```bash
# After validation passes, complete current and start next TODO
jj-todo-done
# If there are multiple next TODOs (parallel branches), it will list them:
# Multiple next TODOs available. Choose one:
# abc123 [todo] Widget A
# def456 [todo] Widget B
#
# Then specify which one:
jj-todo-done abc123
```
The script handles the full transition: marks current as done, edits the next revision, sets it to `[wip]`, and prints its description so you can start working.
## Parallel Tasks (DAG)
Create branches that can be worked independently:
```bash
# Linear foundation
jj-todo-create @ "Task 1: Core infrastructure"
jj-todo-create <T1-id> "Task 2: Base components"
# Parallel branches from Task 2
jj-parallel-todos <T2-id> "Widget A" "Widget B" "Widget C"
# Merge point (all three must complete first)
jj new --no-edit <A-id> <B-id> <C-id> -m "[todo] Integration"
```
**Result:**
```
[todo] Integration
/ | \
Widget A Widget B Widget C
\ | /
Task 2: Base
|
Task 1: Core
```
No rebasing needed - parents specified directly!
## Writing Good TODO Descriptions
### Structure
```
[todo] Short title (< 50 chars)
## Context
Why this task exists, what problem it solves.
## Requirements
- Specific requirement 1
- Specific requirement 2
## Implementation notes
Any hints, constraints, or approaches to consider.
## Acceptance criteria
How to know when this is done.
```
### Example
```
[todo] Implement user authentication
## Context
Users need to log in to access their data. Using JWT tokens
for stateless auth.
## Requirements
- POST /auth/login accepts email + password
- Returns JWT token valid for 24h
- POST /auth/refresh extends token
- Invalid credentials return 401
## Implementation notes
- Use bcrypt for password hashing
- Store refresh tokens in Redis
- See auth.md for token format spec
## Acceptance criteria
- All auth endpoints return correct status codes
- Tokens expire correctly
- Rate limiting prevents brute force
```
## Documenting Implementation Deviations
When implementation differs from specs, document it:
```bash
# After implementing, add implementation notes
jj desc -r @ -m "$(jj-show-desc @)
## Implementation
- Used argon2 instead of bcrypt (more secure)
- Added /auth/logout endpoint (not in original spec)
- Rate limit: 5 attempts per minute (was unspecified)
"
```
This creates an audit trail of decisions.
## AI-Assisted TODO Workflow
TODOs work great with AI assistants:
### Setup Phase (Human)
```bash
# Human creates the plan
jj-todo-create @ "Refactor auth module" "
## Requirements
- Extract auth logic from handlers
- Create AuthService class
- Add unit tests
- Update API docs
"
```
### Execution Phase (AI)
```bash
# AI reads the task
jj-show-desc <todo-id>
# AI checkpoints before starting
jj-checkpoint "before-auth-refactor"
# AI edits the revision
jj edit <todo-id>
jj-flag-update @ wip
# ... AI implements ...
# AI marks complete
jj-flag-update @ untested
```
### Review Phase (Human)
```bash
# Human reviews what AI did
jj evolog -r <todo-id> --git
# If bad, restore checkpoint
jj op restore <checkpoint-op-id>
# If good but needs splitting
jj split -r <todo-id>
```
## Tips
### Keep TODOs Small
Each TODO should be completable in one focused session. If it's too big, split into multiple TODOs.
### Use `--no-edit` Religiously
When creating TODOs, always use `jj-todo-create` or `jj new --no-edit`. Otherwise @ moves and you lose your place.
### Validate Between Steps
After completing each TODO, run your project's validation (typecheck, lint, tests) before moving to the next:
```bash
# Verify current work (use your project's commands)
make check # or: cargo build, pnpm tsc, go build, etc.
# Then complete and move to next
jj-todo-done
```
This catches errors early when context is fresh, rather than debugging cascading failures at the end.
### Watch for Hidden Dependencies
When planning TODOs that touch service/module layers (especially with dependency injection), dependencies between components may not be obvious until you validate. A component might require a service you're modifying or replacing.
If a later TODO fails due to missing dependencies from an earlier one, don't forget to edit the description to make clear the extra work you had to do which wasn't in the specs.
The upfront planning helps surface these, but some will only appear at validation time.
### Check DAG Before Starting
```bash
# Visualize the plan
jj log -r '<first-todo>::'
```
### Reorder if Needed
If you realize task order is wrong:
```bash
# Move Task B to be after Task C instead of Task A
jj rebase -r <B-id> -d <C-id>
```
### Abandon Obsolete TODOs
```bash
# If a TODO is no longer needed
jj abandon <todo-id>
```

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Apply transformation to multiple revisions
# Usage: jj-batch-desc <SED_SCRIPT_FILE> <REV1> [REV2...]
# Example: jj-batch-desc /tmp/replacements.sed abc xyz mno
set -euo pipefail
if [[ $# -lt 2 ]]; then
echo "Usage: jj-batch-desc <SED_SCRIPT_FILE> <REV1> [REV2...]" >&2
exit 1
fi
sed_script="$1"
shift
if [[ ! -f "$sed_script" ]]; then
echo "Error: sed script not found: $sed_script" >&2
exit 1
fi
for rev in "$@"; do
echo "Processing $rev..."
tmpfile="/tmp/jj_desc_${rev}_$$.txt"
jj log -r "$rev" -n1 --no-graph -T description > "$tmpfile"
sed -f "$sed_script" "$tmpfile" | jj desc -r "$rev" --stdin
rm -f "$tmpfile"
done
echo "✅ Processed $# revision(s)"

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Create a named checkpoint before risky operations
# Usage: jj-checkpoint [NAME]
# Later restore with: jj op restore <op-id>
# NAME defaults to "checkpoint"
set -euo pipefail
name="${1:-checkpoint}"
# Get current operation ID
op_id=$(jj op log -n1 --no-graph -T 'self.id().short(12)')
echo "📍 Checkpoint '$name' at operation: $op_id"
echo " Restore with: jj op restore $op_id"
echo ""
echo " Current state:"
jj log -r @ -n1 -T 'change_id.shortest(8) ++ " " ++ description.first_line()'

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Transform revision description through a command
# Usage: jj-desc-transform <REV> <COMMAND...>
# Example: jj-desc-transform @ sed 's/foo/bar/'
# Example: jj-desc-transform mxyz awk '/^##/{print; next} {print " "$0}'
set -euo pipefail
if [[ $# -lt 2 ]]; then
echo "Usage: jj-desc-transform <REV> <COMMAND...>" >&2
exit 1
fi
rev="$1"
shift
jj log -r "$rev" -n1 --no-graph -T description | "$@" | jj desc -r "$rev" --stdin

View File

@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# Find revisions with status flags
# Usage: jj-find-flagged [FLAG]
# FLAG: todo, wip, untested, broken, review (omit for all flagged)
set -euo pipefail
flag="${1:-}"
if [[ -n "$flag" ]]; then
# Use substring match instead of glob for specific flag
jj log -r "description(substring:\"[${flag}]\")"
else
# All flagged revisions - match common flags
jj log -r 'description(substring:"[todo]") | description(substring:"[wip]") | description(substring:"[untested]") | description(substring:"[broken]") | description(substring:"[review]")'
fi

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Update status flag in revision description
# Usage: jj-flag-update <REV> <TO_FLAG>
# TO_FLAG is: todo, wip, untested, broken, review, or "done" (removes flag)
# Example: jj-flag-update @ wip
# Example: jj-flag-update mxyz done
#
# Automatically detects the current flag and replaces it.
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "Usage: jj-flag-update <REV> <TO_FLAG>" >&2
echo "Flags: todo, wip, untested, broken, review, done (done removes flag)" >&2
exit 1
fi
rev="$1"
to_flag="$2"
# Get current description
desc=$(jj log -r "$rev" -n1 --no-graph -T description)
# Detect current flag
current_flag=""
for flag in todo wip untested broken review; do
if [[ "$desc" =~ ^\[${flag}\] ]]; then
current_flag="$flag"
break
fi
done
if [[ -z "$current_flag" ]]; then
if [[ "$to_flag" == "done" ]]; then
# No flag to remove, nothing to do
exit 0
else
# No current flag - prepend the new one
echo "[${to_flag}] ${desc}" | jj desc -r "$rev" --stdin
exit 0
fi
fi
# Build sed pattern
if [[ "$to_flag" == "done" ]]; then
# Remove the flag (and trailing space)
sed_pattern="s/\[${current_flag}\] //"
else
sed_pattern="s/\[${current_flag}\]/[${to_flag}]/"
fi
echo "$desc" | sed "$sed_pattern" | jj desc -r "$rev" --stdin

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Create multiple parallel TODO branches from same parent
# Usage: jj-parallel-todos <PARENT> <TITLE1> <TITLE2> [TITLE3...]
# Example: jj-parallel-todos @ "Widget A" "Widget B" "Widget C"
set -euo pipefail
if [[ $# -lt 2 ]]; then
echo "Usage: jj-parallel-todos <PARENT> <TITLE1> <TITLE2> [TITLE3...]" >&2
exit 1
fi
parent="$1"
shift
for title in "$@"; do
jj new --no-edit "$parent" -m "[todo] ${title}"
done
echo "✅ Created $# parallel TODO branches from $parent"

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Get revision description only (for reading or piping)
# Usage: jj-show-desc [REV]
# REV defaults to @
set -euo pipefail
rev="${1:-@}"
jj log -r "$rev" -n1 --no-graph -T description

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Show detailed revision info with diff
# Usage: jj-show-detailed [REV]
# REV defaults to @
set -euo pipefail
rev="${1:-@}"
jj log -r "$rev" -n1 --no-graph -T builtin_log_detailed --git

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Create empty TODO revision without editing (stays on current @)
# Usage: jj-todo-create <PARENT> <TITLE> [DESCRIPTION]
# Example: jj-todo-create @ "implement feature X" "detailed specs here"
set -euo pipefail
if [[ $# -lt 2 ]]; then
echo "Usage: jj-todo-create <PARENT> <TITLE> [DESCRIPTION]" >&2
exit 1
fi
parent="$1"
title="$2"
description="${3:-}"
if [[ -n "$description" ]]; then
msg="[todo] ${title}
${description}"
else
msg="[todo] ${title}"
fi
jj new --no-edit "$parent" -m "$msg"

View File

@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# Complete current TODO and optionally start the next one
# Usage: jj-todo-done [NEXT_REV]
#
# If NEXT_REV is provided, starts working on that revision.
# If not provided, lists possible next TODOs (children of current) and exits.
#
# Example:
# jj-todo-done # Complete current, show next options
# jj-todo-done abc123 # Complete current, start working on abc123
set -euo pipefail
SCRIPTS_DIR="$(dirname "$0")"
# Mark current as done (auto-detects current flag)
"$SCRIPTS_DIR/jj-flag-update" @ done
current=$(jj log -r @ --no-graph -T 'change_id.shortest(8)')
# Find children that are TODOs (have any flag)
children=$(jj log -r "children(@) & description(substring:\"[todo]\")" --no-graph -T 'change_id.shortest(8) ++ "\n"' 2>/dev/null || true)
if [[ -n "${1:-}" ]]; then
# User specified next revision
next="$1"
jj edit "$next"
"$SCRIPTS_DIR/jj-flag-update" @ wip
echo ""
echo "📋 Starting TODO:"
echo "─────────────────"
"$SCRIPTS_DIR/jj-show-desc" @
elif [[ -z "$children" ]]; then
echo "✅ Completed: $current"
echo ""
echo "No pending TODO children found. You may be done with this chain!"
echo ""
echo "Check remaining TODOs with:"
echo " jj-find-flagged todo"
elif [[ $(echo "$children" | wc -l) -eq 1 ]]; then
# Single child - start it automatically
next=$(echo "$children" | tr -d '[:space:]')
echo "✅ Completed: $current"
echo ""
jj edit "$next"
"$SCRIPTS_DIR/jj-flag-update" @ wip
echo ""
echo "📋 Starting TODO:"
echo "─────────────────"
"$SCRIPTS_DIR/jj-show-desc" @
else
# Multiple children - list them for user to choose
echo "✅ Completed: $current"
echo ""
echo "Multiple next TODOs available. Choose one:"
echo ""
while IFS= read -r child; do
[[ -z "$child" ]] && continue
title=$(jj log -r "$child" --no-graph -T 'description.first_line()' 2>/dev/null || echo "(no description)")
echo " $child $title"
done <<< "$children"
echo ""
echo "Run: jj-todo-done <change-id>"
fi