gamr 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gamr-0.1.1/.bandit +3 -0
- gamr-0.1.1/.gitignore +10 -0
- gamr-0.1.1/.pre-commit-config.yaml +31 -0
- gamr-0.1.1/.python-version +1 -0
- gamr-0.1.1/CHANGELOG.md +18 -0
- gamr-0.1.1/LICENSE +21 -0
- gamr-0.1.1/Makefile +145 -0
- gamr-0.1.1/PKG-INFO +12 -0
- gamr-0.1.1/README.md +99 -0
- gamr-0.1.1/docs/UI_DESIGN.md +151 -0
- gamr-0.1.1/docs/adrs/001-use-textual-framework.md +28 -0
- gamr-0.1.1/docs/adrs/002-pure-python-git-dulwich.md +33 -0
- gamr-0.1.1/docs/adrs/003-watchdog-polling-fallback.md +28 -0
- gamr-0.1.1/docs/adrs/004-datatable-tree-semantics.md +32 -0
- gamr-0.1.1/docs/adrs/005-thread-workers-for-git-ops.md +37 -0
- gamr-0.1.1/docs/adrs/006-git-provider-abc.md +24 -0
- gamr-0.1.1/docs/adrs/007-rapidfuzz-fuzzy-matching.md +27 -0
- gamr-0.1.1/docs/adrs/008-three-view-modes.md +31 -0
- gamr-0.1.1/docs/adrs/009-deferred-expensive-computations.md +28 -0
- gamr-0.1.1/docs/adrs/010-types-in-models.md +28 -0
- gamr-0.1.1/docs/adrs/011-gradient-colors.md +31 -0
- gamr-0.1.1/docs/adrs/012-lsd-icons-config.md +29 -0
- gamr-0.1.1/docs/adrs/013-spaced-paths.md +21 -0
- gamr-0.1.1/docs/adrs/014-preserve-state-on-watch.md +28 -0
- gamr-0.1.1/docs/adrs/015-persistent-state.md +36 -0
- gamr-0.1.1/docs/adrs/016-folder-navigation.md +32 -0
- gamr-0.1.1/docs/adrs/017-three-diff-modes.md +33 -0
- gamr-0.1.1/docs/adrs/018-diff-overview-bar.md +37 -0
- gamr-0.1.1/docs/adrs/019-incremental-table-updates.md +53 -0
- gamr-0.1.1/docs/adrs/020-widgets-presentational.md +39 -0
- gamr-0.1.1/docs/adrs/021-gutter-diff-mode.md +25 -0
- gamr-0.1.1/docs/adrs/022-preview-stability.md +35 -0
- gamr-0.1.1/docs/adrs/023-copy-file-reference.md +34 -0
- gamr-0.1.1/docs/adrs/README.md +27 -0
- gamr-0.1.1/docs/design-twisty-click-target.md +79 -0
- gamr-0.1.1/pyproject.toml +50 -0
- gamr-0.1.1/scripts/chooser.py +96 -0
- gamr-0.1.1/scripts/lint.sh +14 -0
- gamr-0.1.1/scripts/release.sh +83 -0
- gamr-0.1.1/src/gamr/__init__.py +3 -0
- gamr-0.1.1/src/gamr/app.py +498 -0
- gamr-0.1.1/src/gamr/commands.py +52 -0
- gamr-0.1.1/src/gamr/config.py +33 -0
- gamr-0.1.1/src/gamr/gamr.tcss +13 -0
- gamr-0.1.1/src/gamr/models.py +59 -0
- gamr-0.1.1/src/gamr/py.typed +0 -0
- gamr-0.1.1/src/gamr/services/__init__.py +3 -0
- gamr-0.1.1/src/gamr/services/diff_parser.py +76 -0
- gamr-0.1.1/src/gamr/services/file_index.py +74 -0
- gamr-0.1.1/src/gamr/services/file_scanner.py +209 -0
- gamr-0.1.1/src/gamr/services/filter.py +92 -0
- gamr-0.1.1/src/gamr/services/git_provider.py +220 -0
- gamr-0.1.1/src/gamr/services/icons.py +72 -0
- gamr-0.1.1/src/gamr/state.py +213 -0
- gamr-0.1.1/src/gamr/widgets/__init__.py +3 -0
- gamr-0.1.1/src/gamr/widgets/file_tree_table.py +657 -0
- gamr-0.1.1/src/gamr/widgets/filter_bar.py +104 -0
- gamr-0.1.1/src/gamr/widgets/preview_pane.py +678 -0
- gamr-0.1.1/src/gamr/widgets/split.py +100 -0
- gamr-0.1.1/src/gamr/widgets/tree_data.py +124 -0
- gamr-0.1.1/tests/__init__.py +0 -0
- gamr-0.1.1/tests/test_app.py +136 -0
- gamr-0.1.1/tests/test_diff_overview.py +147 -0
- gamr-0.1.1/tests/test_file_index.py +83 -0
- gamr-0.1.1/tests/test_file_scanner.py +95 -0
- gamr-0.1.1/tests/test_file_tree_table.py +104 -0
- gamr-0.1.1/tests/test_filter_bar.py +62 -0
- gamr-0.1.1/tests/test_git_provider.py +122 -0
- gamr-0.1.1/tests/test_gutter_markers.py +137 -0
- gamr-0.1.1/tests/test_icons.py +77 -0
- gamr-0.1.1/tests/test_preview_pane.py +71 -0
- gamr-0.1.1/tests/test_state.py +48 -0
- gamr-0.1.1/tests/test_tree_data.py +106 -0
- gamr-0.1.1/uv.lock +1821 -0
gamr-0.1.1/.bandit
ADDED
gamr-0.1.1/.gitignore
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v5.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
- id: check-yaml
|
|
8
|
+
- id: check-toml
|
|
9
|
+
- id: check-added-large-files
|
|
10
|
+
- id: check-merge-conflict
|
|
11
|
+
- id: detect-private-key
|
|
12
|
+
|
|
13
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
14
|
+
rev: v0.15.1
|
|
15
|
+
hooks:
|
|
16
|
+
- id: ruff
|
|
17
|
+
args: [--fix]
|
|
18
|
+
- id: ruff-format
|
|
19
|
+
|
|
20
|
+
- repo: https://github.com/PyCQA/bandit
|
|
21
|
+
rev: 1.8.3
|
|
22
|
+
hooks:
|
|
23
|
+
- id: bandit
|
|
24
|
+
args: [-r, -c, .bandit]
|
|
25
|
+
files: ^src/
|
|
26
|
+
|
|
27
|
+
- repo: https://github.com/shellcheck-py/shellcheck-py
|
|
28
|
+
rev: v0.10.0.1
|
|
29
|
+
hooks:
|
|
30
|
+
- id: shellcheck
|
|
31
|
+
args: [-x, -e, SC2016]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.14
|
gamr-0.1.1/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
0.1.1 (2025-06-10)
|
|
4
|
+
|
|
5
|
+
Initial public release.
|
|
6
|
+
|
|
7
|
+
- Live file watching with automatic tree updates (watchdog + polling fallback)
|
|
8
|
+
- Pure Python git integration via Dulwich — no git binary required
|
|
9
|
+
- File status indicators (Modified, Added, Deleted, Untracked, Staged)
|
|
10
|
+
- Three diff modes: full file, gutter markers, unified diff
|
|
11
|
+
- Three view modes: tree, flat filenames, flat relative paths
|
|
12
|
+
- Fuzzy filename search with RapidFuzz
|
|
13
|
+
- Git status filtering toggles
|
|
14
|
+
- Syntax-highlighted preview with Monokai theme
|
|
15
|
+
- Column sorting, resizable split pane, state persistence
|
|
16
|
+
- Background blame loading (author + last modified)
|
|
17
|
+
- .gitignore support
|
|
18
|
+
- Graceful degradation on non-git directories
|
gamr-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Edouard Poor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
gamr-0.1.1/Makefile
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Makefile for Gamr — Git-aware Agent Monitor & Review
|
|
2
|
+
#
|
|
3
|
+
# Usage:
|
|
4
|
+
# make Show all available targets
|
|
5
|
+
# make <target> Run a specific target
|
|
6
|
+
|
|
7
|
+
.PHONY: help
|
|
8
|
+
help: ## Show this help message
|
|
9
|
+
@echo "Available targets:"
|
|
10
|
+
@echo ""
|
|
11
|
+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
|
12
|
+
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
|
13
|
+
@echo ""
|
|
14
|
+
@echo "Common workflows:"
|
|
15
|
+
@echo " make install test # Set up and run tests"
|
|
16
|
+
@echo " make lint format # Check and fix code"
|
|
17
|
+
@echo " make security test # Full validation"
|
|
18
|
+
|
|
19
|
+
# ============================================================================
|
|
20
|
+
# Setup and Installation
|
|
21
|
+
# ============================================================================
|
|
22
|
+
|
|
23
|
+
.PHONY: install
|
|
24
|
+
install: ## Install dependencies with uv
|
|
25
|
+
uv sync
|
|
26
|
+
|
|
27
|
+
.PHONY: install-hooks
|
|
28
|
+
install-hooks: ## Install pre-commit git hooks
|
|
29
|
+
uv run pre-commit install
|
|
30
|
+
|
|
31
|
+
# ============================================================================
|
|
32
|
+
# Code Quality
|
|
33
|
+
# ============================================================================
|
|
34
|
+
|
|
35
|
+
.PHONY: lint
|
|
36
|
+
lint: ## Run linting checks (ruff check + format check)
|
|
37
|
+
./scripts/lint.sh
|
|
38
|
+
|
|
39
|
+
.PHONY: format
|
|
40
|
+
format: ## Auto-fix formatting and linting issues
|
|
41
|
+
./scripts/lint.sh --fix
|
|
42
|
+
|
|
43
|
+
.PHONY: pre-commit
|
|
44
|
+
pre-commit: ## Run all pre-commit hooks manually
|
|
45
|
+
uv run pre-commit run --all-files
|
|
46
|
+
|
|
47
|
+
.PHONY: security
|
|
48
|
+
security: ## Run security scans (bandit + pip-audit)
|
|
49
|
+
uv run bandit -r src/ -c .bandit
|
|
50
|
+
uv run pip-audit
|
|
51
|
+
|
|
52
|
+
# ============================================================================
|
|
53
|
+
# Testing
|
|
54
|
+
# ============================================================================
|
|
55
|
+
|
|
56
|
+
.PHONY: test
|
|
57
|
+
test: ## Run all tests
|
|
58
|
+
uv run pytest tests/ -q
|
|
59
|
+
|
|
60
|
+
.PHONY: test-verbose
|
|
61
|
+
test-verbose: ## Run tests with verbose output
|
|
62
|
+
uv run pytest tests/ -v
|
|
63
|
+
|
|
64
|
+
.PHONY: coverage
|
|
65
|
+
coverage: ## Run tests with coverage report
|
|
66
|
+
uv run pytest tests/ -q --cov=gamr --cov-report=term-missing
|
|
67
|
+
|
|
68
|
+
# ============================================================================
|
|
69
|
+
# Dependencies
|
|
70
|
+
# ============================================================================
|
|
71
|
+
|
|
72
|
+
.PHONY: update-deps
|
|
73
|
+
update-deps: ## Update dependencies (7-day lag for supply chain protection)
|
|
74
|
+
@echo "╔══════════════════════════════════════════════════════════════╗"
|
|
75
|
+
@echo "║ Updating dependencies with 7-day publication lag ║"
|
|
76
|
+
@echo "║ (packages published less than 7 days ago are excluded) ║"
|
|
77
|
+
@echo "╚══════════════════════════════════════════════════════════════╝"
|
|
78
|
+
@echo ""
|
|
79
|
+
uv lock --upgrade --exclude-newer "$$(date -v-7d +%Y-%m-%dT00:00:00Z)"
|
|
80
|
+
uv sync
|
|
81
|
+
@echo ""
|
|
82
|
+
@echo "Running security audit..."
|
|
83
|
+
uv run pip-audit
|
|
84
|
+
@echo ""
|
|
85
|
+
@echo "✅ Dependencies updated. Run 'make test' to verify."
|
|
86
|
+
|
|
87
|
+
# ============================================================================
|
|
88
|
+
# Build
|
|
89
|
+
# ============================================================================
|
|
90
|
+
|
|
91
|
+
.PHONY: build
|
|
92
|
+
build: ## Build wheel and sdist
|
|
93
|
+
uv build
|
|
94
|
+
|
|
95
|
+
# ============================================================================
|
|
96
|
+
# Run
|
|
97
|
+
# ============================================================================
|
|
98
|
+
|
|
99
|
+
.PHONY: run
|
|
100
|
+
run: ## Run the app on the current directory
|
|
101
|
+
uv run gamr .
|
|
102
|
+
|
|
103
|
+
# ============================================================================
|
|
104
|
+
# Release
|
|
105
|
+
# ============================================================================
|
|
106
|
+
|
|
107
|
+
.PHONY: release
|
|
108
|
+
release: ## Release (interactive — asks for bump type)
|
|
109
|
+
@CHOICE=$$(uv run scripts/chooser.py --title "Release type?" patch minor major 3>&1 1>&2) || exit 1; \
|
|
110
|
+
./scripts/release.sh $$CHOICE
|
|
111
|
+
|
|
112
|
+
.PHONY: release-patch
|
|
113
|
+
release-patch: ## Release a patch version (bug fixes)
|
|
114
|
+
./scripts/release.sh patch
|
|
115
|
+
|
|
116
|
+
.PHONY: release-minor
|
|
117
|
+
release-minor: ## Release a minor version (new features)
|
|
118
|
+
./scripts/release.sh minor
|
|
119
|
+
|
|
120
|
+
.PHONY: release-major
|
|
121
|
+
release-major: ## Release a major version (breaking changes)
|
|
122
|
+
./scripts/release.sh major
|
|
123
|
+
|
|
124
|
+
# ============================================================================
|
|
125
|
+
# Cleanup
|
|
126
|
+
# ============================================================================
|
|
127
|
+
|
|
128
|
+
.PHONY: clean
|
|
129
|
+
clean: ## Remove build artifacts and caches
|
|
130
|
+
rm -rf dist/ .pytest_cache .ruff_cache .coverage
|
|
131
|
+
|
|
132
|
+
# ============================================================================
|
|
133
|
+
# Validation (Full Quality Check)
|
|
134
|
+
# ============================================================================
|
|
135
|
+
|
|
136
|
+
.PHONY: validate
|
|
137
|
+
validate: lint security test ## Run lint, security, and tests
|
|
138
|
+
@echo ""
|
|
139
|
+
@echo "✅ Validation complete!"
|
|
140
|
+
|
|
141
|
+
# ============================================================================
|
|
142
|
+
# Default Target
|
|
143
|
+
# ============================================================================
|
|
144
|
+
|
|
145
|
+
.DEFAULT_GOAL := help
|
gamr-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gamr
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Git-aware Agent Monitor & Review — a TUI for reviewing AI agent work
|
|
5
|
+
Author: Edouard Poor
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Requires-Dist: dulwich>=0.22.7
|
|
10
|
+
Requires-Dist: rapidfuzz>=3.14.0
|
|
11
|
+
Requires-Dist: textual>=1.0.0
|
|
12
|
+
Requires-Dist: watchdog>=6.0.0
|
gamr-0.1.1/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Gamr
|
|
2
|
+
|
|
3
|
+
**G**it-aware **A**gent **M**onitor & **R**eview — a TUI for reviewing AI agent work with live watching, fuzzy search, and diff preview.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Live file watching** — tree updates automatically when files change (watchdog + polling fallback)
|
|
8
|
+
- **Git integration** — pure Python via Dulwich, no git binary required
|
|
9
|
+
- File status indicators (Modified, Added, Deleted, Untracked, Staged)
|
|
10
|
+
- Three diff modes: full file diff, gutter markers, unified diff (`d`/`D` to cycle)
|
|
11
|
+
- Blame data (last author, last modified) loaded in background
|
|
12
|
+
- Respects `.gitignore` rules
|
|
13
|
+
- **Three view modes** — tree, flat filenames, flat relative paths (`v` to cycle)
|
|
14
|
+
- **Column sorting** — click any column header to sort asc/desc/none
|
|
15
|
+
- **Fuzzy search** — fzf-like filename filtering with RapidFuzz
|
|
16
|
+
- **Git status filtering** — toggle buttons to show only modified, added, etc.
|
|
17
|
+
- **Syntax-highlighted preview** — Monokai theme, scrollable, line numbers
|
|
18
|
+
- **Copy file references** — double-click or drag lines to copy `path:line` to clipboard
|
|
19
|
+
- **Full file diff** — syntax highlighting with inline +/- markers and colored backgrounds
|
|
20
|
+
- **Diff overview bar** — 1-column change map (line or braille style) in full diff mode
|
|
21
|
+
- **Gradient colors** — size and modification time columns colored by relative magnitude
|
|
22
|
+
- **File icons** — loads from `~/.config/lsd/icons.yaml` if present
|
|
23
|
+
- **Resizable split pane** — drag the divider between tree and preview
|
|
24
|
+
- **Desktop-style navigation** — ←/→ to collapse/expand folders, ← on file collapses parent
|
|
25
|
+
- **State persistence** — view mode, columns, collapsed dirs, selection, filters saved between sessions
|
|
26
|
+
- **Graceful degradation** — works on non-git directories (hides git UI)
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
Requires Python 3.11+.
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
uv sync
|
|
34
|
+
uv run gamr [path]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Keybindings
|
|
38
|
+
|
|
39
|
+
| Key | Action |
|
|
40
|
+
| -------- | -------------------------------------------------- |
|
|
41
|
+
| `↑`/`↓` | Navigate files |
|
|
42
|
+
| `→` | Expand directory |
|
|
43
|
+
| `←` | Collapse directory (or parent if on file) |
|
|
44
|
+
| `space` | Toggle expand/collapse |
|
|
45
|
+
| `v` | Cycle view mode (tree → flat name → flat path) |
|
|
46
|
+
| `d` | Cycle diff mode (full → gutter → unified) |
|
|
47
|
+
| `D` | Cycle diff mode reverse |
|
|
48
|
+
| `m` | Toggle Modified filter |
|
|
49
|
+
| `f` | Toggle follow mode (auto-select last changed file) |
|
|
50
|
+
| `ctrl+f` | Focus search input |
|
|
51
|
+
| `b` | Toggle blame columns (last author, git time) |
|
|
52
|
+
| `1`–`6` | Toggle individual columns |
|
|
53
|
+
| `tab` | Switch focus between tree and preview |
|
|
54
|
+
| `ctrl+p` | Command palette (spaced paths, gradient toggle) |
|
|
55
|
+
| `q` | Quit (saves state) |
|
|
56
|
+
|
|
57
|
+
Column headers are clickable to sort (ascending → descending → none).
|
|
58
|
+
|
|
59
|
+
## Architecture
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
src/gamr/
|
|
63
|
+
├── app.py # Textual app, keybindings, orchestration
|
|
64
|
+
├── commands.py # Command palette provider
|
|
65
|
+
├── models.py # FileEntry, GitStatus, DiffMode, etc.
|
|
66
|
+
├── state.py # Persistent state management
|
|
67
|
+
├── gamr.tcss # Stylesheet
|
|
68
|
+
├── services/
|
|
69
|
+
│ ├── diff_parser.py # Unified diff parsing into DiffData
|
|
70
|
+
│ ├── file_scanner.py # Watchdog + polling fallback + git state watcher
|
|
71
|
+
│ ├── file_index.py # Merges scanner + git into FileEntry list
|
|
72
|
+
│ ├── git_provider.py # GitProvider ABC + Dulwich implementation
|
|
73
|
+
│ ├── filter.py # Fuzzy and status filtering
|
|
74
|
+
│ └── icons.py # lsd icons.yaml loader
|
|
75
|
+
└── widgets/
|
|
76
|
+
├── file_tree_table.py # DataTable with tree semantics
|
|
77
|
+
├── tree_data.py # Tree building and sorting logic
|
|
78
|
+
├── filter_bar.py # Git status toggles + search input
|
|
79
|
+
├── preview_pane.py # Syntax highlighting + diff view
|
|
80
|
+
└── split.py # Resizable horizontal split
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Dependencies
|
|
84
|
+
|
|
85
|
+
- [Textual](https://textual.textualize.io/) — TUI framework
|
|
86
|
+
- [Dulwich](https://dulwich.io/) — pure Python git (no compiled deps)
|
|
87
|
+
- [watchdog](https://github.com/gorakhargosh/watchdog) — filesystem events
|
|
88
|
+
- [RapidFuzz](https://github.com/rapidfuzz/RapidFuzz) — fuzzy string matching
|
|
89
|
+
|
|
90
|
+
## Tests
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
uv run pytest
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Documentation
|
|
97
|
+
|
|
98
|
+
- [UI Design Rules](docs/UI_DESIGN.md) — all interaction behaviors and edge cases
|
|
99
|
+
- [Architecture Decision Records](docs/adrs/README.md) — 23 ADRs covering all design choices
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# UI Design Rules
|
|
2
|
+
|
|
3
|
+
## Navigation
|
|
4
|
+
|
|
5
|
+
- **↑/↓** — Move cursor between rows in the file tree
|
|
6
|
+
- **→** on a collapsed directory — Expand it
|
|
7
|
+
- **→** on a file — No action
|
|
8
|
+
- **←** on an expanded directory — Collapse it; cursor stays on the directory; preview clears or shows nothing (dirs have no content)
|
|
9
|
+
- **←** on a file or collapsed directory — Collapse the parent folder, cursor moves to parent; preview clears (now on a directory)
|
|
10
|
+
- **←** on a file at root level (no parent to collapse) — No action; preview stays on current file
|
|
11
|
+
- **←** on a collapsed directory at root level — No action; preview unchanged
|
|
12
|
+
|
|
13
|
+
### Edge cases for ← (collapse/navigate-to-parent)
|
|
14
|
+
|
|
15
|
+
| Current selection | Action | Cursor after | Preview after |
|
|
16
|
+
| ------------------------------------------ | ----------------------------- | ----------------------- | --------------------------------- |
|
|
17
|
+
| Expanded directory | Collapse it | Stays on that directory | Clears (directory has no preview) |
|
|
18
|
+
| File inside `src/` | Collapse `src/` | Moves to `src/` row | Clears (now on directory) |
|
|
19
|
+
| Collapsed directory inside `src/` | Collapse `src/` | Moves to `src/` row | Clears |
|
|
20
|
+
| File at root level (no parent dir in tree) | Nothing | Unchanged | Unchanged |
|
|
21
|
+
| Root-level directory (already collapsed) | Nothing | Unchanged | Unchanged |
|
|
22
|
+
| File while in flat view mode | No action (no tree hierarchy) | Unchanged | Unchanged |
|
|
23
|
+
|
|
24
|
+
**Preview pane rule:** When cursor lands on a directory (after ← collapses parent), the preview pane shows nothing meaningful — directories don't have file content. The preview should retain its last content but dim or show "Select a file to preview" if we want to be explicit. Current behavior: preview simply doesn't update (the `NodeHighlighted` message carries `entry=None` for directory nodes, which is filtered out before rendering).
|
|
25
|
+
- **Space** on a directory — Toggle expand/collapse
|
|
26
|
+
- **Tab** — Switch focus between the file tree and preview pane
|
|
27
|
+
- After expand/collapse, the cursor remains on the same directory row
|
|
28
|
+
|
|
29
|
+
## View Modes
|
|
30
|
+
|
|
31
|
+
- **v** — Cycle through: tree → flat name → flat path
|
|
32
|
+
- **Tree** — Hierarchical with expand/collapse, dirs with ▶/▼ twisties
|
|
33
|
+
- **Flat name** — Leaf filenames only, no paths, no hierarchy
|
|
34
|
+
- **Flat path** — Relative paths (e.g., `src / worker / foo.py`)
|
|
35
|
+
- Spaced paths (`/` → ` / `) is the default; toggled via command palette
|
|
36
|
+
- When sorting is activated, tree mode auto-switches to flat path (sorting requires a flat list)
|
|
37
|
+
- When sort is removed, the view restores to tree mode if that's where it was
|
|
38
|
+
|
|
39
|
+
## Column Sorting
|
|
40
|
+
|
|
41
|
+
- Click any column header — Cycle: ascending (▲) → descending (▼) → no sort
|
|
42
|
+
- Sorting only applies in flat view modes
|
|
43
|
+
- Sort indicators shown as suffix on the column header text
|
|
44
|
+
|
|
45
|
+
## Filtering
|
|
46
|
+
|
|
47
|
+
- **m** — Toggle the Modified git status filter
|
|
48
|
+
- **ctrl+f** — Focus the search input
|
|
49
|
+
- Filter buttons (M, A, D, ?, S) toggle git status filters
|
|
50
|
+
- Search input provides fzf-like fuzzy filename matching (RapidFuzz)
|
|
51
|
+
- Filters compose: status filter applied first, then fuzzy search
|
|
52
|
+
- Filter state (selected filter IDs + search query) persisted between sessions
|
|
53
|
+
|
|
54
|
+
## Follow Mode
|
|
55
|
+
|
|
56
|
+
- **f** — Toggle follow mode on/off (notification shown)
|
|
57
|
+
- When ON and the file watcher detects a change:
|
|
58
|
+
- The tree cursor automatically jumps to the last changed file
|
|
59
|
+
- Parent folders are expanded to reveal it
|
|
60
|
+
- The preview pane renders the file
|
|
61
|
+
- If the file is git-modified, the preview scrolls to the first diff hunk
|
|
62
|
+
- When OFF, file watcher changes preserve the current cursor position
|
|
63
|
+
- Follow mode does not persist between sessions (always starts OFF)
|
|
64
|
+
|
|
65
|
+
## Preview Pane
|
|
66
|
+
|
|
67
|
+
- Automatically shows the highlighted file's content
|
|
68
|
+
- **d** — Cycle diff mode forward: full diff → gutter → unified diff
|
|
69
|
+
- **D** (shift) — Cycle diff mode backward
|
|
70
|
+
- Diff modes only apply to files with git changes; clean files show gutter mode as plain file
|
|
71
|
+
- Full diff — Syntax-highlighted full file with `+`/`-` markers and colored backgrounds (green `#002200` for added, red `#300000` for removed)
|
|
72
|
+
- Gutter — Syntax-highlighted file with a change column after line numbers: orange `●` for changed lines, green `+` for added, red `_` where deletions follow. When file has no changes, renders as plain file (no gutter column, no overview bar).
|
|
73
|
+
- Unified diff — Standard coloured unified diff output
|
|
74
|
+
- Preview and full diff share identical rendering (same line numbers, same syntax highlighting)
|
|
75
|
+
- Monokai background (`#272822`) fills the entire pane
|
|
76
|
+
- Header bar (pinned, doesn't scroll) shows filename on left, diff mode on right
|
|
77
|
+
- When switching diff modes, scroll position is preserved by source line number mapping
|
|
78
|
+
- Preview doesn't change when new files appear in the tree (stable during file watcher updates)
|
|
79
|
+
- When the previewed file changes on disk, scroll position is preserved by source line mapping
|
|
80
|
+
- The 10-second timestamp refresh does not reset preview scroll
|
|
81
|
+
- **Double-click** a line → copies `path:line` to clipboard, flashes highlight
|
|
82
|
+
- **Drag** across lines → live highlight, copies `path:start-end` on release
|
|
83
|
+
- Invalid lines in unified diff (headers, removed) are not selectable
|
|
84
|
+
|
|
85
|
+
## Columns
|
|
86
|
+
|
|
87
|
+
- **1–6** — Toggle individual columns
|
|
88
|
+
- **b** — Toggle blame columns (author + git time) and trigger background load
|
|
89
|
+
- Available columns: Name, Status (St), Lines (+/-), Size, Modified, Author, Git Time
|
|
90
|
+
- Size and Modified columns use a 256-color gradient by relative magnitude
|
|
91
|
+
- Gradient toggled via command palette
|
|
92
|
+
- Blame columns show "..." while loading, then populate progressively
|
|
93
|
+
|
|
94
|
+
## Gradient Colors
|
|
95
|
+
|
|
96
|
+
- Applied to Size and Modified columns
|
|
97
|
+
- Color ramp: `[15, 51, 45, 39, 33, 27, 57, 93, 129, 165, 201, 200, 199, 198, 197, 196]`
|
|
98
|
+
- White/cyan (low) → blue/purple (mid) → magenta/red (high)
|
|
99
|
+
- Ranges computed per the currently visible/filtered file set
|
|
100
|
+
|
|
101
|
+
## File Icons
|
|
102
|
+
|
|
103
|
+
- Loaded from `~/.config/lsd/icons.yaml` if present
|
|
104
|
+
- Resolution order: exact filename → file extension → filetype fallback (file/dir)
|
|
105
|
+
- No PyYAML dependency required (custom parser)
|
|
106
|
+
|
|
107
|
+
## Git Integration
|
|
108
|
+
|
|
109
|
+
- Graceful degradation for non-git directories: hides status/lines columns and filter buttons
|
|
110
|
+
- `.gitignore` rules enforced via Dulwich's `IgnoreFilterManager` (handles nesting, negation, `**` globs)
|
|
111
|
+
- Git status: M (modified), A (added), D (deleted), ? (untracked), SM/SA/SD (staged variants)
|
|
112
|
+
- Diff computed against HEAD
|
|
113
|
+
|
|
114
|
+
## Live File Watching
|
|
115
|
+
|
|
116
|
+
- File tree updates automatically when files change on disk
|
|
117
|
+
- Watchdog for native filesystem events; polling fallback if watchdog fails
|
|
118
|
+
- On change: rebuild file index, re-apply filters, sync table incrementally (only changed rows update)
|
|
119
|
+
- Column headers preserved during data refreshes (no flicker)
|
|
120
|
+
- Parent folders of changed files are auto-expanded to ensure visibility
|
|
121
|
+
- Preview updates in-place (no scroll reset) when the selected file's content changes
|
|
122
|
+
- Relative timestamps refresh every 10 seconds via `update_cell()` (no table rebuild)
|
|
123
|
+
- `.gamrstate` excluded from watching (prevents feedback loop)
|
|
124
|
+
|
|
125
|
+
## State Persistence
|
|
126
|
+
|
|
127
|
+
- Saved to `~/.config/gamr/state.json` on quit
|
|
128
|
+
- Restored on next launch if target directory matches
|
|
129
|
+
- Persisted state: view mode, diff mode, column toggles, spaced paths, gradient, collapsed directories, split position, selected file, selected filter IDs, search query
|
|
130
|
+
|
|
131
|
+
## Resizable Split
|
|
132
|
+
|
|
133
|
+
- Draggable 1-character divider between tree and preview panes
|
|
134
|
+
- Divider is subtle (`$surface-lighten-1`, one shade lighter on hover)
|
|
135
|
+
- Split fraction persisted between sessions
|
|
136
|
+
- Uses `fr` units for pane sizing to avoid rounding gaps
|
|
137
|
+
|
|
138
|
+
## Diff Overview Bar
|
|
139
|
+
|
|
140
|
+
- 1-column bar docked to the right of the preview pane
|
|
141
|
+
- Only visible in full diff mode; hidden in unified diff and plain preview
|
|
142
|
+
- Shows file-level change distribution: green (added), red (removed), yellow (mixed), dim (unchanged)
|
|
143
|
+
- Two styles toggled via command palette (`ctrl+p` → "braille"):
|
|
144
|
+
- **Line mode** (default): `┃`/`│` characters
|
|
145
|
+
- **Braille mode**: Unicode braille dots packing 4 source lines per terminal row
|
|
146
|
+
|
|
147
|
+
## Command Palette (ctrl+p)
|
|
148
|
+
|
|
149
|
+
- Toggle spaced paths
|
|
150
|
+
- Toggle gradient colors
|
|
151
|
+
- Toggle diff overview style (line/braille)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
date: 2026-06-08
|
|
4
|
+
deciders: edouard
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ADR-001: Use Textual as TUI Framework
|
|
8
|
+
|
|
9
|
+
## Context and Problem Statement
|
|
10
|
+
|
|
11
|
+
Which Python TUI framework should we use to build an interactive, responsive file browser with split panes, data tables, and async workers?
|
|
12
|
+
|
|
13
|
+
## Considered Options
|
|
14
|
+
|
|
15
|
+
1. Textual — modern, CSS-like styling, async-native, rich widget library
|
|
16
|
+
2. urwid — mature but lower-level, manual layout
|
|
17
|
+
3. blessed/curses — very low-level, no built-in widgets
|
|
18
|
+
|
|
19
|
+
## Decision Outcome
|
|
20
|
+
|
|
21
|
+
Chosen option: **Textual** because it provides DataTable, Tree, reactive attributes, CSS styling, built-in Workers API for background tasks, and a pilot testing framework. Its async event loop integrates cleanly with our concurrency needs.
|
|
22
|
+
|
|
23
|
+
### Consequences
|
|
24
|
+
|
|
25
|
+
- Good, because DataTable gives us proper column alignment and headers
|
|
26
|
+
- Good, because Workers API handles thread↔UI bridging elegantly
|
|
27
|
+
- Good, because CSS separation keeps styling maintainable
|
|
28
|
+
- Neutral, because Textual is relatively new and API may evolve
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
date: 2026-06-08
|
|
4
|
+
deciders: edouard
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ADR-002: Pure Python Git via Dulwich
|
|
8
|
+
|
|
9
|
+
## Context and Problem Statement
|
|
10
|
+
|
|
11
|
+
How should we interact with git repositories? We need status, diff, blame, and gitignore support without requiring the git binary.
|
|
12
|
+
|
|
13
|
+
## Considered Options
|
|
14
|
+
|
|
15
|
+
1. Dulwich — pure Python, no compiled deps, no git binary needed
|
|
16
|
+
2. pygit2 — libgit2 bindings, fast but requires native compilation
|
|
17
|
+
3. GitPython — wraps the git CLI, requires git installed
|
|
18
|
+
4. Shell out to git — simple but fragile, requires git binary
|
|
19
|
+
|
|
20
|
+
## Decision Outcome
|
|
21
|
+
|
|
22
|
+
Chosen option: **Dulwich** because it's pure Python (works anywhere Python does), has no compiled dependencies, provides porcelain + low-level APIs for status/diff/log, and includes `IgnoreFilterManager` for proper .gitignore handling.
|
|
23
|
+
|
|
24
|
+
### Consequences
|
|
25
|
+
|
|
26
|
+
- Good, because zero native deps — easy install on any platform
|
|
27
|
+
- Good, because `IgnoreFilterManager` handles nested .gitignore, negation, and global excludes
|
|
28
|
+
- Bad, because some operations (blame) are slower than native git
|
|
29
|
+
- Mitigation: expensive operations run in background thread workers
|
|
30
|
+
|
|
31
|
+
## Design Note
|
|
32
|
+
|
|
33
|
+
The `GitProvider` ABC allows swapping Dulwich for another backend (e.g., pygit2 for performance) without changing the rest of the app.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
date: 2026-06-08
|
|
4
|
+
deciders: edouard
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ADR-003: Watchdog with Polling Fallback for File Watching
|
|
8
|
+
|
|
9
|
+
## Context and Problem Statement
|
|
10
|
+
|
|
11
|
+
How do we detect filesystem changes in real-time to keep the file tree up to date?
|
|
12
|
+
|
|
13
|
+
## Considered Options
|
|
14
|
+
|
|
15
|
+
1. watchdog — cross-platform, uses FSEvents on macOS, inotify on Linux
|
|
16
|
+
2. OS-native only (PyObjC/fsevents) — macOS-specific
|
|
17
|
+
3. Pure polling — simple but high latency and CPU cost
|
|
18
|
+
4. watchdog + polling fallback — best of both
|
|
19
|
+
|
|
20
|
+
## Decision Outcome
|
|
21
|
+
|
|
22
|
+
Chosen option: **watchdog with polling fallback** because watchdog provides efficient native filesystem events on all platforms, and polling ensures the app still works if watchdog fails (e.g., network filesystems, Docker volumes).
|
|
23
|
+
|
|
24
|
+
### Consequences
|
|
25
|
+
|
|
26
|
+
- Good, because native events are near-instant on macOS (FSEvents)
|
|
27
|
+
- Good, because polling fallback ensures robustness on edge-case filesystems
|
|
28
|
+
- Neutral, watchdog runs its own thread — requires queue-based bridging to Textual's event loop
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
date: 2026-06-08
|
|
4
|
+
deciders: edouard
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ADR-004: DataTable with Tree Semantics (Not Tree Widget)
|
|
8
|
+
|
|
9
|
+
## Context and Problem Statement
|
|
10
|
+
|
|
11
|
+
How do we display a file tree alongside metadata columns with proper alignment?
|
|
12
|
+
|
|
13
|
+
## Considered Options
|
|
14
|
+
|
|
15
|
+
1. Textual's Tree widget with render_label() override — single text column, manual padding
|
|
16
|
+
2. DataTable with tree-rendered first column — real columns, proper alignment
|
|
17
|
+
3. Side-by-side Tree + DataTable with scroll sync — complex sync logic
|
|
18
|
+
|
|
19
|
+
## Decision Outcome
|
|
20
|
+
|
|
21
|
+
Chosen option: **DataTable with tree semantics in column 1** because it gives us real, properly-aligned columns with headers (clickable for sorting), while the first column renders indentation and expand/collapse icons to simulate tree behavior.
|
|
22
|
+
|
|
23
|
+
### Consequences
|
|
24
|
+
|
|
25
|
+
- Good, because columns are independently sized, have headers, support sorting
|
|
26
|
+
- Good, because DataTable cursor_type="row" gives us keyboard navigation for free
|
|
27
|
+
- Bad, because we reimplement expand/collapse logic (space key handler)
|
|
28
|
+
- Neutral, tree state is internal; the DataTable is rebuilt on expand/collapse
|
|
29
|
+
|
|
30
|
+
## Supersedes
|
|
31
|
+
|
|
32
|
+
Initial implementation used Tree widget with render_label() appending padded metadata — but columns didn't align when filenames varied in length.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
status: accepted
|
|
3
|
+
date: 2026-06-08
|
|
4
|
+
deciders: edouard
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ADR-005: Thread Workers for Blocking Git Operations
|
|
8
|
+
|
|
9
|
+
## Context and Problem Statement
|
|
10
|
+
|
|
11
|
+
Textual runs on a single asyncio event loop. Dulwich operations (status, diff, blame) are synchronous and can block for seconds on large repos. How do we prevent UI freezes?
|
|
12
|
+
|
|
13
|
+
## Decision Outcome
|
|
14
|
+
|
|
15
|
+
Use Textual's `@work(thread=True)` decorator to run blocking operations in thread workers.
|
|
16
|
+
|
|
17
|
+
### Concurrency Architecture
|
|
18
|
+
|
|
19
|
+
| Operation | Worker Type | Strategy |
|
|
20
|
+
|-----------|------------|----------|
|
|
21
|
+
| Git status | Thread, exclusive | Debounced after file changes |
|
|
22
|
+
| Diff stats (+/- lines) | Thread, exclusive | Deferred after initial load |
|
|
23
|
+
| Git blame/log | Thread, group | Progressive per-file updates |
|
|
24
|
+
| File watcher | Thread, long-running | Polls queue every 0.5s |
|
|
25
|
+
| Fuzzy filter | Inline (sync) | Fast enough for <10k files |
|
|
26
|
+
|
|
27
|
+
### Key Constraints
|
|
28
|
+
|
|
29
|
+
- **watchdog → UI**: Events arrive on watchdog's thread; bridged via `call_from_thread()` or `post_message()` (thread-safe)
|
|
30
|
+
- **Dulwich → UI**: Results pushed via `call_from_thread()`; check `worker.is_cancelled` before updating
|
|
31
|
+
- **Exclusive workers**: Cancel previous work when user changes selection/filter (prevents stale writes)
|
|
32
|
+
|
|
33
|
+
### Consequences
|
|
34
|
+
|
|
35
|
+
- Good, because UI stays responsive during expensive git operations
|
|
36
|
+
- Good, because Textual manages worker lifecycle (auto-cleanup on widget removal)
|
|
37
|
+
- Neutral, blame columns show "..." placeholder while loading
|