···35353636(**Updates the status windows; Without snapshotting the working copy by default
3737 This should be called after any command that performs a change *)
3838-let update_status ?(cause_snapshot = false) () =
3838+let update_status ?(update_graph = true) ?(cause_snapshot = false) () =
3939+ let rev = Lwd.peek Vars.ui_state.selected_revision in
4040+ let log_res =
4141+ jj_no_log ~snapshot:cause_snapshot [ "log" ]
4242+ |> colored_string
4343+ in
4444+ if update_graph then Vars.ui_state.trigger_update $= ();
4545+;;
4646+(**Updates the status windows; Without snapshotting the working copy by default
4747+ This should be called after any command that performs a change *)
4848+let update_views ?(cause_snapshot = false) () =
4949+ let rev = Lwd.peek Vars.ui_state.selected_revision in
3950 Eio.Switch.run @@ fun sw ->
4051 let log_res =
4141- jj_no_log ~snapshot:cause_snapshot [ "show"; "-s"; "--color-words" ] |> colored_string
5252+ jj_no_log ~snapshot:cause_snapshot [ "show"; "-s"; "--color-words"; "-r"; rev ]
5353+ |> colored_string
4254 in
4355 (* From now on we use ignore-working-copy so we don't re-snapshot the state and so
4456 we can operate in paralell *)
4557 let tree =
4658 Eio.Fiber.fork_promise ~sw (fun _ ->
4747- jj_no_log ~snapshot:false [ "log" ] |> colored_string)
5959+ jj_no_log ~snapshot:false [ "log"; "-r"; rev ] |> colored_string)
4860 (* TODO: stop using dop last twice *)
4961 and branches =
5062 Eio.Fiber.fork_promise ~sw (fun _ ->
5163 jj_no_log ~snapshot:false [ "branch"; "list"; "-a" ] |> colored_string)
5252- and files_list = Eio.Fiber.fork_promise ~sw (fun _ -> list_files ()) in
6464+ and files_list = Eio.Fiber.fork_promise ~sw (fun _ -> list_files ~rev ()) in
5365 (*wait for all our tasks*)
5466 let tree = Eio.Promise.await_exn tree
5567 and files_list = Eio.Promise.await_exn files_list
···1212 open Vars
1313 open Jj_process.Make (Vars)
14141515- exception Found
1515+ exception FoundStart
1616+ exception FoundFiller
16171717- let elieded_symbol =
1818- let a = String.get_utf_8_uchar "◌" 0 in
1818+ let make_uchar str =
1919+ let a = String.get_utf_8_uchar str 0 in
1920 if a |> Uchar.utf_decode_is_valid
2021 then a |> Uchar.utf_decode_uchar
2122 else failwith "not a unicode string"
2223 ;;
23242525+ let elieded_symbol = make_uchar "◌"
2626+ let rev_symbol = make_uchar "◉"
2727+2828+ let is_whitespace_char (code_point : int) : bool =
2929+ match code_point with
3030+ | 0x0009 (* Tab *)
3131+ | 0x000A (* Line Feed *)
3232+ | 0x000B (* Vertical Tab *)
3333+ | 0x000C (* Form Feed *)
3434+ | 0x000D (* Carriage Return *)
3535+ | 0x0020 (* Space *)
3636+ | 0x0085 (* Next Line *)
3737+ | 0x00A0 (* No-Break Space *)
3838+ | 0x1680 (* Ogham Space Mark *)
3939+ | 0x2000 (* En Quad *)
4040+ | 0x2001 (* Em Quad *)
4141+ | 0x2002 (* En Space *)
4242+ | 0x2003 (* Em Space *)
4343+ | 0x2004 (* Three-Per-Em Space *)
4444+ | 0x2005 (* Four-Per-Em Space *)
4545+ | 0x2006 (* Six-Per-Em Space *)
4646+ | 0x2007 (* Figure Space *)
4747+ | 0x2008 (* Punctuation Space *)
4848+ | 0x2009 (* Thin Space *)
4949+ | 0x200A (* Hair Space *)
5050+ | 0x2028 (* Line Separator *)
5151+ | 0x2029 (* Paragraph Separator *)
5252+ | 0x202F (* Narrow No-Break Space *)
5353+ | 0x205F (* Medium Mathematical Space *)
5454+ | 0x3000 (* Ideographic Space *) ->
5555+ true
5656+ | _ ->
5757+ false
5858+ ;;
5959+6060+ let is_graph_start_char char =
6161+ let i = Uchar.to_int char in
6262+ (*chars like these: ├─╮*)
6363+ let is_pipe = i > 0x2500 && i < 0x259f in
6464+ let is_whitespace = is_whitespace_char i in
6565+ is_pipe || is_whitespace
6666+ ;;
6767+2468 let test_data =
2569 {|◉ yzquvpvl eli.jambu@gmail.com 2024-05-23 15:04:24 3565237c
2670├─╮ merger
···78122 [] (* If list is empty or has only one element, return empty list *)
79123 ;;
80124125125+ (*
126126+ 1. Make the graph
127127+ *)
128128+ let is_line_filler line =
129129+ line
130130+ (* We will iterate through skipping any chars like pipes and whitespace untill we find either:
131131+ a) A rev start char,which would make the line a rev.
132132+ b) Nothing, which would make the the line filler
133133+ *)
134134+ |> String.iteri (fun i char ->
135135+ let uchar = String.get_utf_8_uchar line i |> Uchar.utf_decode_uchar in
136136+ (*I've removed the part that tries to precisely skip all the start chars. this is becasue it gets all stuffed up by the terminal escape codes
137137+ FIXME currently this will get stuffed up if a line has that rev symbol in it
138138+ *)
139139+140140+ (* if not (uchar |> is_graph_start_char) *)
141141+ (* then *)
142142+ if uchar |> Uchar.equal rev_symbol || char == '@'
143143+ then raise FoundStart
144144+ (* else raise FoundFiller *)
145145+ (* else () *)
146146+ )
147147+ ;;
148148+81149 let seperate_revs () =
82150 let graph =
83151 jj_no_log [ "log" ]
84152 |> String.split_on_char '\n'
85153 (* filter out any lines that contain *)
8686- |> List.filter (fun x ->
154154+ |> Base.List.fold ~init:([], None) ~f:(fun (new_list, last) x ->
87155 if x |> String.length <= 1
8888- then false
156156+ then `Filler x :: new_list, None
89157 else (
9090- try
9191- x
9292- |> String.iteri (fun i char ->
9393- let char = String.get_utf_8_uchar x i |> Uchar.utf_decode_uchar in
9494- if char |> Uchar.equal elieded_symbol then raise Found else ());
9595- true
9696- with
9797- | _ ->
9898- false))
9999- |> pairwise (fun (top, descr) -> top ^ "\n" ^ descr) ~f_last:(fun last -> [ last ])
158158+ match last with
159159+ | Some last_line ->
160160+ `Selectable (String.concat "\n" [ last_line; x ]) :: new_list, None
161161+ | None ->
162162+ (try
163163+ is_line_filler x;
164164+ `Filler x :: new_list, None
165165+ with
166166+ | FoundStart ->
167167+ new_list, Some x
168168+ | FoundFiller ->
169169+ `Filler x :: new_list, None)))
170170+ |> fst
171171+ |> List.rev
172172+ |> Array.of_list
100173 in
101174 let revs =
102175 jj_no_log ~color:false [ "log"; "--no-graph"; "-T"; {|change_id++"\n"|} ]
103176 |> String.split_on_char '\n'
104177 |> List.filter (fun x -> x |> String.trim <> "")
178178+ |> Array.of_list
105179 in
106106- if List.length graph <> List.length revs
107107- then
108108- failwith
109109- @@ "When getting list of revs the graph had a different number of items to the \
110110- revs. This shouldn't be possible and is a bug. "
111111- ^ Printf.sprintf "revs:%d graph:%d" (List.length revs) (List.length graph)
112112- ^ "\n"
113113- ^ String.concat "\n" revs
114114- ^ String.concat "\n" graph;
115180 graph, revs
116181 ;;
117182···156221 else string
157222 ;;
158223159159- (*TODO:make a custom widget the renders the commit with and without selection.
160160- with selection replace the dot with a blue version and slightly blue tint the background *)
161161- let revs_list () =
162162- seperate_revs ()
163163- |> fst
164164- |> List.map (fun x is_focused ->
165165- (*hightlight blue when selection is true*)
166166- let prefix =
167167- if is_focused then I.char A.(bg A.blue) '>' 1 2 else I.char A.empty ' ' 1 2
168168- in
169169- I.hcat
170170- [
171171- prefix
172172- ; x ^ "\n" |> unsafe_blit_start_if "@" "◉" |> Jj_tui.AnsiReverse.colored_string
173173- ]
174174- |> Ui.atom)
175175- |> Lwd.pure
176176- |> selection_list
177177- ;;
178224179179- (*TODO:make a custom widget the renders the commit with and without selection.
180180- with selection replace the dot with a blue version and slightly blue tint the background *)
181181- let revs_list_filtered () =
182182- let graph_items, revs = seperate_revs () in
183183- graph_items
184184- |> List.map (fun x is_focused ->
185185- (* hightlight blue when selection is true *)
186186- let prefix =
187187- if is_focused then I.char A.(bg A.blue) '>' 1 2 else I.char A.empty ' ' 1 2
188188- in
189189- I.hcat
190190- [
191191- prefix
192192- ; x ^ "\n" |> unsafe_blit_start_if "@" "◉" |> Jj_tui.AnsiReverse.colored_string
193193- ]
194194- |> Ui.atom)
195195- |> List.map2 (fun rev ui -> Wd.{ ui; data = rev }) revs
196196- |> Lwd.pure
197197- |> Wd.filterable_selection_list
198198- ~on_confirm:(fun _ -> ())
199199- ~filter_predicate:(fun input rev -> rev |> String.starts_with ~prefix:input)
200200- ;;
225225+ (* (*TODO:make a custom widget the renders the commit with and without selection. *)
226226+ (* with selection replace the dot with a blue version and slightly blue tint the background *) *)
227227+ (* let revs_list_filtered () = *)
228228+ (* let graph_items, revs = seperate_revs () in *)
229229+ (* graph_items *)
230230+ (* |> List.map (fun x is_focused -> *)
231231+ (* hightlight blue when selection is true *)
232232+ (* let prefix = *)
233233+ (* if is_focused then I.char A.(bg A.blue) '>' 1 2 else I.char A.empty ' ' 1 2 *)
234234+ (* in *)
235235+ (* I.hcat *)
236236+ (* [ *)
237237+ (* prefix *)
238238+ (* ; x ^ "\n" |> unsafe_blit_start_if "@" "◉" |> Jj_tui.AnsiReverse.colored_string *)
239239+ (* ] *)
240240+ (* |> Ui.atom) *)
241241+ (* |> List.map2 (fun rev ui -> Wd.{ ui; data = rev }) revs *)
242242+ (* |> Lwd.pure *)
243243+ (* |> Wd.filterable_selection_list *)
244244+ (* ~on_confirm:(fun _ -> ()) *)
245245+ (* ~filter_predicate:(fun input rev -> rev |> String.starts_with ~prefix:input) *)
246246+ (* ;; *)
201247202248 (** Start a process that will take full control of both stdin and stdout.
203249 This is used for interactive diffs and such*)
···44open! Util
55open Shared
66open Border_box
77+78(**Selectable list item with a ui and some data *)
89type 'a selectable_item = {
910 data : 'a
···5051 List.nth_opt items selected |> Option.iter (fun x -> on_selection_change x.data);
5152 items, selected
5253 in
5353- (*Ui.vcat can be a little weird when the*)
5454+ (* Ui.vcat can be a little weird when the *)
5455 items
5556 |> List.mapi (fun i x ->
5657 if selected == i
···9293 (*portion of the list that is behind the selection*)
9394 let list_ratio =
9495 ((selected |> float_of_int) +. offset) /. (length |> float_of_int)
9696+ in
9797+ (*if our position is further down the list than the portion that is shown we will shift by that amoumt *)
9898+ Float.max (list_ratio -. size_ratio) 0.0 *. (size |> snd |> float_of_int)
9999+ |> int_of_float
100100+ in
101101+ let$ items = render_items
102102+ and$ shift_amount = shift_amount in
103103+ items
104104+ |> Ui.shift_area 0 shift_amount
105105+ |> Ui.resize ~sh:1
106106+ |> simpleSizeSensor ~size_var
107107+ |> Ui.resize ~w:3 ~sw:1 ~h:0
108108+ |> simpleSizeSensor ~size_var:rendered_size_var
109109+ in
110110+ scrollitems
111111+;;
112112+113113+type 'a maybeSelectable =
114114+ | Selectable of 'a selectable_item
115115+ | Filler of Ui.t
116116+117117+(** Same as [selection_list_custom] except that it supports not all element in the list being selectable *)
118118+let selection_list_exclusions
119119+ ?(focus = Focus.make ())
120120+ ?(on_selection_change = fun _ -> ())
121121+ ~custom_handler
122122+ (items : 'a maybeSelectable array Lwd.t)
123123+ =
124124+ (*
125125+ The rough overview is:
126126+ 1. Make a lookup list that has the indexes of all the selectable items within the overall list, we will be selecting from those
127127+ 2. Render the items, making sure to tell the selected one to render as selected.
128128+ 3. Calculate how much we should scroll by.
129129+ 4. offset by the scroll amount, apply size sensors and output final ui
130130+ *)
131131+ let selected_var = Lwd.var 0 in
132132+ let selected_position = Lwd.var (0, 0) in
133133+ let selectable_item_indexes =
134134+ let$ items = items in
135135+ let lut = Array.make (Array.length items) 0 in
136136+ let final_len =
137137+ items
138138+ |> Base.Array.foldi ~init:0 ~f:(fun i selectable_count item ->
139139+ match item with
140140+ | Selectable _ ->
141141+ Array.set lut selectable_count i;
142142+ selectable_count + 1
143143+ | Filler _ ->
144144+ selectable_count)
145145+ in
146146+ Array.sub lut 0 final_len
147147+ in
148148+ (*handle selections*)
149149+ let render_items =
150150+ let$ focus = focus |> Focus.status
151151+ and$ items, selected, selectable_item_indexes =
152152+ (* This doesn't depend on changes in focus but it should update whenever there are new items or a selection change*)
153153+ let$ items = items
154154+ and$ selectable_item_indexes = selectable_item_indexes
155155+ and$ selected = Lwd.get selected_var in
156156+ (* First ensure if our list has gotten shorter we haven't selected off the list*)
157157+ (* We do this here to ensure that the selected var is updated before we render to avoid double rendering*)
158158+ let max_selected = Int.max 0 (Array.length selectable_item_indexes - 1) in
159159+ if Int.min selected max_selected <> selected then selected_var $= max_selected;
160160+ let selected = Lwd.peek selected_var in
161161+ (* We lookup the index of the selected item in the list of items, remeber our list isn't entirely selectable items*)
162162+ if Array.length selectable_item_indexes > 0
163163+ then (
164164+ let item_idx = selectable_item_indexes.(selected) in
165165+ items.(item_idx) |> fun x ->
166166+ (match x with
167167+ | Selectable s ->
168168+ on_selection_change s.data
169169+ | Filler _ ->
170170+ failwith "selected an item that wasn't selectable. This shouldn't be possible");
171171+ items, item_idx, selectable_item_indexes)
172172+ else items, 0, selectable_item_indexes
173173+ in
174174+ (* Ui.vcat can be a little weird when the *)
175175+ items
176176+ |> Array.mapi (fun i x ->
177177+ match x with
178178+ | Filler ui ->
179179+ ui
180180+ | Selectable x ->
181181+ if selected == i
182182+ then
183183+ x.ui true
184184+ |> Ui.transient_sensor (fun ~x ~y ~w:_ ~h:_ () ->
185185+ if (x, y) <> Lwd.peek selected_position then selected_position $= (x, y))
186186+ else x.ui false)
187187+ |> Array.to_list
188188+ |> Ui.vcat
189189+ |> Ui.keyboard_area ~focus (function
190190+ | `Arrow `Up, [] ->
191191+ let selected = max (Lwd.peek selected_var - 1) 0 in
192192+ selected_var $= selected;
193193+ `Handled
194194+ | `Arrow `Down, [] ->
195195+ let selected =
196196+ Int.max
197197+ (min
198198+ (Lwd.peek selected_var + 1)
199199+ ((selectable_item_indexes |> Array.length) - 1))
200200+ 0
201201+ in
202202+ selected_var $= selected;
203203+ `Handled
204204+ | a ->
205205+ custom_handler items selected_var a)
206206+ in
207207+ let rendered_size_var = Lwd.var (0, 0) in
208208+ (*Handle scrolling*)
209209+ let scrollitems =
210210+ let size_var = Lwd.var (0, 0) in
211211+ let shift_amount =
212212+ (*get the actual idx not just the selection number*)
213213+ let$ selected_idx =
214214+ Lwd.map2
215215+ (Lwd.get selected_var)
216216+ selectable_item_indexes
217217+ ~f:(fun selected indexes ->
218218+ if Array.length indexes > 0 then indexes.(selected) else 0)
219219+ and$ size = Lwd.get size_var
220220+ and$ length = items |>$ Array.length
221221+ and$ ren_size = Lwd.get rendered_size_var in
222222+ (*portion of the total size of the element that is rendered*)
223223+ let size_ratio =
224224+ (ren_size |> snd |> float_of_int) /. (size |> snd |> float_of_int)
225225+ in
226226+ (*Tries to ensure that we start scrolling the list when we've selected about a third of the way down (using 3.0 causes weird jumping, so i use just less than )*)
227227+ let offset = size_ratio *. ((length |> float_of_int) /. 2.9) in
228228+ (*portion of the list that is behind the selection*)
229229+ let list_ratio =
230230+ ((selected_idx |> float_of_int) +. offset) /. (length |> float_of_int)
95231 in
96232 (*if our position is further down the list than the portion that is shown we will shift by that amoumt *)
97233 Float.max (list_ratio -. size_ratio) 0.0 *. (size |> snd |> float_of_int)
+2-2
jj_tui/lib/widgets/widgets.ml
···305305 match show_popup with
306306 | Some (content, label) ->
307307 let prompt_field = content in
308308- prompt_field |>$ Ui.resize ~w:5 |> border_box ~label_top:label|> clear_bg
308308+ prompt_field |>$ Ui.resize ~w:5 |> border_box ~label_top:label |> clear_bg
309309 | None ->
310310 Ui.empty |> Lwd.pure
311311 in
···323323;;
324324325325let is_focused ~focus f ui =
326326- Lwd.map2 ui (focus |> Focus.status) ~f:(fun ui focus -> f ui (focus |> Focus.has_focus) )
326326+ Lwd.map2 ui (focus |> Focus.status) ~f:(fun ui focus -> f ui (focus |> Focus.has_focus))
327327;;
-1
jj_tui/widget-test/main.ml
···261261 `Unhandled)
262262 in
263263 let outerFocus = Focus.make () in
264264-265264 (*We wrap the button in some more UI*)
266265 let$ outer = W.vbox [ button "I'm a button"; Lwd.get output |>$ W.string ]
267266 and$ focus = Focus.status outerFocus in