terminal user interface to jujutsu. Focused on speed and clarity
1# AGENTS.md - Coding Agent Guidelines for jj_tui
2
3> A terminal UI for the Jujutsu version control system, built in OCaml with Notty/Nottui.
4
5## Project Structure
6
7```
8jj_tui/
9├── bin/ # Main executable and UI components
10│ ├── main.ml # Entry point
11│ ├── jj_ui.ml # Main UI orchestration
12│ ├── graph_view.ml, graph_commands.ml # Commit graph UI
13│ ├── file_view.ml, file_commands.ml # File diff UI
14│ └── global_vars.ml # Shared state
15├── lib/ # Core library (jj_tui)
16│ ├── ansiReverse.ml # ANSI escape parsing
17│ ├── render_jj_graph.ml # Commit graph rendering
18│ ├── config.ml, key_map.ml # Configuration
19│ └── *_tests.ml # Inline tests
20├── test/lib/ # Additional test library (jj_tui_test)
21└── forks/ # Vendored dependencies (notty, nottui, lwd)
22```
23
24## Build Commands
25
26**Requires Nix** - This project uses Nix for dependency management.
27
28```bash
29# Enter development shell (required first)
30nix develop
31
32# Build
33dune build
34
35# Build and watch
36dune build --watch
37
38# Run the application
39dune exec jj_tui
40
41# Run all tests
42dune runtest
43
44# Run tests for specific library
45dune runtest -p jj_tui
46
47# Run tests and show output
48dune runtest --force
49
50# Format code
51dune fmt
52# or
53ocamlformat -i <file.ml>
54
55# Check formatting without applying
56dune fmt --preview
57```
58
59## GIT:
60This project uses jujutsu not git. for any git command run the jj equivalent
61
62### Running Individual Tests
63
64Tests use `ppx_expect` inline tests. To run tests in a specific file:
65
66```bash
67# Run inline tests for jj_tui library
68dune runtest jj_tui/lib
69
70# Run inline tests for test library
71dune runtest jj_tui/test/lib
72
73# Promote expect test changes (update golden output)
74dune promote
75```
76## Key things to remember:
77- Run `dune build` if there are type errors, often these can be caused by changes across files not being picked up which requires a rebuild
78- If you are getting a type error and can't fix it: Records with the same fields names can cause weird type inference issues, once it's infered it won't change.
79 You need to either:
80 - locally open the module with the type you want,
81 - or do Module.( myrecord.field)
82 - or explicitly annotate the type
83
84## Code Style Guidelines
85
86### OCamlformat Configuration
87
88Uses `profile = janestreet` with customizations. Key settings:
89- `let-binding-spacing = double-semicolon` - End let bindings with `;;`
90- `break-cases = nested` - Multi-line match statements
91- `if-then-else = keyword-first` - Align `then`/`else`
92- `space-around-records/lists/arrays = true` - Spacing for trailing commas
93
94### Import/Open Patterns
95
96```ocaml
97(* Module-level opens at top of file *)
98open Lwd_infix
99open Notty
100open Nottui
101open Jj_tui
102open! Jj_tui.Util (* open! for shadowing *)
103
104(* Functor-based module creation *)
105module Make (Vars : Global_vars.Vars) = struct
106 open Vars
107 module Process = Jj_process.Make (Vars)
108 open Process
109 (* ... *)
110end
111```
112
113### Naming Conventions
114
115- **Functions/values**: `snake_case` - `get_hovered_rev`, `parse_escape_seq`
116- **Types**: `snake_case` - `ui_state_t`, `rev_id`
117- **Modules**: `PascalCase` - `Internal`, `Parser`, `Key_Map`
118- **Type parameters**: `'a`, `'acc`, `'b`
119- **Record fields**: `snake_case` with semicolons
120- **Variant constructors**: `PascalCase` - `Unique`, `Duplicate`, `Apply`
121
122### Type Definitions
123
124```ocaml
125(* Record types - use semicolons before fields *)
126type t = {
127 key_map : Key_map.key_config [@updater]
128 ; single_pane_width_threshold : int
129 ; max_commits : int
130}
131[@@deriving yaml, record_updater ~derive:yaml]
132
133(* Variant types *)
134type 'a maybe_unique =
135 | Unique of 'a
136 | Duplicate of 'a
137```
138
139### Error Handling
140
141```ocaml
142(* Prefer Result for parsing/fallible operations *)
143let of_string remap =
144 match remap with
145 | "up" -> Ok (`Arrow `Up)
146 | _ -> Error (`Msg ("Invalid remap: " ^ remap))
147
148(* Use Option for optional values *)
149Sys.getenv_opt "XDG_CONFIG_HOME" |> Option.value ~default:"~/.config"
150
151(* Exception handling for I/O *)
152try
153 let ic = open_in config_file in
154 (* ... *)
155with
156| Sys_error _ -> default_config
157| ex -> [%log warn "Error: %s" (Printexc.to_string ex)]; default_config
158```
159
160### Lwd Operators (Reactive UI)
161
162```ocaml
163let ( <-$ ) f v = Lwd.map ~f (Lwd.get v)
164let ( $-> ) v f = Lwd.map ~f (Lwd.get v)
165let ( let$$ ) v f = Lwd.map ~f (Lwd.get v)
166let ( |>$ ) v f = Lwd.map ~f v
167let ( >> ) f g x = g (f x) (* Compose left-to-right *)
168let ( << ) f g x = f (g x) (* Compose right-to-left *)
169
170(* Usage *)
171let$ root = root in
172root |> Nottui.Ui.event_filter (...)
173```
174
175### Logging
176
177Uses `logs-ppx` with custom timestamp wrapper:
178
179```ocaml
180open Jj_tui.Logging
181
182[%log info "Loading config..."]
183[%log warn "Error parsing config: %s" msg]
184[%log debug "Old logs cleaned up"]
185```
186
187### Testing (ppx_expect)
188
189```ocaml
190let%expect_test "test_name" =
191 let result = some_function () in
192 print_endline result;
193 [%expect {|
194 expected output here
195 |}]
196;;
197```
198
199### Documentation Comments
200
201```ocaml
202(** Module-level documentation *)
203
204(** Function documentation - concise, one line preferred *)
205let get_unique_id maybe_unique_rev = ...
206
207(**
208 Multi-line documentation for complex functions.
209 Explains algorithm or non-obvious behavior.
210*)
211```
212
213## Key Patterns
214
215### Functor-Based Dependency Injection
216
217```ocaml
218module Make (Vars : Global_vars.Vars) = struct
219 (* Access Vars.* throughout the module *)
220end
221```
222
223### UI State Management
224
225Global state in `Global_vars.Vars` using `Lwd.var`:
226
227```ocaml
228type ui_state_t = {
229 view : [`Main | `Cmd_I of cmd_args | ...] Lwd.var
230 ; hovered_revision : string maybe_unique Lwd.var
231 (* ... *)
232}
233```
234
235### Command Handling
236
237Commands defined in `graph_commands.ml` / `file_commands.ml`:
238
239```ocaml
240type command =
241 | Cmd of string list
242 | Cmd_async of string * string list
243 | Dynamic of (unit -> command)
244 | Selection_prompt of (...)
245```
246
247## Dependencies
248
249Key libraries:
250- **nottui** / **nottui_picos**: Terminal UI framework (forked)
251- **lwd** / **lwd_picos**: Reactive programming (forked)
252- **notty**: Terminal rendering (forked)
253- **angstrom**: Parser combinators
254- **picos**: Multicore/async runtime
255- **ppx_expect**: Inline testing
256- **ppx_deriving_yaml/yojson**: Serialization
257
258## Common Pitfalls
259
2601. **End `let` bindings with `;;`** - Required by ocamlformat config
2612. **Use Lwd operators** - Don't call `Lwd.get` directly in render functions
2623. **Functor pattern** - Most modules require `Make(Vars)` instantiation
2634. **Vendored forks** - Don't modify files in `forks/` unless necessary
2645. **Nix required** - Build system not set up for pure opam/dune
265
266
267