···11+# Changes to Match Original JJ Rendering Format
22+33+## Goal
44+Match the original `jj log` output format which displays commit information on two lines:
55+66+**Original jj format:**
77+```
88+@ ztooztwk eli.jambu@gmail.com 2026-01-15 14:05:59 235795c5
99+│ (empty) (no description set)
1010+```
1111+1212+## Changes Made
1313+1414+### 1. Updated `render_commit_content` in `graph_view.ml` (lines 19-79)
1515+1616+**Previous format (single line):**
1717+```
1818+change_id author_name timestamp (bookmarks) description
1919+```
2020+2121+**New format (two lines):**
2222+```
2323+Line 1: change_id email timestamp [bookmarks] commit_id_short
2424+Line 2: (empty) description
2525+```
2626+2727+**Key changes:**
2828+- Show **full email** instead of extracting name before `@`
2929+- Add **commit_id_short** (8 characters) at end of line 1
3030+- Move **description** to line 2
3131+- Add **(empty)** prefix when `node.empty` is true
3232+- Remove parentheses around bookmarks, show as space-separated list
3333+- Use `I.vcat` to create two-line image
3434+3535+### 2. Updated `render_graph_row` in `graph_view.ml` (lines 81-109)
3636+3737+**Problem:** When content is multi-line, the graph character only appears on the first line with `I.hcat [ graph_img; content_img ]`.
3838+3939+**Solution:** Detect multi-line content and manually create graph continuation for subsequent lines:
4040+4141+```ocaml
4242+if content_height > 1 then
4343+ (* Replace node glyphs (○, @, ◌, ◆) with vertical bar │ *)
4444+ let graph_continuation = replace_node_glyphs_with_bar row.graph_chars in
4545+ (* Create each line with appropriate graph prefix *)
4646+ let lines = List.init content_height (fun i ->
4747+ let line_img = I.vcrop i 1 content_img in
4848+ if i = 0 then I.hcat [ graph_img; line_img ]
4949+ else I.hcat [ graph_continuation; line_img ]
5050+ ) in
5151+ I.vcat lines
5252+else
5353+ I.hcat [ graph_img; content_img ]
5454+```
5555+5656+**How it works:**
5757+1. Check if content height > 1
5858+2. Create `graph_continuation` by replacing all node glyphs with `│`
5959+3. For each line:
6060+ - Line 0: Use original `graph_chars` (contains node glyph)
6161+ - Line 1+: Use `graph_continuation` (node glyph replaced with `│`)
6262+4. Vertically stack all lines
6363+6464+### 3. Color Scheme
6565+6666+**Line 1:**
6767+- `change_id`: cyan (bold cyan if working_copy, yellow if empty, lightmagenta if immutable)
6868+- `email`: dim white
6969+- `timestamp`: dim white
7070+- `bookmarks`: bold green
7171+- `commit_id_short`: dim cyan
7272+7373+**Line 2:**
7474+- `description`: white (dim if empty/preview, lightyellow if wip)
7575+7676+## Examples
7777+7878+### Simple commit (empty)
7979+```
8080+@ ztooztwk eli.jambu@gmail.com 2026-01-15 14:05:59 235795c5
8181+│ (empty) (no description set)
8282+```
8383+8484+### Commit with description
8585+```
8686+○ smqmznlq eli.jambu@gmail.com 2026-01-15 01:40:23 09a9f33f
8787+│ Add new feature
8888+```
8989+9090+### Commit with bookmarks
9191+```
9292+◆ noszsqtm eli.jambu@gmail.com 2025-11-22 00:26:06 main master 35b532af
9393+│ remove aarch64 linux because it doesn't seem to work
9494+```
9595+9696+### Complex graph
9797+```
9898+│ ○ nkwwwlnw eli.jambu@gmail.com 2026-01-15 00:30:16 89abd641
9999+│ │ rewrite
100100+```
101101+102102+## Testing
103103+104104+- ✅ `dune build` - compiles successfully
105105+- ✅ `dune runtest` - all existing tests pass
106106+- ✅ Multi-line rendering works correctly
107107+- ✅ Graph continuation characters display properly
108108+109109+## Files Modified
110110+111111+1. **`jj_tui/bin/graph_view.ml`**
112112+ - `render_commit_content` (lines 19-79): Two-line format
113113+ - `render_graph_row` (lines 81-109): Multi-line graph handling
114114+115115+## No Breaking Changes
116116+117117+- All existing functionality preserved
118118+- Tests pass without modification
119119+- Only visual formatting changed
···11(executable
22 (public_name jj_tui)
33 (name main)
44- (modes byte native )
44+ (modes byte native)
55 (libraries
66 signal
77 jj_tui
···3030;; (useful in CI), otherwise falls back to `git describe`. If neither is
3131;; available, it writes "unknown". This uses only shell builtins and common
3232;; utilities (no python required).
3333+3334(rule
3435 (targets version.ml)
3536 (action
3636- (with-stdout-to version.ml
3737- (run sh -c
3838- "v=${GIT_DESCRIBE:-$(git describe --tags --always --dirty 2>/dev/null || echo unknown)};
3939- esc=$(printf '%s' \"$v\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\\\"/g');
4040- printf 'let version = \"%s\"\\n' \"$esc\""
4141- ))))
3737+ (with-stdout-to
3838+ version.ml
3939+ (run
4040+ sh
4141+ -c
4242+ "v=${GIT_DESCRIBE:-$(git describe --tags --always --dirty 2>/dev/null || echo unknown)};\n esc=$(printf '%s' \"$v\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\\\"/g');\n printf 'let version = \"%s\"\\n' \"$esc\""))))
+43-48
jj_tui/bin/file_commands.ml
···14141515 (* Define all file commands *)
1616 let get_command_registry active_files get_commands =
1717- [ {
1717+ [
1818+ {
1819 id = "show_help"
1920 ; description = "Show help"
2021 ; sorting_key = 0.0
···3637 ( "Revision to move file to"
3738 , fun rev ->
3839 Cmd
3939- ( [ "squash"
4040- ; "-u"
4141- ; "--keep-emptied"
4242- ; "--from"
4343- ; get_hovered_rev ()
4444- ; "--into"
4545- ; rev
4646- ]
4747- @ Lwd.peek active_files ) ))
4040+ ([
4141+ "squash"
4242+ ; "-u"
4343+ ; "--keep-emptied"
4444+ ; "--from"
4545+ ; get_hovered_rev ()
4646+ ; "--into"
4747+ ; rev
4848+ ]
4949+ @ Lwd.peek active_files) ))
4850 }
4951 ; {
5052 id = "move_to_child"
···5557 Dynamic_r
5658 (fun rev ->
5759 Cmd
5858- ( [ "squash"
5959- ; "-u"
6060- ; "--keep-emptied"
6161- ; "--from"
6262- ; rev
6363- ; "--into"
6464- ; rev ^ "+"
6565- ]
6666- @ Lwd.peek active_files )))
6060+ ([
6161+ "squash"; "-u"; "--keep-emptied"; "--from"; rev; "--into"; rev ^ "+"
6262+ ]
6363+ @ Lwd.peek active_files)))
6764 }
6865 ; {
6966 id = "move_to_parent"
···7471 Dynamic_r
7572 (fun rev ->
7673 Cmd
7777- ( [ "squash"
7878- ; "-u"
7979- ; "--keep-emptied"
8080- ; "--from"
8181- ; rev
8282- ; "--into"
8383- ; rev ^ "-"
8484- ]
8585- @ Lwd.peek active_files )))
7474+ ([
7575+ "squash"; "-u"; "--keep-emptied"; "--from"; rev; "--into"; rev ^ "-"
7676+ ]
7777+ @ Lwd.peek active_files)))
8678 }
8779 ; {
8880 id = "commit"
···9385 PromptThen
9486 ( "Commit message"
9587 , fun message ->
9696-9797- Fun
9898- (fun _-> (* I need this to work with any commit. So i should split then describe instead*)
9999- let rev=Vars.get_hovered_rev () in
8888+ Fun
8989+ (fun _ ->
9090+ (* I need this to work with any commit. So i should split then describe instead*)
9191+ let rev = Vars.get_hovered_rev () in
10092 jj
101101- ( [ "split";"-r"; rev; "-m";message; "--insert-before"; "@" ]
102102- @ Lwd.peek active_files )|>ignore;
103103- )))
9393+ ([ "split"; "-r"; rev; "-m"; message; "--insert-before"; "@" ]
9494+ @ Lwd.peek active_files)
9595+ |> ignore) ))
10496 }
10597 ; {
10698 id = "abandon"
···116108 ^ (selected |> String.concat "\n")
117109 ^ "\nin rev "
118110 ^ rev)
119119- (Cmd (["restore"; "--to"; rev; "--from"; rev ^ "-"] @ selected))))
111111+ (Cmd ([ "restore"; "--to"; rev; "--from"; rev ^ "-" ] @ selected))))
120112 }
121113 ; {
122114 id = "absorb"
123123- ; description = "Move changes from this revision to the nearest parent that modified the same lines"
115115+ ; description =
116116+ "Move changes from this revision to the nearest parent that modified the same \
117117+ lines"
124118 ; sorting_key = 5.0
125119 ; make_cmd =
126120 (fun () ->
···132126 ^ (selected |> String.concat "\n")
133127 ^ "\nin rev "
134128 ^ rev)
135135- (Cmd (["absorb"; "--from"; rev] @ selected))))
129129+ (Cmd ([ "absorb"; "--from"; rev ] @ selected))))
136130 }
137131 ; {
138132 id = "absorb-into"
···143137 PromptThen
144138 ( "Revision to move file to"
145139 , fun dest ->
146146- Dynamic_r
147147- (fun rev ->
148148- let selected = Lwd.peek active_files in
149149- confirm_prompt
150150- ("absorb all changes to:\n"
151151- ^ (selected |> String.concat "\n")
152152- ^ "\nin rev "
153153- ^ rev)
154154- (Cmd (["absorb"; "--from"; rev;"--to"; dest] @ selected)))))
140140+ Dynamic_r
141141+ (fun rev ->
142142+ let selected = Lwd.peek active_files in
143143+ confirm_prompt
144144+ ("absorb all changes to:\n"
145145+ ^ (selected |> String.concat "\n")
146146+ ^ "\nin rev "
147147+ ^ rev)
148148+ (Cmd ([ "absorb"; "--from"; rev; "--to"; dest ] @ selected))) ))
155149 }
156150 ; {
157151 id = "undo"
···163157 |> List.to_seq
164158 |> Seq.map (fun x -> x.id, x)
165159 |> Hashtbl.of_seq
166166-end
160160+ ;;
161161+end
+7-7
jj_tui/bin/file_view.ml
···12121313 (* Import file commands *)
1414 module FileCommands = File_commands.Make (Vars)
1515-1615 open Jj_tui.Key_map
1616+1717 let active_files = Lwd.var [ "" ]
18181919 (* Remove the hardcoded make_command_mapping function and use the dynamic one *)
2020 let command_mapping = ref None
2121-2121+2222 let rec get_command_mapping () =
2323 match !command_mapping with
2424- | Some mapping -> mapping
2424+ | Some mapping ->
2525+ mapping
2526 | None ->
2627 let key_map = (Lwd.peek ui_state.config).key_map.file in
2728 let registry = FileCommands.get_command_registry active_files get_command_mapping in
···2930 command_mapping := Some mapping;
3031 mapping
3132 ;;
3232-3333+3334 let hovered_var = ref "./"
34353536 let file_view ~focus summary_focus =
···6667 | `Enter, [] ->
6768 Focus.request_reversable summary_focus;
6869 `Handled
6969- | k ->
7070- handleInputs (get_command_mapping ()) k
7070+ | k ->
7171+ handleInputs (get_command_mapping ()) k
7172 | _ ->
7273 `Unhandled)
7374 in
···7980 in
8081 ui
8182 ;;
8282-8383end
+3-9
jj_tui/bin/global_vars.ml
···4141}
42424343let get_unique_id maybe_unique_rev =
4444- match maybe_unique_rev with
4545- | Unique s ->
4646- s
4747- | Duplicate s ->
4848- s
4444+ match maybe_unique_rev with Unique s -> s | Duplicate s -> s
4945;;
50465147(** Global variables for the ui. Here we keep anything that's just a pain to pipe around*)
···8278 ; jj_show_promise = ref @@ Promise.of_value ()
8379 ; jj_branches = Lwd.var I.empty
8480 ; jj_change_files = Lwd.var []
8585- ; hovered_revision =
8686- Lwd.var (Unique "@")
8787- ; selected_revisions =
8888- Lwd.var [ Unique "@"; ]
8181+ ; hovered_revision = Lwd.var (Unique "@")
8282+ ; selected_revisions = Lwd.var [ Unique "@" ]
8983 ; revset = Lwd.var None
9084 ; graph_revs = Lwd.var [||]
9185 ; input = Lwd.var `Normal
+11-17
jj_tui/bin/graph_commands.ml
···5050 let rev_args = List.concat_map (fun r -> [ "-r"; r ]) revs in
5151 let title = Printf.sprintf "Git push (%s) will:" remote in
5252 let dry_run_cmd =
5353- [ "git"; "push"; "--allow-new"; "--dry-run"; "--remote"; remote ]
5454- @ rev_args
5353+ [ "git"; "push"; "--allow-new"; "--dry-run"; "--remote"; remote ] @ rev_args
5554 in
5655 let real_cmd =
5756 Cmd_async
5857 ( "pushing to remote..."
5959- , [ "git"; "push"; "--allow-new"; "--remote"; remote ]
6060- @ rev_args )
5858+ , [ "git"; "push"; "--allow-new"; "--remote"; remote ] @ rev_args )
6159 in
6260 confirm_dry_run_prompt ~title ~dry_run_cmd ~real_cmd)
6361 in
···6664 (fun () ->
6765 let title = Printf.sprintf "Git push (%s) will:" remote in
6866 let dry_run_cmd =
6969- [ "git"; "push";"--deleted"; "--allow-new"; "--dry-run"; "--remote"; remote ]
7070-6767+ [ "git"; "push"; "--deleted"; "--allow-new"; "--dry-run"; "--remote"; remote ]
7168 in
7269 let real_cmd =
7370 Cmd_async
7471 ( "pushing to remote..."
7575- , [ "git"; "push"; "--allow-new"; "--deleted"; "--remote"; remote ]
7676- )
7272+ , [ "git"; "push"; "--allow-new"; "--deleted"; "--remote"; remote ] )
7773 in
7874 confirm_dry_run_prompt ~title ~dry_run_cmd ~real_cmd)
7975 in
···9086 ; sort_key = 0.
9187 ; description = Printf.sprintf "git push hovered to %s" remote
9288 ; cmd = push_cmd ()
9393- } );
9494- ( Key.key_of_string_exn "P"
8989+ } )
9090+ ; ( Key.key_of_string_exn "P"
9591 , {
9692 key = Key.key_of_string_exn "P"
9793 ; sort_key = 0.1
9894 ; description = Printf.sprintf "git push all to %s" remote
9999- ; cmd = push_all_cmd()
9595+ ; cmd = push_all_cmd ()
10096 } )
10197 ; ( Key.key_of_string_exn "f"
10298 , {
···356352 let rev_args = revs |> List.concat_map (fun x -> [ "-r"; x ]) in
357353 let title = "Git push will:" in
358354 let dry_run_cmd =
359359- [ "git"; "push"; "--allow-new"; "--dry-run" ] @ rev_args
355355+ [ "git"; "push"; "--allow-new"; "--dry-run" ] @ rev_args
360356 in
361357 let real_cmd =
362358 Cmd_async
363363- ( "pushing to remote..."
364364- , [ "git"; "push"; "--allow-new" ] @ rev_args )
359359+ ("pushing to remote...", [ "git"; "push"; "--allow-new" ] @ rev_args)
365360 in
366361 confirm_dry_run_prompt ~title ~dry_run_cmd ~real_cmd))
367362 }
···375370 (fun () ->
376371 let title = "Git push will:" in
377372 let dry_run_cmd =
378378- [ "git"; "push"; "--deleted"; "--allow-new"; "--dry-run" ]
373373+ [ "git"; "push"; "--deleted"; "--allow-new"; "--dry-run" ]
379374 in
380375 let real_cmd =
381376 Cmd_async
382382- ( "pushing to remote..."
383383- , [ "git"; "push"; "--deleted"; "--allow-new" ] )
377377+ ("pushing to remote...", [ "git"; "push"; "--deleted"; "--allow-new" ])
384378 in
385379 confirm_dry_run_prompt ~title ~dry_run_cmd ~real_cmd))
386380 }
+90-74
jj_tui/bin/graph_view.ml
···1616 (* Import graph commands *)
1717 module GraphCommands = Graph_commands.Make (Vars)
18181919- (** Render commit content for a node - shows change_id, author, timestamp, description, bookmarks *)
2020- let render_commit_content (node : Render_jj_graph.node) : Notty.image =
2121- let open Notty in
2222- let open Notty.A in
2323- let styled_text attr text = I.string attr text in
2424- let change_id_short =
2525- String.sub node.change_id 0 (min 8 (String.length node.change_id))
2626- in
2727- let author_name =
2828- match String.split_on_char '@' node.author_email with
2929- | name :: _ ->
3030- name
1919+ (* Use the library's render function for commit content *)
2020+ let render_commit_content = Commit_render.render_commit_content
2121+2222+ (** Group rows by their owning node. Each group is (node_row, continuation_rows).
2323+ Each NodeRow starts a new group containing it and all following non-NodeRows until
2424+ the next NodeRow. *)
2525+ let group_rows_by_node (rows : Render_jj_graph.graph_row_output list) :
2626+ (Render_jj_graph.graph_row_output * Render_jj_graph.graph_row_output list) list
2727+ =
2828+ let open Render_jj_graph in
2929+ let rec loop acc current_group = function
3130 | [] ->
3232- node.author_email
3333- in
3434- let description_line =
3535- match String.split_on_char '\n' node.description with
3636- | first :: _ when String.trim first <> "" ->
3737- String.trim first
3838- | _ ->
3939- "(no description set)"
4040- in
4141- let parts = ref [] in
4242- let change_id_attr =
4343- if node.is_preview
4444- then fg lightblack ++ st dim
4545- else if node.working_copy
4646- then fg lightcyan ++ st bold
4747- else if node.immutable
4848- then fg lightmagenta
4949- else if node.empty
5050- then fg yellow
5151- else fg cyan
3131+ List.rev (match current_group with Some g -> g :: acc | None -> acc)
3232+ | row :: rest ->
3333+ (match row.row_type with
3434+ | NodeRow ->
3535+ let acc = match current_group with Some g -> g :: acc | None -> acc in
3636+ loop acc (Some (row, [])) rest
3737+ | _ ->
3838+ (match current_group with
3939+ | Some (node_row, conts) ->
4040+ loop acc (Some (node_row, conts @ [ row ])) rest
4141+ | None ->
4242+ (* Orphan row, shouldn't happen, but skip it *)
4343+ loop acc None rest))
5244 in
5353- parts := styled_text change_id_attr change_id_short :: !parts;
5454- parts := styled_text (fg white ++ st dim) (" " ^ author_name) :: !parts;
5555- parts := styled_text (fg white ++ st dim) (" " ^ node.author_timestamp) :: !parts;
5656- if List.length node.bookmarks > 0
5757- then (
5858- let bookmarks_str = " (" ^ String.concat ", " node.bookmarks ^ ")" in
5959- parts := styled_text (fg green ++ st bold) bookmarks_str :: !parts);
6060- let desc_attr =
6161- if node.is_preview || node.empty
6262- then fg white ++ st dim
6363- else if node.wip
6464- then fg lightyellow
6565- else fg white
6666- in
6767- parts := styled_text desc_attr (" " ^ description_line) :: !parts;
6868- !parts |> List.rev |> I.hcat
4545+ loop [] None rows
6946 ;;
70477171- (** Render a graph row by combining graph prefix with content *)
7272- let render_graph_row
7373- (row : Render_jj_graph.graph_row_output)
7474- ~(render_content : Render_jj_graph.node -> Notty.image) : Notty.image
4848+ (** Render a node group by distributing content lines across available rows.
4949+ Returns a list of (row, rendered_image) pairs. *)
5050+ let render_node_group
5151+ ((node_row, continuation_rows) :
5252+ Render_jj_graph.graph_row_output * Render_jj_graph.graph_row_output list)
5353+ ~(render_content : Render_jj_graph.node -> Notty.image list) :
5454+ (Render_jj_graph.graph_row_output * Notty.image) list
7555 =
7656 let open Notty in
7777- let graph_img = I.string A.empty row.graph_chars in
7878- match row.row_type with
7979- | NodeRow ->
8080- let content_img = render_content row.node in
8181- I.hcat [ graph_img; content_img ]
8282- | LinkRow | PadRow | TermRow ->
8383- graph_img
5757+ let open Render_jj_graph in
5858+ let content_lines = render_content node_row.node in
5959+6060+ let available_rows = node_row :: continuation_rows in
6161+ let result = ref [] in
6262+ (* Distribute content lines across available rows *)
6363+ List.iteri
6464+ (fun i row ->
6565+ let graph_img = row.graph_image in
6666+ let combined =
6767+ if i < List.length content_lines
6868+ then I.hcat [ graph_img; List.nth content_lines i ]
6969+ else graph_img
7070+ in
7171+ result := (row, combined) :: !result)
7272+ available_rows;
7373+ (* If content needs more lines than available, add synthetic continuation rows *)
7474+ if List.length content_lines > List.length available_rows
7575+ then (
7676+ let node_glyphs = [ "○"; "@"; "◌"; "◆" ] in
7777+ let synthetic_graph =
7878+ let chars = node_row.graph_chars in
7979+ let replaced = ref chars in
8080+ List.iter
8181+ (fun glyph ->
8282+ replaced := Str.global_replace (Str.regexp_string glyph) "│" !replaced)
8383+ node_glyphs;
8484+ I.string A.empty !replaced
8585+ in
8686+ for i = List.length available_rows to List.length content_lines - 1 do
8787+ let line_img = List.nth content_lines i in
8888+ result := (node_row, I.hcat [ synthetic_graph; line_img ]) :: !result
8989+ done);
9090+ List.rev !result
8491 ;;
85928693 let bookmark_select_prompt get_bookmark_list name func =
···137144 let state =
138145 Render_jj_graph.{ depth = 0; columns = [||]; pending_joins = [] }
139146 in
140140- let rendered_rows = Render_jj_graph.render_nodes_structured state nodes in
147147+ let rendered_rows = Render_jj_graph.render_nodes_structured state nodes ~node_attr:(Commit_render.graph_node_attr) in
141148 error_var $= None;
142149 rendered_rows, rev_ids
143150 with
···149156 (*We will make two arrays, one with both selectable and filler and one with only selectable*)
150157 let selectable_idx = ref 0 in
151158 let selectable_items = Array.make (Array.length rev_ids) (Obj.magic ()) in
159159+ (* Group rows by node and render each group with content distribution *)
160160+ let grouped_rows = group_rows_by_node rendered_rows in
152161 let items =
153153- rendered_rows
154154- |> List.map (fun (row : Render_jj_graph.graph_row_output) ->
155155- match row.row_type with
156156- | NodeRow ->
157157- let ui =
158158- W.Lists.selectable_item
159159- (render_graph_row row ~render_content:render_commit_content |> Ui.atom)
160160- in
162162+ grouped_rows
163163+ |> List.concat_map (fun group ->
164164+ let rendered_group =
165165+ render_node_group group ~render_content:render_commit_content
166166+ in
167167+ (* Convert rendered group to list items: first is Selectable, rest are Fillers *)
168168+ match rendered_group with
169169+ | [] ->
170170+ []
171171+ | (_first_row, first_img) :: rest_rows ->
161172 let id = rev_ids.(!selectable_idx) in
173173+ let selectable_ui = W.Lists.selectable_item (first_img |> Ui.atom) in
162174 let data =
163175 W.Lists.
164176 {
165165- ui
177177+ ui = selectable_ui
166178 ; id = id |> Global_vars.get_unique_id |> String.hash
167179 ; data = rev_ids.(!selectable_idx)
168180 }
169181 in
170170- (*Add to our selectable array*)
182182+ (* Add to our selectable array *)
171183 Array.set selectable_items !selectable_idx data;
172184 selectable_idx := !selectable_idx + 1;
173173- W.Lists.(Selectable data)
174174- | LinkRow | PadRow | TermRow ->
175175- let graph_img = I.string A.empty row.graph_chars in
176176- W.Lists.(Filler (graph_img |> Ui.atom |> Lwd.pure)))
185185+ let first_item = W.Lists.(Selectable data) in
186186+ (* All other rows in the group become fillers *)
187187+ let filler_items =
188188+ List.map
189189+ (fun (_row, img) -> W.Lists.(Filler (img |> Ui.atom |> Lwd.pure)))
190190+ rest_rows
191191+ in
192192+ first_item :: filler_items)
177193 |> Array.of_list
178194 in
179195 items
···1919 let output = Buffer.create buffer_size in
2020 let rec read_loop () =
2121 match Unix.read fd buffer 0 buffer_size with
2222- | 0 -> Buffer.contents output (* EOF reached *)
2222+ | 0 ->
2323+ Buffer.contents output (* EOF reached *)
2324 | n ->
2425 Buffer.add_subbytes output buffer 0 n;
2526 read_loop ()
2627 | exception Unix.Unix_error (Unix.EINTR, _, _) ->
2728 read_loop ()
2829 | exception Unix.Unix_error (Unix.EBADF, _, _) ->
2929- Buffer.contents output (* Handle EBADF error *)
3030+ Buffer.contents output (* Handle EBADF error *)
3031 | exception Unix.Unix_error (Unix.EAGAIN, _, _) ->
3131- Unix.sleepf 0.01; (* Short sleep to avoid busy waiting *)
3232+ Unix.sleepf 0.01;
3333+ (* Short sleep to avoid busy waiting *)
3234 read_loop ()
3335 in
3436 read_loop ()
3737+;;
35383639module Make (Vars : Global_vars.Vars) = struct
3740 (** Makes a new process that has acess to all input and output
···4043 let stdout = Unix.stdout in
4144 let stdin = Unix.stdin in
4245 (* Create a pipe for stderr to capture it *)
4343- let stderr_r, stderr_w = Unix.pipe () in
4646+ let stderr_r, stderr_w = Unix.pipe () in
4447 let pid = Unix.create_process command.(0) command stdin stdout stderr_w in
4548 (* Close write end in parent *)
4646- Unix.close stderr_w;
4747- let _, status = Unix.waitpid [] pid in
4848- (* Read stderr contents *)
4949- let stderr_content = read_fd_contents stderr_r in
5050- Unix.close stderr_r;
5151- status, stderr_content
4949+ Unix.close stderr_w;
5050+ let _, status = Unix.waitpid [] pid in
5151+ (* Read stderr contents *)
5252+ let stderr_content = read_fd_contents stderr_r in
5353+ Unix.close stderr_r;
5454+ status, stderr_content
5255 ;;
53565457 (*
···134137 let@ stdout_o, stdout_i =
135138 finally
136139 (fun (o, i) ->
137137- Unix.close o;
138138- dispose i)
140140+ Unix.close o;
141141+ dispose i)
139142 (Picos_io.Unix.pipe ~cloexec:true)
140143 in
141144 let@ stdin_o, stdin_i =
142145 finally
143146 (fun (o, i) ->
144144- Unix.close i;
145145- dispose o)
147147+ Unix.close i;
148148+ dispose o)
146149 (Picos_io.Unix.pipe ~cloexec:true)
147150 in
148151 let@ stderr_o, stderr_i =
149152 finally
150153 (fun (o, i) ->
151151- Unix.close o;
152152- dispose i)
154154+ Unix.close o;
155155+ dispose i)
153156 (Picos_io.Unix.pipe ~cloexec:true)
154157 in
155158 (* This should ensure that all children processes are killed before we cleanup the pipes*)
···158161 let@ pid =
159162 finally
160163 (fun pid ->
161161- (* if the process didn't finish we will kill the process and then wait it's pid to release the pid*)
162162- if not !isDone
163163- then (
164164- try
165165- [%log debug "pid: %i Cleaning up cancelled command %s" pid (args |> String.concat " ")];
166166- Unix.kill pid Sys.sigkill;
167167- Unix.waitpid [ Unix.WUNTRACED ] pid |> ignore
168168- with
169169- | _ ->
170170- ()))
164164+ (* if the process didn't finish we will kill the process and then wait it's pid to release the pid*)
165165+ if not !isDone
166166+ then (
167167+ try
168168+ [%log
169169+ debug
170170+ "pid: %i Cleaning up cancelled command %s"
171171+ pid
172172+ (args |> String.concat " ")];
173173+ Unix.kill pid Sys.sigkill;
174174+ Unix.waitpid [ Unix.WUNTRACED ] pid |> ignore
175175+ with
176176+ | _ ->
177177+ ()))
171178 (fun _ ->
172172- Unix.create_process_env
173173- cmd
174174- (cmd :: args |> Array.of_list)
175175- (Unix.environment ())
176176- stdin_o
177177- stdout_i
178178- stderr_i)
179179+ Unix.create_process_env
180180+ cmd
181181+ (cmd :: args |> Array.of_list)
182182+ (Unix.environment ())
183183+ stdin_o
184184+ stdout_i
185185+ stderr_i)
179186 in
180180-181181- [%log debug "pid: %i started" pid ];
187187+ [%log debug "pid: %i started" pid];
182188 let prom = Flock.fork_as_promise (fun () -> Unix.waitpid [] pid) in
183189 (* Close unused pipe ends in the parent process *)
184190 Unix.close stdout_i;
···193199 isDone := true;
194200 (* let stderr = read_fd_to_end stderr_i in *)
195201 (* let stdout= ""in *)
196196- code, status, stdout, stderr,pid
202202+ code, status, stdout, stderr, pid
197203 ;;
198198-199204200205 (* Ui_loop.run (Lwd.pure (W.printf "Hello world"));; *)
201206 let cmdArgs cmd args =
202207 let start_time = Unix.gettimeofday () in
203203- let code, status, out_content, err_content,pid = picos_process cmd args in
208208+ let code, status, out_content, err_content, pid = picos_process cmd args in
204209 let end_time = Unix.gettimeofday () in
205205- let exit_code_text=
210210+ let exit_code_text =
206211 match status with
207212 | Unix.WEXITED code ->
208213 Printf.sprintf "exit: %i" code
209209- | Unix.WSIGNALED x->
214214+ | Unix.WSIGNALED x ->
210215 Printf.sprintf "signalled: %i" x
211211- | Unix.WSTOPPED x->
216216+ | Unix.WSTOPPED x ->
212217 Printf.sprintf "stopped: %i" x
213213- in
214214-218218+ in
215219 [%log
216220 debug
217221 "Executing pid:%i %s '%s %s' took: %fms "
···318322 in
319323 on_start ();
320324 Picos_std_structured.Flock.fork (fun () ->
321321- try run ()
322322- with
325325+ try run () with
323326 | exn ->
324327 let msg = Printexc.to_string exn in
325328 [%log warn "Exception in jj_async: %s" msg];
+7-7
jj_tui/bin/jj_widgets.ml
···3737 data = name
3838 ; id = name |> String.hash
3939 ; ui =
4040- str
4040+ str
4141 |> Jj_tui.AnsiReverse.colored_string
4242 |> Ui.atom
4343 |> Ui.resize ~w:100 ~h:1 ~mw:100
···7979 let lines = String.split_on_char '\n' log in
8080 lines
8181 |> List.filter_map (fun line ->
8282- if line |>String.trim|> String.length =0
8282+ if line |> String.trim |> String.length = 0
8383 then None
8484 else (
8585 match Base.String.lsplit2 ~on:' ' line with
8686- | Some (name, _) -> Some (name, line)
8787- | None -> Some (line, line)))
8686+ | Some (name, _) ->
8787+ Some (name, line)
8888+ | None ->
8989+ Some (line, line)))
8890 ;;
89919092 let get_remotes_selectable () =
···9597 data = name
9698 ; id = name |> String.hash
9799 ; ui =
9898- str
100100+ str
99101 |> Jj_tui.AnsiReverse.colored_string
100102 |> Ui.atom
101103 |> Ui.resize ~w:100 ~h:1 ~mw:100
···208210 | _ ->
209211 `Unhandled)
210212 ;;
211211-212212-213213end
+12-7
jj_tui/bin/main.ml
···44module Jj_ui = Jj_ui.Make (Vars)
55open Picos_std_structured
66open Jj_tui.Logging
77+78let () =
89 (* Handle --version / -v early. A module `Version` is expected to be
910 available (provided by `version.ml`, generated at build-time or present
1011 as a fallback). It should expose `val version : string`. Use
1112 `Version.version` here so the code refers to that module explicitly. *)
1212- if Array.length Sys.argv > 1 then
1313+ if Array.length Sys.argv > 1
1414+ then (
1315 match Sys.argv.(1) with
1416 | "--version" | "-v" ->
1517 print_endline Version.version;
1618 exit 0
1717- | _ -> ();
1818- Ui.global_config.border_style<-Nottui.Ui.Border.unicode_rounded;
1919- Ui.global_config.border_style_focused<-Nottui.Ui.Border.unicode_rounded;
2020- (* Ui.global_config.border_attr<-A.empty; *)
2121- (* Ui.global_config.border_attr_focused<-; *)
1919+ | _ ->
2020+ ();
2121+ Ui.global_config.border_style <- Nottui.Ui.Border.unicode_rounded;
2222+ Ui.global_config.border_style_focused <- Nottui.Ui.Border.unicode_rounded)
2223;;
2424+2525+(* Ui.global_config.border_attr<-A.empty; *)
2626+(* Ui.global_config.border_attr_focused<-; *)
2727+2328let await_read_unix fd timeout : [ `Ready | `NotReady ] =
2429 let rec select () =
2530 match Unix.select [ fd ] [] [ fd ] timeout with
···6166 if term_width <> prev_term_width || term_height <> prev_term_height
6267 then Lwd.set Vars.term_width_height (term_width, term_height)
6368 in
6464- Nottui_picos.Ui_loop.run ~tick ~term ~renderer ~quit root
6969+ Nottui_picos.Ui_loop.run ~tick ~term ~renderer ~quit root
6570;;
66716772let start_ui () =
+17-19
jj_tui/bin/show_view.ml
···8080 ;;
81818282 let get_latest_message cursor =
8383-8484- let rec seek_latest last cursor=
8585- let peeked=Stream.peek_opt cursor in
8686- match peeked with
8787- |Some (last,new_cursor)->
8888- seek_latest last new_cursor
8989- |None->
9090- [%log debug "skipping to next status because two were queued"];
9191- (last,cursor)
9292- in
9393- let msg, new_cursor = cursor |> Stream.read in
9494-9595- (*little 50ms delay to let us move to the next one if it's ready*)
9696- Picos.Fiber.sleep ~seconds:0.05;
9797- (*if the queue isn't empty just skip the current because we really only ever want the newest*)
9898- seek_latest msg new_cursor
9999-8383+ let rec seek_latest last cursor =
8484+ let peeked = Stream.peek_opt cursor in
8585+ match peeked with
8686+ | Some (last, new_cursor) ->
8787+ seek_latest last new_cursor
8888+ | None ->
8989+ [%log debug "skipping to next status because two were queued"];
9090+ last, cursor
9191+ in
9292+ let msg, new_cursor = cursor |> Stream.read in
9393+ (*little 50ms delay to let us move to the next one if it's ready*)
9494+ Picos.Fiber.sleep ~seconds:0.05;
9595+ (*if the queue isn't empty just skip the current because we really only ever want the newest*)
9696+ seek_latest msg new_cursor
9797+ ;;
1009810199 (* Wait for messages to come in the stream.
102100 When a message comes, we try to render it.
···109107 let cursor = ref (Stream.tap stream) in
110108 while true do
111109 [%log debug "waiting for next status"];
112112- let msg,new_cursor=get_latest_message !cursor in
113113- cursor:=new_cursor;
110110+ let msg, new_cursor = get_latest_message !cursor in
111111+ cursor := new_cursor;
114112 [%log debug "cancelling older status because of new message"];
115113 Promise.terminate_after ~seconds:0. !current_summary_computation;
116114 Promise.terminate_after ~seconds:0. !current_detail_computation;
+107
jj_tui/lib/commit_render.ml
···11+(**
22+ `commit_render.ml`
33+44+ Module for rendering commit nodes to Notty images.
55+ Handles rendering commit metadata with proper styling including shortest unique prefix highlighting.
66+*)
77+88+open Notty
99+1010+(** Render an ID with prefix highlighting.
1111+ The prefix gets the full color attribute, while the rest gets a dimmed version. *)
1212+let render_id ~prefix_attr ~rest_attr ~prefix ~rest =
1313+ if String.length rest > 0
1414+ then I.(string prefix_attr prefix <|> string rest_attr rest)
1515+ else I.string prefix_attr prefix
1616+;;
1717+1818+(** Color for the graph node glyph based on node state. *)
1919+let graph_node_attr (node : Render_jj_graph.node) : Notty.A.t =
2020+ let open Notty.A in
2121+ if node.is_preview
2222+ then fg lightblack
2323+ else if node.working_copy
2424+ then fg green ++ st bold
2525+ else if node.immutable
2626+ then fg cyan
2727+ else fg white
2828+;;
2929+3030+3131+(** The amount of padding to add to the left of the commit content. *)
3232+let pad_amount = 2
3333+let add_padding img = I.pad ~l:pad_amount img
3434+(** Render commit content for a node - shows change_id, author, timestamp, description, bookmarks.
3535+Matches original jj format:
3636+Line 1: change_id email timestamp commit_id_short
3737+Line 2: (empty) description
3838+*)
3939+let render_commit_content (node : Render_jj_graph.node) : Notty.image list =
4040+ let open Notty in
4141+ let open Notty.A in
4242+4343+ let magenta= if node.working_copy then lightmagenta else magenta in
4444+(*make style bold if working copy*)
4545+let bs = if node.working_copy then st bold else st A.no_style in
4646+ let styled_text attr text = I.string attr text in
4747+ let description_line =
4848+ match String.split_on_char '\n' node.description with
4949+ | first :: _ when String.trim first <> "" ->
5050+ String.trim first
5151+ | _ ->
5252+ "(no description set)"
5353+ in
5454+ (* Line 1: change_id email timestamp bookmarks commit_id_short *)
5555+ let line1_parts = ref [] in
5656+ (* Determine base color for change_id based on node state *)
5757+5858+ (* Render change_id with prefix highlighting *)
5959+ let change_id_prefix_attr =
6060+ fg magenta ++ st bold
6161+ in
6262+ let change_id_rest_attr = fg lightblack ++bs in
6363+ let change_id_img =
6464+ render_id
6565+ ~prefix_attr:change_id_prefix_attr
6666+ ~rest_attr:change_id_rest_attr
6767+ ~prefix:node.change_id_prefix
6868+ ~rest:node.change_id_rest
6969+ in
7070+ line1_parts := change_id_img :: !line1_parts;
7171+ (* Author email and timestamp *)
7272+ line1_parts
7373+ := styled_text (fg yellow ++bs) (" " ^ node.author_email) :: !line1_parts;
7474+ line1_parts
7575+ := styled_text (fg cyan++bs ) (" " ^ node.author_timestamp) :: !line1_parts;
7676+ (* Add bookmarks after timestamp if they exist *)
7777+ if List.length node.bookmarks > 0
7878+ then (
7979+ let bookmarks_str = " " ^ String.concat " " node.bookmarks in
8080+ line1_parts := styled_text (fg magenta ) bookmarks_str :: !line1_parts);
8181+ (* Render commit_id with prefix highlighting *)
8282+ let commit_id_prefix_attr = (if node.working_copy then fg lightblue else fg blue) ++ st bold in
8383+ let commit_id_rest_attr = fg lightblack ++bs in
8484+ let commit_id_img =
8585+ render_id
8686+ ~prefix_attr:commit_id_prefix_attr
8787+ ~rest_attr:commit_id_rest_attr
8888+ ~prefix:(" " ^ node.commit_id_prefix)
8989+ ~rest:node.commit_id_rest
9090+ in
9191+ line1_parts := commit_id_img :: !line1_parts;
9292+ let line1 = !line1_parts |> List.rev |> I.hcat in
9393+ (* Line 2: (empty) description *)
9494+ let desc_attr =
9595+ ( if node.is_preview || node.empty
9696+ then lightgreen
9797+ else if node.description=""
9898+ then yellow
9999+ else white)
100100+ |>fg|> (++) bs
101101+ in
102102+ let description_with_prefix =
103103+ if node.empty then "(empty) " ^ description_line else description_line
104104+ in
105105+ let line2 = styled_text desc_attr description_with_prefix in
106106+ [add_padding line1; add_padding line2 ]
107107+;;
···33type t = {
44 key_map : Key_map.key_config [@updater]
55 ; single_pane_width_threshold : int
66- ; max_commits: int
66+ ; max_commits : int
77}
88[@@deriving yaml, record_updater ~derive:yaml]
991010-let default_config : t = { key_map = Key_map.default; single_pane_width_threshold = 100; max_commits= 100}
1010+let default_config : t =
1111+ { key_map = Key_map.default; single_pane_width_threshold = 100; max_commits = 100 }
1212+;;
11131214let get_config_dir () =
1315 let os = Os.poll_os () in
+28-4
jj_tui/lib/jj_json.ml
···2929 ; empty : bool
3030 ; bookmarks : string list
3131 ; author : jj_author
3232+ ; change_id_prefix : string
3333+ ; change_id_rest : string
3434+ ; commit_id_prefix : string
3535+ ; commit_id_rest : string
3236}
3337[@@deriving yojson]
3438···4650 ++ ',"divergent":' ++ json(divergent)
4751 ++ ',"empty":' ++ json(empty)
4852 ++ ',"bookmarks":[' ++ bookmarks.map(|b| json(b.name())).join(",") ++ ']'
4949- ++ ',"author":{"email":' ++ json(author.email()) ++ ',"timestamp":' ++ json(author.timestamp()) ++ '}'
5353+ ++ ',"author":{"email":' ++ json(author.email().local()) ++ ',"timestamp":' ++ json(author.timestamp().local().format("%Y-%m-%d %H:%M:%S")) ++ '}'
5454+ ++ ',"change_id_prefix":' ++ json(change_id.shortest(8).prefix())
5555+ ++ ',"change_id_rest":' ++ json(change_id.shortest(8).rest())
5656+ ++ ',"commit_id_prefix":' ++ json(commit_id.shortest(8).prefix())
5757+ ++ ',"commit_id_rest":' ++ json(commit_id.shortest(8).rest())
5058 ++ '}
5159'|}
5260;;
53615454-(** Parse JSONL (one JSON object per line) from jj log output *)
6262+(** Parse JSONL (one JSON object per line) from jj log output.
6363+ When graph is included, trim all content before the first '{' on each line
6464+ and skip lines without '{' (graph-only lines). *)
5565let parse_jj_log_output (input : string) : (jj_commit list, string) result =
5666 try
5767 let lines =
···5969 in
6070 let commits =
6171 lines
6262- |> List.map (fun line ->
6363- let json = Yojson.Safe.from_string line in
7272+ |> List.filter_map (fun line ->
7373+ (* Find the first '{' to skip graph characters *)
7474+ match String.index_opt line '{' with
7575+ | None ->
7676+ (* No JSON on this line, skip it (e.g., graph-only lines) *)
7777+ None
7878+ | Some idx ->
7979+ (* Extract JSON from first '{' to end of line *)
8080+ let json_str = String.sub line idx (String.length line - idx) in
8181+ Some json_str)
8282+ |> List.map (fun json_str ->
8383+ let json = Yojson.Safe.from_string json_str in
6484 match jj_commit_of_yojson json with
6585 | Ok commit ->
6686 commit
···109129 ; hidden = jj_commit.hidden
110130 ; divergent = jj_commit.divergent
111131 ; is_preview = false
132132+ ; change_id_prefix = jj_commit.change_id_prefix
133133+ ; change_id_rest = jj_commit.change_id_rest
134134+ ; commit_id_prefix = jj_commit.commit_id_prefix
135135+ ; commit_id_rest = jj_commit.commit_id_rest
112136 }
113137 in
114138 Hashtbl.add node_tbl jj_commit.commit_id n);
+20-20
jj_tui/lib/jj_json_tests.ml
···2233let%expect_test "parse_valid_jsonl" =
44 let input =
55- {|{"commit_id":"abc123","parents":[],"change_id":"xyz","description":"First commit","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-01"}}
66-{"commit_id":"def456","parents":["abc123"],"change_id":"uvw","description":"Second commit","working_copy":true,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":["main"],"author":{"email":"test@example.com","timestamp":"2024-01-02"}}|}
55+ {|{"commit_id":"abc123","parents":[],"change_id":"xyz","description":"First commit","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-01"},"change_id_prefix":"xy","change_id_rest":"z","commit_id_prefix":"abc","commit_id_rest":"123"}
66+{"commit_id":"def456","parents":["abc123"],"change_id":"uvw","description":"Second commit","working_copy":true,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":["main"],"author":{"email":"test@example.com","timestamp":"2024-01-02"},"change_id_prefix":"uv","change_id_rest":"w","commit_id_prefix":"def","commit_id_rest":"456"}|}
77 in
88 (match parse_jj_log_output input with
99 | Ok commits ->
···27272828let%expect_test "parse_root_commit" =
2929 let input =
3030- {|{"commit_id":"root","parents":[],"change_id":"xyz","description":"Root","working_copy":false,"immutable":true,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-01"}}|}
3030+ {|{"commit_id":"root","parents":[],"change_id":"xyz","description":"Root","working_copy":false,"immutable":true,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-01"},"change_id_prefix":"xy","change_id_rest":"z","commit_id_prefix":"ro","commit_id_rest":"ot"}|}
3131 in
3232 (match parse_jj_log_output input with
3333 | Ok commits ->
···43434444let%expect_test "commits_to_nodes_parent_linking" =
4545 let input =
4646- {|{"commit_id":"parent","parents":[],"change_id":"p","description":"Parent","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-01"}}
4747-{"commit_id":"child","parents":["parent"],"change_id":"c","description":"Child","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-02"}}|}
4646+ {|{"commit_id":"parent","parents":[],"change_id":"p","description":"Parent","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-01"},"change_id_prefix":"p","change_id_rest":"","commit_id_prefix":"par","commit_id_rest":"ent"}
4747+{"commit_id":"child","parents":["parent"],"change_id":"c","description":"Child","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-02"},"change_id_prefix":"c","change_id_rest":"","commit_id_prefix":"chi","commit_id_rest":"ld"}|}
4848 in
4949 (match parse_jj_log_output input with
5050 | Ok commits ->
···70707171let%expect_test "parse_multiple_parents" =
7272 let input =
7373- {|{"commit_id":"parent1","parents":[],"change_id":"p1","description":"Parent 1","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-01"}}
7474-{"commit_id":"parent2","parents":[],"change_id":"p2","description":"Parent 2","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-02"}}
7575-{"commit_id":"merge","parents":["parent1","parent2"],"change_id":"m","description":"Merge commit","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-03"}}|}
7373+ {|{"commit_id":"parent1","parents":[],"change_id":"p1","description":"Parent 1","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-01"},"change_id_prefix":"p","change_id_rest":"1","commit_id_prefix":"par","commit_id_rest":"ent1"}
7474+{"commit_id":"parent2","parents":[],"change_id":"p2","description":"Parent 2","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-02"},"change_id_prefix":"p","change_id_rest":"2","commit_id_prefix":"par","commit_id_rest":"ent2"}
7575+{"commit_id":"merge","parents":["parent1","parent2"],"change_id":"m","description":"Merge commit","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-03"},"change_id_prefix":"m","change_id_rest":"","commit_id_prefix":"mer","commit_id_rest":"ge"}|}
7676 in
7777 (match parse_jj_log_output input with
7878 | Ok commits ->
···100100101101let%expect_test "parse_commit_with_bookmarks" =
102102 let input =
103103- {|{"commit_id":"abc123","parents":[],"change_id":"xyz","description":"Commit with bookmarks","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":["main","feature"],"author":{"email":"test@example.com","timestamp":"2024-01-01"}}|}
103103+ {|{"commit_id":"abc123","parents":[],"change_id":"xyz","description":"Commit with bookmarks","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":["main","feature"],"author":{"email":"test@example.com","timestamp":"2024-01-01"},"change_id_prefix":"xy","change_id_rest":"z","commit_id_prefix":"abc","commit_id_rest":"123"}|}
104104 in
105105 (match parse_jj_log_output input with
106106 | Ok commits ->
···119119120120let%expect_test "parse_wip_commit" =
121121 let input =
122122- {|{"commit_id":"wip123","parents":[],"change_id":"xyz","description":"wip: work in progress","working_copy":true,"immutable":false,"wip":true,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-01"}}|}
122122+ {|{"commit_id":"wip123","parents":[],"change_id":"xyz","description":"wip: work in progress","working_copy":true,"immutable":false,"wip":true,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-01"},"change_id_prefix":"xy","change_id_rest":"z","commit_id_prefix":"wip","commit_id_rest":"123"}|}
123123 in
124124 (match parse_jj_log_output input with
125125 | Ok commits ->
···165165166166let%expect_test "commits_to_nodes_preserves_order" =
167167 let input =
168168- {|{"commit_id":"first","parents":[],"change_id":"f","description":"First","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-01"}}
169169-{"commit_id":"second","parents":["first"],"change_id":"s","description":"Second","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-02"}}
170170-{"commit_id":"third","parents":["second"],"change_id":"t","description":"Third","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-03"}}|}
168168+ {|{"commit_id":"first","parents":[],"change_id":"f","description":"First","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-01"},"change_id_prefix":"f","change_id_rest":"","commit_id_prefix":"fir","commit_id_rest":"st"}
169169+{"commit_id":"second","parents":["first"],"change_id":"s","description":"Second","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-02"},"change_id_prefix":"s","change_id_rest":"","commit_id_prefix":"sec","commit_id_rest":"ond"}
170170+{"commit_id":"third","parents":["second"],"change_id":"t","description":"Third","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-03"},"change_id_prefix":"t","change_id_rest":"","commit_id_prefix":"thi","commit_id_rest":"rd"}|}
171171 in
172172 (match parse_jj_log_output input with
173173 | Ok commits ->
···186186187187let%expect_test "commits_to_nodes_copies_fields" =
188188 let input =
189189- {|{"commit_id":"test","parents":[],"change_id":"xyz","description":"Test commit","working_copy":true,"immutable":true,"wip":true,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-01"}}|}
189189+ {|{"commit_id":"test","parents":[],"change_id":"xyz","description":"Test commit","working_copy":true,"immutable":true,"wip":true,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-01"},"change_id_prefix":"xy","change_id_rest":"z","commit_id_prefix":"te","commit_id_rest":"st"}|}
190190 in
191191 (match parse_jj_log_output input with
192192 | Ok commits ->
···210210211211let%expect_test "commits_to_nodes_missing_parent_creates_elided" =
212212 let input =
213213- {|{"commit_id":"child","parents":["missing_parent"],"change_id":"c","description":"Child with missing parent","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-02"}}|}
213213+ {|{"commit_id":"child","parents":["missing_parent"],"change_id":"c","description":"Child with missing parent","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-02"},"change_id_prefix":"c","change_id_rest":"","commit_id_prefix":"chi","commit_id_rest":"ld"}|}
214214 in
215215 (match parse_jj_log_output input with
216216 | Ok commits ->
···237237238238let%expect_test "commits_to_nodes_multiple_children_same_missing_parent" =
239239 let input =
240240- {|{"commit_id":"child1","parents":["missing_parent"],"change_id":"c1","description":"Child 1","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-02"}}
241241-{"commit_id":"child2","parents":["missing_parent"],"change_id":"c2","description":"Child 2","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-03"}}|}
240240+ {|{"commit_id":"child1","parents":["missing_parent"],"change_id":"c1","description":"Child 1","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-02"},"change_id_prefix":"c","change_id_rest":"1","commit_id_prefix":"chi","commit_id_rest":"ld1"}
241241+{"commit_id":"child2","parents":["missing_parent"],"change_id":"c2","description":"Child 2","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-03"},"change_id_prefix":"c","change_id_rest":"2","commit_id_prefix":"chi","commit_id_rest":"ld2"}|}
242242 in
243243 (match parse_jj_log_output input with
244244 | Ok commits ->
···262262 |}]
263263;;
264264265265-let%expect_test "commits_to_nodes_multiple_children_same_missing_parent" =
265265+let%expect_test "commits_to_nodes_same_missing_parent_physical_equality" =
266266 let input =
267267- {|{"commit_id":"child1","parents":["missing_parent"],"change_id":"c1","description":"Child 1","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-02"}}
268268-{"commit_id":"child2","parents":["missing_parent"],"change_id":"c2","description":"Child 2","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-03"}}|}
267267+ {|{"commit_id":"child1","parents":["missing_parent"],"change_id":"c1","description":"Child 1","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-02"},"change_id_prefix":"c","change_id_rest":"1","commit_id_prefix":"chi","commit_id_rest":"ld1"}
268268+{"commit_id":"child2","parents":["missing_parent"],"change_id":"c2","description":"Child 2","working_copy":false,"immutable":false,"wip":false,"hidden":false,"divergent":false,"empty":false,"bookmarks":[],"author":{"email":"test@example.com","timestamp":"2024-01-03"},"change_id_prefix":"c","change_id_rest":"2","commit_id_prefix":"chi","commit_id_rest":"ld2"}|}
269269 in
270270 (match parse_jj_log_output input with
271271 | Ok commits ->
+53-46
jj_tui/lib/key.ml
···11-type modifier = [ `Meta | `Shift | `Ctrl ]
11+type modifier =
22+ [ `Meta
33+ | `Shift
44+ | `Ctrl
55+ ]
2637type t = {
44- key: char;
55- modifiers: modifier list;
88+ key : char
99+ ; modifiers : modifier list
610}
711812let sort_and_dedup_modifiers mods =
99- let modifier_order = function
1010- | `Shift -> 0
1111- | `Meta -> 1
1212- | `Ctrl -> 2
1313- in
1414- mods
1515- |> List.sort_uniq (fun a b -> compare (modifier_order a) (modifier_order b))
1313+ let modifier_order = function `Shift -> 0 | `Meta -> 1 | `Ctrl -> 2 in
1414+ mods |> List.sort_uniq (fun a b -> compare (modifier_order a) (modifier_order b))
1515+;;
16161717let key_of_string str =
1818 let parts = String.split_on_char '+' str in
1919 let rec process_parts mods = function
2020- | [] -> Error "No key character provided"
2121- | [k] when String.length k = 1 ->
2222- let key = k.[0] in
2323- Ok { key = key; modifiers = sort_and_dedup_modifiers ( mods) }
2020+ | [] ->
2121+ Error "No key character provided"
2222+ | [ k ] when String.length k = 1 ->
2323+ let key = k.[0] in
2424+ Ok { key; modifiers = sort_and_dedup_modifiers mods }
2425 | mod_str :: rest ->
2525- let modifier = match String.uppercase_ascii mod_str with
2626- | "C" | "CTRL" -> Ok `Ctrl
2727- | "S" | "SHIFT" -> Ok `Shift
2828- | "A" | "ALT" -> Ok `Meta
2929- | other -> Error (Printf.sprintf "Unknown modifier: %s" other)
2626+ let modifier =
2727+ match String.uppercase_ascii mod_str with
2828+ | "C" | "CTRL" ->
2929+ Ok `Ctrl
3030+ | "S" | "SHIFT" ->
3131+ Ok `Shift
3232+ | "A" | "ALT" ->
3333+ Ok `Meta
3434+ | other ->
3535+ Error (Printf.sprintf "Unknown modifier: %s" other)
3036 in
3131- match modifier with
3232- | Ok m -> process_parts (m :: mods) rest
3333- | Error e -> Error e
3737+ (match modifier with Ok m -> process_parts (m :: mods) rest | Error e -> Error e)
3438 in
3539 process_parts [] parts
4040+;;
36413737-let key_of_string_exn str= match key_of_string str with Ok k -> k | Error msg -> failwith ("Invalid key: " ^ msg)
4242+let key_of_string_exn str =
4343+ match key_of_string str with Ok k -> k | Error msg -> failwith ("Invalid key: " ^ msg)
4444+;;
38453946let key_to_string { key; modifiers } =
4047 let modifier_str =
4148 modifiers
4242- |> List.map (function
4343- | `Shift -> "S"
4444- | `Meta -> "A"
4545- | `Ctrl -> "C")
4949+ |> List.map (function `Shift -> "S" | `Meta -> "A" | `Ctrl -> "C")
4650 |> String.concat "+"
4751 in
4848- if modifier_str = "" then
4949- String.make 1 key
5050- else
5151- modifier_str ^ "+" ^ (String.make 1 key)
5252+ if modifier_str = "" then String.make 1 key else modifier_str ^ "+" ^ String.make 1 key
5353+;;
52545355let key_of_yaml = function
5456 | `String s ->
5557 (match key_of_string s with
5656- | Ok k -> Ok k
5757- | Error msg -> Error (`Msg("Invalid key format: " ^ msg)))
5858- | _ -> Error (`Msg "Expected string for key")
5858+ | Ok k ->
5959+ Ok k
6060+ | Error msg ->
6161+ Error (`Msg ("Invalid key format: " ^ msg)))
6262+ | _ ->
6363+ Error (`Msg "Expected string for key")
6464+;;
59656060-let key_to_yaml k =
6161- `String (key_to_string k)
6262-6666+let key_to_yaml k = `String (key_to_string k)
6367let pp fmt k = Format.fprintf fmt "%s" (key_to_string k)
6464-6565-let equal k1 k2 = k1.key = k2.key && k1.modifiers = k2.modifiers
6868+let equal k1 k2 = k1.key = k2.key && k1.modifiers = k2.modifiers
66696767-let hash k = Char.hash k.key + List.fold_left (fun acc m -> acc * 31 + match m with
6868- | `Meta -> 1
6969- | `Shift -> 2
7070- | `Ctrl -> 3
7171-) 0 k.modifiers
7070+let hash k =
7171+ Char.hash k.key
7272+ + List.fold_left
7373+ (fun acc m -> (acc * 31) + match m with `Meta -> 1 | `Shift -> 2 | `Ctrl -> 3)
7474+ 0
7575+ k.modifiers
7676+;;
72777378let compare k1 k2 =
7479 match compare k1.key k2.key with
7575- | 0 -> List.compare compare k1.modifiers k2.modifiers
7676- | c -> c
8080+ | 0 ->
8181+ List.compare compare k1.modifiers k2.modifiers
8282+ | c ->
8383+ c
7784;;
+1-5
jj_tui/lib/logging.ml
···7070 in
7171 let with_stamp h tags k ppf fmt =
7272 let stamp =
7373- match tags with
7474- | None ->
7575- None
7676- | Some tags ->
7777- Logs.Tag.find timestamp_tag tags
7373+ match tags with None -> None | Some tags -> Logs.Tag.find timestamp_tag tags
7874 in
7975 let dt = Format.pp_print_option (Logs.Tag.printer timestamp_tag) in
8076 Format.kfprintf
+6-6
jj_tui/lib/os.ml
···11module Internal = struct
22-let normalise_os raw =
33- match String.lowercase_ascii raw with "darwin" | "osx" -> "macos" | s -> s
44-;;
22+ let normalise_os raw =
33+ match String.lowercase_ascii raw with "darwin" | "osx" -> "macos" | s -> s
44+ ;;
55end
6677let poll_os () =
···99 match Sys.os_type with
1010 | "Unix" ->
1111 (try
1212- let uname_in = Unix.open_process_args_in "uname" [| "uname"; "-s" |] in
1212+ let uname_in = Unix.open_process_args_in "uname" [| "uname"; "-s" |] in
1313 let str = uname_in |> In_channel.input_all in
1414- Unix.wait()|>ignore;
1414+ Unix.wait () |> ignore;
1515 Some (str |> String.lowercase_ascii |> String.trim)
1616 with
1717 | _ ->
···2020 Some (s |> String.lowercase_ascii |> String.trim)
2121 in
2222 match raw with None | Some "" -> None | Some s -> Some (Internal.normalise_os s)
2323-;;2323+;;
···11type rev_id = {
22 change_id : string
33 ; commit_id : string
44-55- ; divergent : bool(** Indicates the changeid is conflicted and we must use the commitid *)
44+ ; divergent : bool
55+ (** Indicates the changeid is conflicted and we must use the commitid *)
66}
7788type 'a maybe_unique =
+4-9
jj_tui/lib/process_wrappers.ml
···174174 graph, revs |> Array.of_list
175175 ;;
176176177177- (** Fetch graph data as JSON and parse into commits *)
177177+ (** Fetch graph data as JSON and parse into commits.
178178+ Uses graph output (not --no-graph) because the graph ensures nodes are
179179+ in the correct topological order for rendering. *)
178180 let get_graph_json ?revset limit =
179181 let args =
180180- [
181181- "log"
182182- ; "--no-graph"
183183- ; "-T"
184184- ; Jj_json.json_log_template
185185- ; "--limit"
186186- ; string_of_int limit
187187- ]
182182+ [ "log"; "-T"; Jj_json.json_log_template; "--limit"; string_of_int limit ]
188183 in
189184 let args = match revset with Some r -> args @ [ "-r"; r ] | None -> args in
190185 let output = jj_no_log args ~color:false in
+116-20
jj_tui/lib/render_jj_graph.ml
···5353 ; hidden : bool
5454 ; divergent : bool
5555 ; is_preview : bool
5656+ ; change_id_prefix : string
5757+ ; change_id_rest : string
5858+ ; commit_id_prefix : string
5959+ ; commit_id_rest : string
5660}
57615862(** Special marker for elided nodes *)
···7680 ; hidden = true
7781 ; divergent = false
7882 ; is_preview = false
8383+ ; change_id_prefix = ""
8484+ ; change_id_rest = ""
8585+ ; commit_id_prefix = ""
8686+ ; commit_id_rest = ""
7987 }
8088;;
8189···111119 ; hidden = false
112120 ; divergent = false
113121 ; is_preview = true
122122+ ; change_id_prefix = ""
123123+ ; change_id_rest = ""
124124+ ; commit_id_prefix = ""
125125+ ; commit_id_rest = ""
114126 }
115127;;
116128···158170(** Structured output for UI integration *)
159171type graph_row_output = {
160172 graph_chars : string (** The graph prefix like "○ " or "├─╮" *)
173173+ ; graph_image : Notty.image (** Notty image for graph prefix, with styling *)
161174 ; node : node (** The node this row represents *)
162175 ; row_type : row_type (** What kind of row this is *)
163176}
···884897 else PadRow
885898;;
886899900900+(** Trim trailing whitespace from a graph image to match its string form. *)
901901+let trim_graph_image ~graph_chars (img : Notty.image) : Notty.image =
902902+ let open Notty in
903903+ let trimmed_width = I.width (I.string A.empty graph_chars) in
904904+ let width = I.width img in
905905+ if width > trimmed_width then I.hcrop 0 (width - trimmed_width) img else img
906906+;;
907907+887908(** Render nodes to structured output for UI integration *)
888909let render_nodes_structured
889910 ?(info_lines = fun _ -> 0)
911911+ ?(node_attr = fun _ -> Notty.A.empty)
890912 (_state : state)
891913 (nodes : node list) : graph_row_output list
892914 =
···897919 (fun n ->
898920 let row = next_row ~columns n in
899921 (match !extra_pad_line_ref with
900900- | Some s ->
922922+ | Some (s, img) ->
901923 let trimmed = String.trim s in
924924+ let trimmed_img = trim_graph_image ~graph_chars:trimmed img in
902925 result
903903- := { graph_chars = trimmed; node = n; row_type = classify_row_type trimmed }
926926+ := {
927927+ graph_chars = trimmed
928928+ ; graph_image = trimmed_img
929929+ ; node = n
930930+ ; row_type = classify_row_type trimmed
931931+ }
904932 :: !result;
905933 extra_pad_line_ref := None
906934 | None ->
907935 ());
908936 let node_buf = Buffer.create 64 in
937937+ let node_images = ref [] in
909938 Array.iter
910939 (fun entry ->
911940 match entry with
912941 | NL_Node ->
913942 Buffer.add_utf_8_uchar node_buf row.glyph;
914914- Buffer.add_char node_buf ' '
943943+ Buffer.add_char node_buf ' ';
944944+ let glyph_img = Notty.I.uchar (node_attr row.row_node) row.glyph 1 1 in
945945+ let space_img = Notty.I.string Notty.A.empty " " in
946946+ node_images := Notty.I.hcat [ glyph_img; space_img ] :: !node_images
915947 | NL_Parent ->
916916- Buffer.add_string node_buf glyphs.(Glyph.parent)
948948+ Buffer.add_string node_buf glyphs.(Glyph.parent);
949949+ node_images
950950+ := Notty.I.string Notty.A.empty glyphs.(Glyph.parent) :: !node_images
917951 | NL_Ancestor ->
918918- Buffer.add_string node_buf glyphs.(Glyph.ancestor)
952952+ Buffer.add_string node_buf glyphs.(Glyph.ancestor);
953953+ node_images
954954+ := Notty.I.string Notty.A.empty glyphs.(Glyph.ancestor) :: !node_images
919955 | NL_Blank ->
920920- Buffer.add_string node_buf glyphs.(Glyph.space))
956956+ Buffer.add_string node_buf glyphs.(Glyph.space);
957957+ node_images
958958+ := Notty.I.string Notty.A.empty glyphs.(Glyph.space) :: !node_images)
921959 row.node_line;
922960 let node_str = Buffer.contents node_buf |> String.trim in
961961+ let node_img = !node_images |> List.rev |> Notty.I.hcat in
962962+ let node_img = trim_graph_image ~graph_chars:node_str node_img in
923963 result
924924- := { graph_chars = node_str; node = n; row_type = classify_row_type node_str }
964964+ := {
965965+ graph_chars = node_str
966966+ ; graph_image = node_img
967967+ ; node = n
968968+ ; row_type = classify_row_type node_str
969969+ }
925970 :: !result;
926971 (match row.link_line with
927972 | Some link_row ->
928973 let link_buf = Buffer.create 64 in
974974+ let link_images = ref [] in
929975 Array.iter
930976 (fun cur ->
931977 let glyph_idx = select_link_glyph cur ~merge:row.merge in
932932- Buffer.add_string link_buf glyphs.(glyph_idx))
978978+ Buffer.add_string link_buf glyphs.(glyph_idx);
979979+ link_images
980980+ := Notty.I.string Notty.A.empty glyphs.(glyph_idx) :: !link_images)
933981 link_row;
934982 let link_str = Buffer.contents link_buf |> String.trim in
983983+ let link_img = !link_images |> List.rev |> Notty.I.hcat in
984984+ let link_img = trim_graph_image ~graph_chars:link_str link_img in
935985 result
936936- := { graph_chars = link_str; node = n; row_type = classify_row_type link_str }
986986+ := {
987987+ graph_chars = link_str
988988+ ; graph_image = link_img
989989+ ; node = n
990990+ ; row_type = classify_row_type link_str
991991+ }
937992 :: !result
938993 | None ->
939994 ());
···941996 (match row.term_line with
942997 | Some term_row ->
943998 let term_buf1 = Buffer.create 64 in
999999+ let term_images1 = ref [] in
9441000 Array.iteri
9451001 (fun i term ->
9461002 if term
947947- then Buffer.add_string term_buf1 glyphs.(Glyph.parent)
10031003+ then (
10041004+ Buffer.add_string term_buf1 glyphs.(Glyph.parent);
10051005+ term_images1
10061006+ := Notty.I.string Notty.A.empty glyphs.(Glyph.parent) :: !term_images1)
9481007 else (
9491008 let pad_glyph = pad_line_to_glyph row.pad_lines.(i) in
950950- Buffer.add_string term_buf1 glyphs.(pad_glyph)))
10091009+ Buffer.add_string term_buf1 glyphs.(pad_glyph);
10101010+ term_images1
10111011+ := Notty.I.string Notty.A.empty glyphs.(pad_glyph) :: !term_images1))
9511012 term_row;
9521013 let term_str1 = Buffer.contents term_buf1 |> String.trim in
10141014+ let term_img1 = !term_images1 |> List.rev |> Notty.I.hcat in
10151015+ let term_img1 = trim_graph_image ~graph_chars:term_str1 term_img1 in
9531016 result
954954- := { graph_chars = term_str1; node = n; row_type = classify_row_type term_str1 }
10171017+ := {
10181018+ graph_chars = term_str1
10191019+ ; graph_image = term_img1
10201020+ ; node = n
10211021+ ; row_type = classify_row_type term_str1
10221022+ }
9551023 :: !result;
9561024 let term_buf2 = Buffer.create 64 in
10251025+ let term_images2 = ref [] in
9571026 Array.iteri
9581027 (fun i term ->
9591028 if term
960960- then Buffer.add_string term_buf2 glyphs.(Glyph.termination)
10291029+ then (
10301030+ Buffer.add_string term_buf2 glyphs.(Glyph.termination);
10311031+ term_images2
10321032+ := Notty.I.string Notty.A.empty glyphs.(Glyph.termination) :: !term_images2)
9611033 else (
9621034 let pad_glyph = pad_line_to_glyph row.pad_lines.(i) in
963963- Buffer.add_string term_buf2 glyphs.(pad_glyph)))
10351035+ Buffer.add_string term_buf2 glyphs.(pad_glyph);
10361036+ term_images2
10371037+ := Notty.I.string Notty.A.empty glyphs.(pad_glyph) :: !term_images2))
9641038 term_row;
9651039 let term_str2 = Buffer.contents term_buf2 |> String.trim in
10401040+ let term_img2 = !term_images2 |> List.rev |> Notty.I.hcat in
10411041+ let term_img2 = trim_graph_image ~graph_chars:term_str2 term_img2 in
9661042 result
967967- := { graph_chars = term_str2; node = n; row_type = classify_row_type term_str2 }
10431043+ := {
10441044+ graph_chars = term_str2
10451045+ ; graph_image = term_img2
10461046+ ; node = n
10471047+ ; row_type = classify_row_type term_str2
10481048+ }
9681049 :: !result;
9691050 need_extra_pad := true
9701051 | None ->
9711052 ());
9721053 let pad_buf = Buffer.create 64 in
10541054+ let pad_images = ref [] in
9731055 Array.iter
9741056 (fun entry ->
9751057 let glyph_idx = pad_line_to_glyph entry in
976976- Buffer.add_string pad_buf glyphs.(glyph_idx))
10581058+ Buffer.add_string pad_buf glyphs.(glyph_idx);
10591059+ pad_images := Notty.I.string Notty.A.empty glyphs.(glyph_idx) :: !pad_images)
9771060 row.pad_lines;
9781061 let base_pad_line = Buffer.contents pad_buf in
979979- if !need_extra_pad then extra_pad_line_ref := Some base_pad_line;
10621062+ let base_pad_img = !pad_images |> List.rev |> Notty.I.hcat in
10631063+ if !need_extra_pad then extra_pad_line_ref := Some (base_pad_line, base_pad_img);
9801064 let extra_rows = info_lines n in
9811065 for _ = 1 to extra_rows do
9821066 let info_pad_buf = Buffer.create 64 in
10671067+ let info_pad_images = ref [] in
9831068 Array.iter
9841069 (fun col ->
9851070 let glyph_idx = pad_line_to_glyph (column_to_pad_line col) in
986986- Buffer.add_string info_pad_buf glyphs.(glyph_idx))
10711071+ Buffer.add_string info_pad_buf glyphs.(glyph_idx);
10721072+ info_pad_images
10731073+ := Notty.I.string Notty.A.empty glyphs.(glyph_idx) :: !info_pad_images)
9871074 !columns;
9881075 let info_pad_str = Buffer.contents info_pad_buf |> String.trim in
10761076+ let info_pad_img = !info_pad_images |> List.rev |> Notty.I.hcat in
10771077+ let info_pad_img = trim_graph_image ~graph_chars:info_pad_str info_pad_img in
9891078 result
9901079 := {
9911080 graph_chars = info_pad_str
10811081+ ; graph_image = info_pad_img
9921082 ; node = n
9931083 ; row_type = classify_row_type info_pad_str
9941084 }
···9961086 done)
9971087 nodes;
9981088 (match !extra_pad_line_ref with
999999- | Some s ->
10891089+ | Some (s, img) ->
10001090 let trimmed = String.trim s in
10911091+ let trimmed_img = trim_graph_image ~graph_chars:trimmed img in
10011092 let last_node = List.hd (List.rev nodes) in
10021093 result
10031003- := { graph_chars = trimmed; node = last_node; row_type = classify_row_type trimmed }
10941094+ := {
10951095+ graph_chars = trimmed
10961096+ ; graph_image = trimmed_img
10971097+ ; node = last_node
10981098+ ; row_type = classify_row_type trimmed
10991099+ }
10041100 :: !result
10051101 | None ->
10061102 ());
···114114 *)
115115 Lwd_table.map_reduce
116116 (fun _ x ->
117117- match control_character_index x 0 with
118118- | exception Not_found ->
119119- x, None
120120- | i ->
121121- let prefix = String.sub x 0 i in
122122- (match split_lines x [] (i + 1) with
123123- | [] ->
124124- assert false
125125- | suffix :: rest ->
126126- let ui = rest |> List.rev_map wrap_line |> Lwd_utils.reduce Ui.pack_y in
127127- prefix, Some (ui, suffix)))
117117+ match control_character_index x 0 with
118118+ | exception Not_found ->
119119+ x, None
120120+ | i ->
121121+ let prefix = String.sub x 0 i in
122122+ (match split_lines x [] (i + 1) with
123123+ | [] ->
124124+ assert false
125125+ | suffix :: rest ->
126126+ let ui = rest |> List.rev_map wrap_line |> Lwd_utils.reduce Ui.pack_y in
127127+ prefix, Some (ui, suffix)))
128128 ( ("", None)
129129 , fun (pa, ta) (pb, tb) ->
130130 match ta with
···140140 | Some (ub, sb) ->
141141 join3 ua (wrap_line line) ub, sb) ) )
142142 table
143143- |> (* After reducing the table, we produce the final UI, interpreting
143143+ |>
144144+ (* After reducing the table, we produce the final UI, interpreting
144145 unterminated prefix and suffix has line of their own. *)
145146 Lwd.map ~f:(function
146147 | pa, None ->