Initial commit
This commit is contained in:
81
skills/github-pr-workflow/SKILL.md
Normal file
81
skills/github-pr-workflow/SKILL.md
Normal 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>`
|
||||
23
skills/github-pr-workflow/scripts/gh-pr-checks
Executable file
23
skills/github-pr-workflow/scripts/gh-pr-checks
Executable 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)"'
|
||||
28
skills/github-pr-workflow/scripts/gh-pr-review-comments
Executable file
28
skills/github-pr-workflow/scripts/gh-pr-review-comments
Executable 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)
|
||||
"'
|
||||
21
skills/github-pr-workflow/scripts/gh-pr-reviews
Executable file
21
skills/github-pr-workflow/scripts/gh-pr-reviews
Executable 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)"'
|
||||
27
skills/github-pr-workflow/scripts/gh-pr-summary
Executable file
27
skills/github-pr-workflow/scripts/gh-pr-summary
Executable 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)")"'
|
||||
590
skills/package-npm-nix/SKILL.md
Normal file
590
skills/package-npm-nix/SKILL.md
Normal 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>
|
||||
175
skills/textual-builder/SKILL.md
Normal file
175
skills/textual-builder/SKILL.md
Normal 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)
|
||||
77
skills/textual-builder/assets/card-game-template/README.md
Normal file
77
skills/textual-builder/assets/card-game-template/README.md
Normal 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
|
||||
```
|
||||
324
skills/textual-builder/assets/card-game-template/app.py
Normal file
324
skills/textual-builder/assets/card-game-template/app.py
Normal 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()
|
||||
51
skills/textual-builder/assets/card-game-template/app.tcss
Normal file
51
skills/textual-builder/assets/card-game-template/app.tcss
Normal 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;
|
||||
}
|
||||
182
skills/textual-builder/references/basics.md
Normal file
182
skills/textual-builder/references/basics.md
Normal 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
|
||||
```
|
||||
346
skills/textual-builder/references/interactivity.md
Normal file
346
skills/textual-builder/references/interactivity.md
Normal 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}")
|
||||
```
|
||||
298
skills/textual-builder/references/layout.md
Normal file
298
skills/textual-builder/references/layout.md
Normal 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;
|
||||
}
|
||||
```
|
||||
323
skills/textual-builder/references/styling.md
Normal file
323
skills/textual-builder/references/styling.md
Normal 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;
|
||||
}
|
||||
```
|
||||
241
skills/textual-builder/references/widgets.md
Normal file
241
skills/textual-builder/references/widgets.md
Normal 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!"
|
||||
```
|
||||
271
skills/typst-writer/SKILL.md
Normal file
271
skills/typst-writer/SKILL.md
Normal 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`
|
||||
57
skills/typst-writer/references/bibliography.md
Normal file
57
skills/typst-writer/references/bibliography.md
Normal 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
|
||||
)
|
||||
```
|
||||
154
skills/typst-writer/references/syntax.md
Normal file
154
skills/typst-writer/references/syntax.md
Normal 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
|
||||
}
|
||||
```
|
||||
112
skills/working-with-jj/SKILL.md
Normal file
112
skills/working-with-jj/SKILL.md
Normal 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
|
||||
165
skills/working-with-jj/references/batch-operations.md
Normal file
165
skills/working-with-jj/references/batch-operations.md
Normal 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"'
|
||||
```
|
||||
211
skills/working-with-jj/references/command-syntax.md
Normal file
211
skills/working-with-jj/references/command-syntax.md
Normal 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*")'` |
|
||||
168
skills/working-with-jj/references/git-remotes.md
Normal file
168
skills/working-with-jj/references/git-remotes.md
Normal 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
|
||||
```
|
||||
143
skills/working-with-jj/references/revsets.md
Normal file
143
skills/working-with-jj/references/revsets.md
Normal 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.
|
||||
177
skills/working-with-jj/references/templates.md
Normal file
177
skills/working-with-jj/references/templates.md
Normal 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
|
||||
```
|
||||
294
skills/working-with-jj/references/todo-workflow.md
Normal file
294
skills/working-with-jj/references/todo-workflow.md
Normal 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>
|
||||
```
|
||||
29
skills/working-with-jj/scripts/jj-batch-desc
Executable file
29
skills/working-with-jj/scripts/jj-batch-desc
Executable 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)"
|
||||
18
skills/working-with-jj/scripts/jj-checkpoint
Normal file
18
skills/working-with-jj/scripts/jj-checkpoint
Normal 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()'
|
||||
17
skills/working-with-jj/scripts/jj-desc-transform
Executable file
17
skills/working-with-jj/scripts/jj-desc-transform
Executable 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
|
||||
16
skills/working-with-jj/scripts/jj-find-flagged
Executable file
16
skills/working-with-jj/scripts/jj-find-flagged
Executable 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
|
||||
52
skills/working-with-jj/scripts/jj-flag-update
Executable file
52
skills/working-with-jj/scripts/jj-flag-update
Executable 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
|
||||
20
skills/working-with-jj/scripts/jj-parallel-todos
Executable file
20
skills/working-with-jj/scripts/jj-parallel-todos
Executable 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"
|
||||
9
skills/working-with-jj/scripts/jj-show-desc
Executable file
9
skills/working-with-jj/scripts/jj-show-desc
Executable 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
|
||||
9
skills/working-with-jj/scripts/jj-show-detailed
Executable file
9
skills/working-with-jj/scripts/jj-show-detailed
Executable 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
|
||||
25
skills/working-with-jj/scripts/jj-todo-create
Executable file
25
skills/working-with-jj/scripts/jj-todo-create
Executable 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"
|
||||
64
skills/working-with-jj/scripts/jj-todo-done
Executable file
64
skills/working-with-jj/scripts/jj-todo-done
Executable 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
|
||||
Reference in New Issue
Block a user