Interactive git staging with tree view
(noun): the quicker picker upper
A complete git workflow TUI written in modern Fortran 2018. Navigate your repository's dirty files in a tree view, stage and unstage changes, commit, push, and perform virtually any git operation without leaving the terminal. Features three-mode design, fuzzy search navigation, and fzf integration.
Normal, Git, and Rename modes separate navigation from operations.
Type to jump to files. fzf-style scoring prioritizes exact and prefix matches.
Stage, commit, push, pull, fetch, branch, merge, rebase, stash, and more.
After fetch, files with incoming changes show blue indicators.
Branch switching, stash selection, and more use fzf for fuzzy selection.
Rename files without leaving fuss. Handles case-only changes on macOS.
Caches git command results to minimize shell overhead during navigation.
Preserves terminal content like vim. Clean exit restores original state.
Navigate to any git repository and run fuss. Interactive mode is the default.
fuss:trunk ← repo name : branch name . ├── ▼ src │ ├── main.f90 ↑ ← staged │ └── utils.f90 ✗ ← modified ├── README.md ↑✗ ← both staged and unstaged changes └── new_file.txt ✗ ← untracked Legend: ↑=staged ✗=modified ✗=untracked ↓=incoming Keys: j/k/↓/↑:nav | ←/→:nav tree | space:toggle | .:hide-dots | alt-g:git-mode
fuss in your git repositoryfuss is designed to work out of the box with zero configuration. It reads your git configuration and adapts to your terminal automatically. There are no configuration files to manage.
fuss automatically detects your terminal emulator via $TERM_PROGRAM
and adjusts padding to prevent top-line cutoff in alternate screen mode:
| Option | Type | Default | Description |
|---|---|---|---|
iTerm2 | macOS | 4 lines | Needs extra padding in alternate screen |
WezTerm | cross-platform | 3 lines | GPU-accelerated terminal |
Ghostty | cross-platform | 3 lines | Native platform terminal |
Apple Terminal | macOS | 3 lines | Built-in macOS terminal |
Others | various | 2 lines | Default padding for unrecognized terminals |
fuss uses your existing git configuration. These settings affect fuss behavior:
user.name / user.email - Used for commitsremote.origin.url - Used for push/pull/fetch.gitignore - Respected when showing file statuscore.editor - Not used (fuss has inline commit prompt)Several Git Mode operations use fzf for selection. Customize fzf behavior with environment variables:
fuss caches git status results for 500ms to avoid redundant commands during rapid navigation. The cache is automatically invalidated after state-changing operations:
# Operations that invalidate cache:
- Staging (a) / Unstaging (u)
- Stage all (S) / Unstage all (U)
- Commits (m, M)
- Push (p) / Pull (l) / Fetch (f)
- Branch operations (b, n, R, G)
- Stash (z, Z)
- Reset (O) / Rebase (I)
- File deletion (r) / Discard (x)
- File rename (Alt-n → Tab) fuss writes debug information to help troubleshoot input handling:
The debug log captures control character inputs, fuzzy search buffer state, and rename mode operations.
fuss is a terminal-based git workflow tool that displays your repository's dirty files in an interactive tree view. Unlike GUI git clients, fuss runs entirely in the terminal, preserves your workflow, and requires no mouse.
Written in modern Fortran 2018, fuss demonstrates that Fortran is capable of building terminal applications with complex input handling. The project uses Fortran's excellent module system, pointer-based tree structures, and ISO Fortran environment intrinsics.
fuss uses a three-mode design to separate different types of operations. This prevents accidental commits or destructive actions during navigation.
Default. Navigate, search, expand/collapse. Safe operations only.
Press Alt-g. All git operations including destructive ones.
Press Alt-n. Inline file renaming with cursor control.
Normal mode is the default state. All operations are safe - you cannot accidentally modify git state or delete files.
The status bar shows nothing special in Normal mode. Git Mode shows
[ GIT MODE ].
Press Alt-g to enter Git Mode. All git operations become available. Press q or ESC to return to Normal Mode.
Navigation (j/k, arrows, Space) still works in Git Mode.
Press Alt-n to enter Rename Mode. The cursor appears in the filename, allowing inline editing.
Note: Enter key doesn't work in cbreak mode. Use Tab to confirm.
On case-insensitive filesystems (macOS HFS+/APFS), fuss handles case-only renames (e.g., "README.md" → "Readme.md") correctly using mv -f.
fuss implements a hierarchical navigation model designed for tree structures. j/k move between siblings at the same depth, while arrows navigate the tree structure.
Press j or ↓ to move to the next sibling at the same depth. Press k or ↑ to move to the previous sibling.
.
├── ▼ src ← j from here...
│ ├── main.f90
│ └── utils.f90
├── ▼ tests ← ...lands here (sibling at depth 1)
│ └── test.f90
└── Makefile ← j again lands here
# Navigation wraps around at each depth level Sibling navigation skips over nested items, making it fast to jump between top-level directories.
Arrow keys navigate the tree structure: right to enter directories, left to return to parent.
Press Space on any directory
to toggle its expanded state. Collapsed directories show ▶,
expanded directories show ▼.
Collapse state is preserved when the tree is rebuilt after git operations. This uses path-based tracking with binary search optimization for large trees.
In Normal Mode, simply start typing to search. Fuss jumps to the best match in real-time using fzf-style scoring.
1. Exact match → 10000 points
2. Prefix match → 5000 points
3. Consecutive chars → 100 + 50×n bonus per char
4. Word boundary → +150 bonus (after /, _, -, .)
5. Start of string → +200 bonus
# Two-pass search: basenames first, then full paths
# Search buffer clears after 500ms of inactivity Each file displays its git status with colored indicators:
| Option | Type | Default | Description |
|---|---|---|---|
↑ (green) | staged | - | Changes staged for commit (git add) |
✗ (red) | modified | - | Tracked file with modifications |
✗ (gray) | untracked | - | New file not yet tracked by git |
↓ (blue) | incoming | - | Changes incoming from remote (after fetch) |
↑✗ | both | - | File has both staged and unstaged changes |
Directory nodes show ▼ when expanded
and ▶ when collapsed. Gitignored
files appear in gray text.
All git operations require Git Mode. Press Alt-g to enter, q to exit.
| Option | Type | Default | Description |
|---|---|---|---|
a | - | - | Stage file or all files in directory (git add) |
u | - | - | Unstage file (git restore --staged) |
S | - | - | Stage all changes (git add --all) |
U | - | - | Unstage all (git restore --staged .) |
x | - | - | Discard changes to file (with confirmation) |
r | - | - | Delete file (with confirmation) |
Pressing a on a directory stages all files recursively within it.
| Option | Type | Default | Description |
|---|---|---|---|
m | - | - | Commit with inline message prompt |
M | - | - | Amend last commit (shows previous message) |
t | - | - | Create tag (with optional message and push) |
Commit uses an inline prompt instead of opening an external editor. Amend shows the previous commit message and lets you edit or keep it.
| Option | Type | Default | Description |
|---|---|---|---|
b | - | - | Switch branch (fzf selection) |
n | - | - | Create new branch (prompts for name) |
R | - | - | Delete branch (fzf selection) |
G | - | - | Merge branch into current |
Branch switching and deletion use fzf for fuzzy selection from all available branches.
| Option | Type | Default | Description |
|---|---|---|---|
p | - | - | Push to remote |
l | - | - | Pull from remote |
f | - | - | Fetch from remote (updates incoming indicators) |
After fetching, files with incoming changes show a blue ↓ indicator. Use d to view incoming diff.
| Option | Type | Default | Description |
|---|---|---|---|
d | - | - | View diff of selected file (staged or incoming) |
c | - | - | View file contents in pager |
w | - | - | Git blame for selected file |
h | - | - | Browse commit history |
L | - | - | Browse reflog |
s | - | - | Show full git status |
| Option | Type | Default | Description |
|---|---|---|---|
z | - | - | Stash changes (with optional message) |
Z | - | - | Pop/apply stash (fzf selection) |
y | - | - | Cherry-pick commit |
v | - | - | Revert commit |
O | - | - | Reset (interactive options) |
I | - | - | Interactive rebase |
q, ESC | - | - | Exit Git Mode (return to Normal Mode) |
fuss is written in modern Fortran 2018, organized into focused modules with clear dependencies. The main program coordinates the interactive loop, while specialized modules handle git operations, tree management, and terminal I/O.
fuss/
├── src/
│ ├── version_module.f90 # Auto-generated from VERSION
│ ├── types_module.f90 # Data structures
│ ├── cache_module.f90 # Git result caching
│ ├── terminal_module.f90 # Terminal I/O
│ ├── git_module.f90 # Git operations
│ ├── tree_module.f90 # Tree construction
│ ├── display_module.f90 # ANSI rendering
│ └── fuss_main.f90 # Main program
├── Makefile # Build system
└── VERSION # Version string | Option | Type | Default | Description |
|---|---|---|---|
version_module | auto-generated | - | Version string from VERSION file |
types_module | data types | - | tree_node, file_entry, selectable_item |
cache_module | performance | - | Git command result caching (500ms TTL) |
terminal_module | I/O | - | Terminal control, raw mode, screen management |
git_module | operations | - | All git operations and file retrieval |
tree_module | data | - | Tree construction, traversal, sorting |
display_module | rendering | - | Tree rendering with ANSI colors |
fuss_main | program | - | Main program, arguments, interactive loop |
Defines the core data structures used throughout fuss:
type :: tree_node
character(len=256) :: name
logical :: is_file
logical :: is_staged, is_unstaged, is_untracked
logical :: has_incoming, is_gitignored
logical :: is_expanded ! For directory collapse
type(tree_node), pointer :: first_child => null()
type(tree_node), pointer :: next_sibling => null()
end type
type :: file_entry
character(len=512) :: path
logical :: is_staged, is_unstaged, is_untracked
logical :: has_incoming, is_gitignored
end type
type :: selectable_item
character(len=512) :: path
logical :: is_file, is_staged, is_unstaged
logical :: is_untracked, has_incoming, is_gitignored
integer :: depth
type(tree_node), pointer :: node => null()
end type Implements a 500ms TTL cache for git command results:
type :: file_cache
real(8) :: timestamp ! CPU time when cached
type(file_entry), allocatable :: files(:)
integer :: n_files
logical :: valid ! False after state changes
end type
! Two caches maintained:
type(file_cache) :: dirty_cache ! git status --porcelain
type(file_cache) :: all_cache ! find + dirty status
! Cache time-to-live (500ms)
real(8), parameter :: CACHE_TTL = 0.5d0 Handles all terminal I/O including raw mode, escape sequences, and screen management:
enter_alternate_screen() ! ESC[?1049h
exit_alternate_screen() ! ESC[?1049l
clear_screen() ! ESC[H ESC[2J
enable_raw_mode() ! stty cbreak -echo
disable_raw_mode() ! stty sane
read_key(key) ! Read single character
read_line(prompt, line) ! Read line with echo
get_terminal_height() ! stty size / tput lines / $LINES Implements all git operations as subroutines. Operations that modify state invalidate the cache before returning.
Handles tree construction, traversal, and sorting:
Renders the tree with ANSI escape codes for colors and highlighting:
fuss uses a first-child/next-sibling linked list structure for efficient memory use and traversal:
# Tree representation:
.
├── src/
│ ├── main.f90
│ └── utils.f90
└── Makefile
# Internal structure:
root (".")
↓ first_child
src ("src")
↓ first_child → next_sibling
main.f90 utils.f90
↓ next_sibling
Makefile Binary search is used for collapsed path restoration after tree rebuilds. Quicksort keeps trees alphabetically sorted at each level.
fuss implements sophisticated input handling to support arrow keys, Alt-key combinations, and fuzzy search.
fuss uses cbreak mode (not fully raw) for input:
! Enable cbreak mode
call execute_command_line('stty cbreak -echo < /dev/tty')
! cbreak mode:
! - Input available character-by-character (no line buffering)
! - Echo disabled (fuss draws its own display)
! - Newlines still processed (unlike raw mode)
! Restore on exit
call execute_command_line('stty sane < /dev/tty') Arrow keys send multi-character escape sequences that fuss decodes:
! Arrow key sequences:
ESC [ A → Up arrow → achar(28)
ESC [ B → Down arrow → achar(29)
ESC [ C → Right arrow → achar(30)
ESC [ D → Left arrow → achar(31)
! Detection:
! 1. Read first character
! 2. If ESC (27), read next
! 3. If '[', read direction char
! 4. Encode as control code Alt-key combinations are sent as ESC followed by the letter:
! Alt-letter sequences:
ESC g → Alt-g → achar(7) ! Enter git mode
ESC n → Alt-n → achar(14) ! Enter rename mode
ESC s → Alt-s → achar(19) ! Show status
ESC v → Alt-v → achar(22) ! View file
! Encoding formula:
! key = achar(1 + ichar(letter) - ichar('a'))
! a=1, b=2, c=3, ..., g=7, ..., n=14, ... The 500ms TTL cache prevents redundant git commands during rapid navigation:
function cache_valid(cache) result(valid)
logical :: valid
real(8) :: current_time
if (.not. cache%valid) then
valid = .false.
return
end if
call cpu_time(current_time)
valid = (current_time - cache%timestamp) < CACHE_TTL
end function
! Cache invalidation called after:
! - All git operations that modify state
! - File rename operations fuss implements fzf-style fuzzy matching with scoring:
function fuzzy_match_score(pattern, text) result(score)
! Exact match → 10000
! Prefix match → 5000
! Each char match → 100
! Consecutive → +50×n bonus
! Word boundary → +150 bonus
! Start of string → +200 bonus
! Gap penalty → -1 per gap char
! Length penalty → -len(text)
end function
! Two-pass search:
! Pass 1: Match against basenames only
! Pass 2: Match against full paths (if no good basename match)
! Search buffer timeout: 500ms fuss implements viewport-based scrolling for large repositories:
! Calculate visible items:
! Fixed UI elements: top_padding + 6 lines
! - top_padding: terminal-specific (2-4 lines)
! - 1: repo:branch header
! - 1: blank line
! - 1: "." root
! - 1: blank before help
! - 2: legend + controls
visible_items = term_height - top_padding - 6
if (visible_items < 3) visible_items = 3
! Center selection in viewport:
viewport_offset = selected - visible_items / 2
! Clamp to valid range:
if (viewport_offset < 1) viewport_offset = 1
if (viewport_offset > n_items - visible_items + 1) then
viewport_offset = n_items - visible_items + 1
end if | Option | Type | Default | Description |
|---|---|---|---|
-h, --help | - | - | Show help with all keybindings |
-v, --version | - | - | Show version and project URL |
-p, --print | - | - | Print tree and exit (non-interactive) |
-a, --all | - | - | Show all files, not just dirty files |
Navigation and browsing (default mode):
| Option | Type | Default | Description |
|---|---|---|---|
j, ↓ | - | - | Navigate to next sibling at same depth |
k, ↑ | - | - | Navigate to previous sibling at same depth |
→ | - | - | Enter directory (expand if collapsed, move to first child) |
← | - | - | Navigate to parent directory |
Space | - | - | Toggle expand/collapse current directory |
. | - | - | Toggle hiding dotfiles and gitignored files |
a-z, 0-9 | - | - | Fuzzy search jump (type to find matching files) |
Backspace | - | - | Remove last character from search buffer |
ESC | - | - | Clear search buffer |
Alt-g | - | - | Enter Git Mode for git operations |
Alt-v | - | - | View selected file in pager |
Alt-s | - | - | Show full git status in pager |
Alt-n | - | - | Enter rename mode for selected file |
Ctrl-c | - | - | Quit fuss |
| Option | Type | Default | Description |
|---|---|---|---|
a | - | - | Stage file or all files in directory (git add) |
u | - | - | Unstage file (git restore --staged) |
S | - | - | Stage all changes (git add --all) |
U | - | - | Unstage all (git restore --staged .) |
x | - | - | Discard changes to file (with confirmation) |
r | - | - | Delete file (with confirmation) |
| Option | Type | Default | Description |
|---|---|---|---|
m | - | - | Commit with inline message prompt |
M | - | - | Amend last commit (shows previous message) |
t | - | - | Create tag (with optional message and push) |
| Option | Type | Default | Description |
|---|---|---|---|
p | - | - | Push to remote |
l | - | - | Pull from remote |
f | - | - | Fetch from remote (updates incoming indicators) |
| Option | Type | Default | Description |
|---|---|---|---|
b | - | - | Switch branch (fzf selection) |
n | - | - | Create new branch (prompts for name) |
R | - | - | Delete branch (fzf selection) |
G | - | - | Merge branch into current |
| Option | Type | Default | Description |
|---|---|---|---|
d | - | - | View diff of selected file (staged or incoming) |
c | - | - | View file contents in pager |
w | - | - | Git blame for selected file |
h | - | - | Browse commit history |
L | - | - | Browse reflog |
s | - | - | Show full git status |
| Option | Type | Default | Description |
|---|---|---|---|
z | - | - | Stash changes (with optional message) |
Z | - | - | Pop/apply stash (fzf selection) |
y | - | - | Cherry-pick commit |
v | - | - | Revert commit |
O | - | - | Reset (interactive options) |
I | - | - | Interactive rebase |
q, ESC | - | - | Exit Git Mode (return to Normal Mode) |
| Option | Type | Default | Description |
|---|---|---|---|
a-z, A-Z, 0-9 | - | - | Type characters at cursor position |
_, -, ., Space | - | - | Type special characters |
←, → | - | - | Move cursor left/right within name |
Backspace | - | - | Delete character before cursor |
Tab | - | - | Confirm rename and save |
ESC | - | - | Cancel rename |
Some terminals need extra padding in alternate screen mode. Fuss auto-detects via $TERM_PROGRAM but may not recognize all terminals.
Workaround: Try a different terminal or resize the window slightly.
If fuss crashes or is killed, terminal may be in raw mode.
Ensure your terminal supports 256 colors and $TERM
is set correctly (e.g., xterm-256color).
fuss must be run inside a git repository. Navigate to a directory containing
a .git folder.
These operations require fzf to be installed.
Some terminals send different escape sequences. Use j/k as alternatives for up/down navigation.
Some terminals intercept Alt keys for menus. Check terminal settings or use Git Mode keybindings directly (enter git mode with q/ESC first).
Ensure you're pressing Tab (not Enter). Enter key doesn't work in cbreak mode.
fuss caches results for 500ms. If still slow:
fuss --help for full keybinding referencefuss --version to verify your installed version