···128128129129type 'a selection_list_prompt_data =
130130 { label : string
131131- ; items : 'a Selection_list.selectable_item list Lwd.t
131131+ ; items : 'a Selection_list.multi_selectable_item list Lwd.t
132132 ; on_exit : [ `Closed | `Finished of 'a ] -> unit
133133 }
134134···177177178178type 'a filterable_selection_list_prompt_data =
179179 { label : string
180180- ; items : 'a Selection_list.selectable_item list Lwd.t
181181- ;filter_predicate:(string-> 'a-> bool)
180180+ ; items : 'a Selection_list.multi_selectable_item list Lwd.t
181181+ ; filter_predicate : string -> 'a -> bool
182182 ; on_exit : [ `Closed | `Finished of 'a ] -> unit
183183 }
184184···195195 let$ show_prompt_val = Lwd.get show_prompt_var in
196196 show_prompt_val
197197 |> Option.map
198198- @@ fun { label; items;filter_predicate; on_exit } ->
198198+ @@ fun { label; items; filter_predicate; on_exit } ->
199199 let on_exit result =
200200 Focus.release_reversable focus;
201201 show_prompt_var $= None;
···206206 Selection_list.filterable_selection_list
207207 ~filter_predicate
208208 ~focus
209209- ~on_confirm:(fun item ->
210210- (`Finished item) |> on_exit;
211211- )
209209+ ~on_confirm:(fun item -> `Finished item |> on_exit)
212210 items
213211 |> modify_body
214212 in
···227225 match show_popup with
228226 | Some (content, label) ->
229227 let prompt_field = content in
230230- prompt_field |>$ Ui.resize ~w:5 ~sw:1 |> BB.box ~label_top:label |> clear_bg
228228+ prompt_field |>$ Ui.resize ~w:5 ~sw:1 |> BB.box ~label_top:label |> clear_bg
231229 | None -> Ui.empty |> Lwd.pure
232230 in
233231 W.zbox [ ui; popup_ui |>$ Ui.resize ~crop:neutral_grav ~pad:neutral_grav ]
+6-6
forks/nottui/lib/nottui/widgets/overlays.mli
···1919 on_exit : [ `Closed | `Finished of string ] -> unit;
2020}
21212222-(** Text box prompt that takes user input then calls [on_exit] with the result.
2222+(** Text box prompt that takes user input then calls [on_exit] with the result.
23232424This will display ontop of any ui it is passed when show_prompt_var is [Some].*)
2525···3535(** Config for a selection_list_prompt*)
3636type 'a selection_list_prompt_data = {
3737 label : string;
3838- items : 'a Selection_list.selectable_item list Lwd.t;
3838+ items : 'a Selection_list.multi_selectable_item list Lwd.t;
3939 on_exit : [ `Closed | `Finished of 'a ] -> unit;
4040}
41414242(** Selection_list prompt.
43434444This will display ontop of any ui it is passed when show_prompt_var is [Some].
4545-@param modify_body Function that takes the completed body of the prompt, incase you want to resize it or otherwise change it
4545+@param modify_body Function that takes the completed body of the prompt, incase you want to resize it or otherwise change it
4646*)
4747val selection_list_prompt :
4848 ?pad_w:int ->
···55555656type 'a filterable_selection_list_prompt_data =
5757 { label : string
5858- ; items : 'a Selection_list.selectable_item list Lwd.t
5858+ ; items : 'a Selection_list.multi_selectable_item list Lwd.t
5959 ;filter_predicate:(string-> 'a-> bool)
6060 ; on_exit : [ `Closed | `Finished of 'a ] -> unit
6161 }
6262(** Selection_list prompt that is filterable.
63636464This will display ontop of any ui it is passed when show_prompt_var is [Some].
6565-@param modify_body Function that takes the completed body of the prompt, incase you want to resize it or otherwise change it
6565+@param modify_body Function that takes the completed body of the prompt, incase you want to resize it or otherwise change it
6666*)
6767val selection_list_prompt_filterable :
6868 ?pad_w:int ->
···7171 ?focus:Nottui_main.Focus.handle ->
7272 show_prompt_var:'a filterable_selection_list_prompt_data option Lwd.var ->
7373 Nottui_main.ui Lwd.t -> Nottui_main.ui Lwd.t
7474-7474+75757676 (**This is a simple popup that can show ontop of other ui elements *)
7777val popup :
+267-32
forks/nottui/lib/nottui/widgets/selection_list.ml
···33open Lwd_infix
44open Shared
5566-type 'a selectable_item =
66+let thrd (_, _, x) = x
77+88+type 'a multi_selectable_item =
79 { data : 'a
88- ; ui : bool -> Ui.t Lwd.t
1010+ ; id : int
1111+ ; ui : selected:bool -> hovered:bool -> Ui.t Lwd.t
912 }
10131111-type 'a maybeSelectable =
1212- | Selectable of 'a selectable_item
1414+type 'a maybe_multi_selectable =
1515+ | Selectable of 'a multi_selectable_item
1316 | Filler of Ui.t Lwd.t
14171515-let selection_list_exclusions
1616- ?(focus = Focus.make ())
1717- ?(on_selection_change = fun _ -> ())
1818- ~custom_handler
1919- (items : 'a maybeSelectable array Lwd.t)
2020- =
2121- (*
2222- The rough overview is:
2323- 1. Make a new list that only contains our selectable items
2424- 2. Render the items, making sure to tell the selected one to render as selected.
2525- 3. Calculate how much we should scroll by.
2626- 4. offset by the scroll amount, apply size sensors and output final ui
2727- *)
2828- let selected_var = Lwd.var 0 in
2929- let selected_position = Lwd.var (0, 0) in
1818+module MyMap = Map.Make (Int)
1919+2020+(** Get a map of all the selectable items*)
2121+let get_selectable_items_map (items : 'a maybe_multi_selectable array Lwd.t) =
2222+ let selectable_items =
2323+ let$ items = items in
2424+ (*Map of selectable items with their id as key*)
2525+ items
2626+ |> Array.fold_left
2727+ (fun map item ->
2828+ match item with
2929+ | Selectable item -> MyMap.add item.id (item.id, item) map
3030+ | Filler _ -> map)
3131+ MyMap.empty
3232+ in
3333+ selectable_items
3434+;;
3535+3636+let get_selectable_items (items : 'a maybe_multi_selectable array Lwd.t) =
3037 let selectable_items =
3138 let$ items = items in
3239 (*Array of selectable items and their idx in the original array*)
···4855 in
4956 Array.sub selectable_items 0 final_len
5057 in
5858+ selectable_items
5959+;;
6060+6161+let multi_selection_list_exclusions
6262+ ?(focus = Focus.make ())
6363+ ?(on_selection_change = fun ~hovered ~selected -> ())
6464+ ~custom_handler
6565+ (items : 'a maybe_multi_selectable array Lwd.t)
6666+ =
6767+ (*
6868+ The rough overview is:
6969+ 1. Make a new list that only contains our selectable items
7070+ 2. Render the items, making sure to tell the selected one to render as selected.
7171+ 3. Calculate how much we should scroll by.
7272+ 4. offset by the scroll amount, apply size sensors and output final ui
7373+ *)
7474+ let selected_items_var = Lwd.var MyMap.empty in
7575+ (*hovered var is a tuple of (id, overall_idx,selection_idx)*)
7676+ (*we set it up this way so we can avoid double rendering. We sometimes wish to change the value of the hover var during rendering and that would not update till the next render and cause a re-render*)
7777+ let hovered_var = ref (0, 0, 0) in
7878+ let hover_changed = Lwd.var () in
7979+ let selected_position = Lwd.var (0, 0) in
8080+ let selectable_items_map = get_selectable_items_map items in
8181+ let selectable_items = get_selectable_items items in
5182 (*handle selections*)
5283 let render_items =
5384 let$* focus = focus |> Focus.status
5454- and$ items, selected, selectable_items =
8585+ and$ items, selectable_items =
8686+ (* This doesn't depend on changes in focus but it should update whenever there are new items or a selection change*)
8787+ let$ items = items
8888+ and$ selectable_items = selectable_items in
8989+ (*We are only beeking this one because we only want this run when the items list changes*)
9090+ let hovered_id, hovered_ovearll_idx, hovered_selection_idx = !hovered_var in
9191+ (*TODO: This is obviously very slow*)
9292+ selected_items_var
9393+ $= (Lwd.peek selected_items_var
9494+ |> MyMap.filter (fun selected value ->
9595+ selectable_items |> Array.exists (fun (_, item) -> item.id = selected)));
9696+ (*first we handle upading our selection if the list of items has changed*)
9797+ (* We do this here to ensure that the selected var is updated before we render to avoid double rendering*)
9898+ let hovered_id, hovered_ovearll_idx, hovered_selection_idx =
9999+ (*If the id is not in the list anymore then we should just pick the closest item by index*)
100100+ (* We have a few progressively less ideal states here:
101101+ 1. We found our exact same id item in a new place
102102+ 2. We found an item that's in the same location as the previous one
103103+ 3. We just return something
104104+ *)
105105+ selectable_items
106106+ |> Array.fold_left
107107+ (fun (count, acc) (idx, item) ->
108108+ let nCount = count + 1 in
109109+ match acc with
110110+ | `Found _ -> nCount, acc
111111+ | `Same_idx _ ->
112112+ if item.id = hovered_id
113113+ then nCount, `Found (hovered_id, idx, count)
114114+ else nCount, acc
115115+ | `Searching _ ->
116116+ if item.id = hovered_id
117117+ then nCount, `Found (hovered_id, idx, count)
118118+ else if count == hovered_selection_idx
119119+ then nCount, `Same_idx (hovered_id, idx, count)
120120+ else nCount, `Searching (hovered_id, idx, count))
121121+ (0, `Searching (0, 0, 0))
122122+ |> snd
123123+ |> function
124124+ | `Found (id, idx, count) -> id, idx, count
125125+ | `Same_idx (id, idx, count) -> id, idx, count
126126+ | `Searching (id, idx, count) -> id, idx, count
127127+ in
128128+ hovered_var := hovered_id, hovered_ovearll_idx, hovered_selection_idx;
129129+ if Array.length selectable_items > 0
130130+ then (
131131+ let item_idx, item = selectable_items.(hovered_selection_idx) in
132132+ on_selection_change
133133+ ~hovered:item.data
134134+ ~selected:
135135+ (Lwd.peek selected_items_var |> MyMap.to_list |> List.map (fun (_,a) -> a));
136136+ items, selectable_items)
137137+ else items, selectable_items
138138+ and$ _ = Lwd.get hover_changed
139139+ and$ selected_items = Lwd.get selected_items_var in
140140+ let _, _, hovered = !hovered_var in
141141+ (*==== Rendering The list ====*)
142142+ (* Ui.vcat can be a little weird when the *)
143143+ if items |> Array.length = 0
144144+ then Ui.empty |> Lwd.pure
145145+ else
146146+ items
147147+ |> Array.mapi (fun i x ->
148148+ match x with
149149+ | Filler ui -> ui
150150+ | Selectable x ->
151151+ let hovered = hovered == i in
152152+ let selected = selected_items |> MyMap.mem x.id in
153153+ if hovered
154154+ then
155155+ x.ui ~hovered ~selected
156156+ |>$ Ui.transient_sensor (fun ~x ~y ~w:_ ~h:_ () ->
157157+ if (x, y) <> Lwd.peek selected_position then selected_position $= (x, y))
158158+ else x.ui ~hovered ~selected)
159159+ |> Array.to_list
160160+ |> Shared.vbox
161161+ |>$ Ui.keyboard_area ~focus (function
162162+ | `Arrow `Up, [] ->
163163+ let hovered_idx = max ((!hovered_var |> thrd) - 1) 0 in
164164+ let hovered = (selectable_items.(hovered_idx) |> snd).id, 0, hovered_idx in
165165+ hovered_var := hovered;
166166+ Lwd.set hover_changed ();
167167+ on_selection_change
168168+ ~hovered:(selectable_items.(hovered_idx) |> snd).data
169169+ ~selected:
170170+ (Lwd.peek selected_items_var |> MyMap.to_list |> List.map (fun (_,a) -> a));
171171+ `Handled
172172+ | `Arrow `Down, [] ->
173173+ let hovered_idx =
174174+ Int.max
175175+ (min ((!hovered_var |> thrd) + 1) ((selectable_items |> Array.length) - 1))
176176+ 0
177177+ in
178178+ let hovered = (selectable_items.(hovered_idx) |> snd).id, 0, hovered_idx in
179179+ hovered_var := hovered;
180180+ Lwd.set hover_changed ();
181181+ on_selection_change
182182+ ~hovered:(selectable_items.(hovered_idx) |> snd).data
183183+ ~selected:
184184+ (Lwd.peek selected_items_var |> MyMap.to_list |> List.map (fun (_,a) -> a));
185185+ `Handled
186186+ | `ASCII ' ', [] ->
187187+ let hovered_id, _, hovered_idx = !hovered_var in
188188+ let data=
189189+(selectable_items.(hovered_idx) |> snd).data in let selected = Lwd.peek selected_items_var in
190190+ if selected |> MyMap.mem hovered_id
191191+ then Lwd.set selected_items_var (MyMap.remove hovered_id selected)
192192+ else Lwd.set selected_items_var (MyMap.add hovered_id data selected);
193193+ (* TODO: make sure this actually apllies, there is some chance the peek will no update*)
194194+ on_selection_change
195195+ ~hovered:data
196196+ ~selected:
197197+ (Lwd.peek selected_items_var |> MyMap.to_list |> List.map (fun (_,a)-> a));
198198+ `Handled
199199+ | a -> custom_handler ~selected:(Lwd.peek selected_items_var) ~selectable_items a)
200200+ in
201201+ let rendered_size_var = Lwd.var (0, 0) in
202202+ (*Handle scrolling*)
203203+ let scrollitems =
204204+ let size_var = Lwd.var (0, 0) in
205205+ let shift_amount =
206206+ (*get the actual idx not just the selection number*)
207207+ let$ selected_idx =
208208+ Lwd.map2 (Lwd.get hover_changed) selectable_items ~f:(fun () selectable ->
209209+ let _, _, hovered_idx = !hovered_var in
210210+ if Array.length selectable > hovered_idx
211211+ then selectable.(hovered_idx) |> fst
212212+ else 0)
213213+ and$ size = Lwd.get size_var
214214+ and$ length = items |>$ Array.length
215215+ and$ ren_size = Lwd.get rendered_size_var in
216216+ (*portion of the total size of the element that is rendered*)
217217+ let size_ratio =
218218+ (ren_size |> snd |> float_of_int) /. (size |> snd |> float_of_int)
219219+ in
220220+ (*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 )*)
221221+ let offset = size_ratio *. ((length |> float_of_int) /. 2.9) in
222222+ (*portion of the list that is behind the selection*)
223223+ let list_ratio =
224224+ ((selected_idx |> float_of_int) +. offset) /. (length |> float_of_int)
225225+ in
226226+ (*if our position is further down the list than the portion that is shown we will shift by that amoumt *)
227227+ Float.max (list_ratio -. size_ratio) 0.0 *. (size |> snd |> float_of_int)
228228+ |> int_of_float
229229+ in
230230+ let$ items = render_items
231231+ and$ shift_amount = shift_amount in
232232+ items
233233+ |> Ui.shift_area 0 shift_amount
234234+ |> Ui.resize ~sh:1
235235+ |> simpleSizeSensor ~size_var
236236+ |> Ui.resize ~w:3 ~sw:1 ~h:0
237237+ |> simpleSizeSensor ~size_var:rendered_size_var
238238+ in
239239+ scrollitems
240240+;;
241241+242242+let selection_list_exclusions
243243+ ?(focus = Focus.make ())
244244+ ?(on_selection_change = fun _ -> ())
245245+ ~custom_handler
246246+ (items : 'a maybe_multi_selectable array Lwd.t)
247247+ =
248248+ (*
249249+ The rough overview is:
250250+ 1. Make a new list that only contains our selectable items
251251+ 2. Render the items, making sure to tell the selected one to render as selected.
252252+ 3. Calculate how much we should scroll by.
253253+ 4. offset by the scroll amount, apply size sensors and output final ui
254254+ *)
255255+ let selected_var = Lwd.var 0 in
256256+ let selected_position = Lwd.var (0, 0) in
257257+ let selectable_items = get_selectable_items items in
258258+ (*handle selections*)
259259+ let render_items =
260260+ let$* focus = focus |> Focus.status
261261+ and$ items, hovered, selectable_items =
55262 (* This doesn't depend on changes in focus but it should update whenever there are new items or a selection change*)
56263 let$ items = items
57264 and$ selectable_items = selectable_items
···83290 match x with
84291 | Filler ui -> ui
85292 | Selectable x ->
8686- if selected == i
293293+ if hovered == i
87294 then
8888- x.ui true
295295+ x.ui ~hovered:true ~selected:false
89296 |>$ Ui.transient_sensor (fun ~x ~y ~w:_ ~h:_ () ->
90297 if (x, y) <> Lwd.peek selected_position then selected_position $= (x, y))
9191- else x.ui false)
298298+ else x.ui ~hovered:false ~selected:false)
92299 |> Array.to_list
93300 |> Shared.vbox
94301 |>$ Ui.keyboard_area ~focus (function
···144351 scrollitems
145352;;
146353147147-let selectable_item ui is_focused =
354354+let selectable_item ui ~selected ~hovered =
148355 let height = Ui.layout_height ui in
149356 let prefix =
150150- if is_focused then I.char A.(bg blue) '>' 1 height else I.char A.empty ' ' 1 height
357357+ if selected && hovered (* (Uchar.of_int 0x2265) *)
358358+ then I.uchar A.(bg A.cyan ++ fg black ++ st bold) (Uchar.of_char 'x') 1 height
359359+ else if selected
360360+ then I.uchar A.(bg A.cyan ++ fg black ++ st bold) (Uchar.of_char 'o') 1 height
361361+ else if hovered
362362+ then I.uchar A.(fg A.cyan ++ st bold) (Uchar.of_int 0x25b6) 1 height
363363+ else I.char A.empty ' ' 1 height
151364 in
152365 Ui.hcat [ prefix |> Ui.atom; ui ] |> Lwd.pure
153366;;
154367155155-let selectable_item_lwd ui is_focused =
368368+let selectable_item_lwd ui ~selected ~hovered =
156369 let$ ui = ui in
157370 let height = Ui.layout_height ui in
158371 let prefix =
159159- if is_focused then I.char A.(bg blue) '>' 1 height else I.char A.empty ' ' 1 height
372372+ if selected && hovered
373373+ then I.uchar A.(bg blue) (Uchar.of_int 0x2265) 1 height
374374+ else if selected
375375+ then I.char A.(bg blue) '=' 1 height
376376+ else if hovered
377377+ then I.char A.(bg blue) '>' 1 height
378378+ else I.char A.empty ' ' 1 height
160379 in
161380 Ui.hcat [ prefix |> Ui.atom; ui ]
162381;;
163382383383+let multi_selection_list_custom
384384+ ?(focus = Focus.make ())
385385+ ?(on_selection_change = fun ~hovered ~selected -> ())
386386+ ~custom_handler
387387+ (items : 'a multi_selectable_item list Lwd.t)
388388+ =
389389+ multi_selection_list_exclusions
390390+ ~focus
391391+ ~on_selection_change
392392+ ~custom_handler
393393+ (items
394394+ |>$ fun items ->
395395+ let selectable_items = Array.make (List.length items) (Obj.magic ()) in
396396+ items |> List.iteri (fun i x -> Array.set selectable_items i (Selectable x));
397397+ selectable_items)
398398+164399let selection_list_custom
165400 ?(focus = Focus.make ())
166401 ?(on_selection_change = fun _ -> ())
167402 ~custom_handler
168168- (items : 'a selectable_item list Lwd.t)
403403+ (items : 'a multi_selectable_item list Lwd.t)
169404 =
170405 selection_list_exclusions
171406 ~focus
···183418 ~(filter_predicate : string -> 'a -> bool)
184419 ~custom_handler
185420 ~filter_text_var
186186- (items : 'a selectable_item list Lwd.t)
421421+ (items : 'a multi_selectable_item list Lwd.t)
187422 =
188423 (*filter the list whenever the input changes*)
189424 let items =
···204439;;
205440206441let filterable_selection_list
207207- ?(pad_w=1)
208208- ?(pad_h=0)
442442+ ?(pad_w = 1)
443443+ ?(pad_h = 0)
209444 ?(focus = Focus.make ())
210445 ~filter_predicate
211446 ?(on_esc = fun _ -> ())
···241476 vbox
242477 [ filter_text_ui |> Border_box.box ~pad_w ~pad_h
243478 ; (list_ui
244244- |> Border_box.box ~pad_w ~pad_h
479479+ |> Border_box.box ~pad_w ~pad_h
245480 |>$ fun x ->
246481 let mw = (x |> Ui.layout_spec).mw in
247482 if mw > Lwd.peek max_width then max_width $= mw;
···11+open Nottui_main
22+module MyMap : Map.S with type key = int
33+14(**Selectable list item with a ui and some data *)
22-type 'a selectable_item =
55+type 'a multi_selectable_item =
36 { data : 'a
47 (**info attached to each ui elment in the list, used for filtering and on_select callback *)
55- ; ui : bool -> Nottui_main.ui Lwd.t
88+ ; id : int
99+ ; ui : selected:bool -> hovered:bool -> Ui.t Lwd.t
610 }
71188-type 'a maybeSelectable =
99- | Selectable of 'a selectable_item
1010- | Filler of Nottui_main.ui Lwd.t
1212+type 'a maybe_multi_selectable =
1313+ | Selectable of 'a multi_selectable_item
1414+ | Filler of Ui.t Lwd.t
1515+1616+(** multi_selectable exclusions *)
1717+val multi_selection_list_exclusions
1818+ : ?focus:Nottui_main.Focus.handle
1919+ -> ?on_selection_change:(hovered:'a -> selected:'a list -> unit)
2020+ -> custom_handler:
2121+ (selected:'a MyMap.t
2222+ -> selectable_items:(int * 'a multi_selectable_item) array
2323+ -> Nottui_main.Ui.key
2424+ -> Nottui_main.Ui.may_handle)
2525+ -> 'a maybe_multi_selectable array Lwd.t
2626+ -> Nottui_main.ui Lwd.t
11271228(** Same as [selection_list_custom] except that it supports not all element in the list being selectable *)
1329val selection_list_exclusions
1430 : ?focus:Nottui_main.Focus.handle
1531 -> ?on_selection_change:('a -> unit)
1632 -> custom_handler:
1717- ('a selectable_item -> Nottui_main.Ui.key -> Nottui_main.Ui.may_handle)
1818- -> 'a maybeSelectable array Lwd.t
3333+ ('a multi_selectable_item -> Nottui_main.Ui.key -> Nottui_main.Ui.may_handle)
3434+ -> 'a maybe_multi_selectable array Lwd.t
1935 -> Nottui_main.ui Lwd.t
20362137(**Makes a ui element selectable.
···2339 Takes [ui] and returns a function that appends '>' to the start when given [true] and ' ' when false
24402541 Used in conjuction with [selection_list_custom]*)
2626-val selectable_item : Nottui_main.ui -> bool -> Nottui_main.ui Lwd.t
4242+val selectable_item
4343+ : Nottui_main.ui
4444+ -> selected:bool
4545+ -> hovered:bool
4646+ -> Nottui_main.ui Lwd.t
27472828-val selectable_item_lwd : Nottui_main.ui Lwd.t -> bool -> Nottui_main.ui Lwd.t
4848+val selectable_item_lwd
4949+ : Nottui_main.ui Lwd.t
5050+ -> selected:bool
5151+ -> hovered:bool
5252+ -> Nottui_main.ui Lwd.t
5353+5454+(** multi selection list that allows for custom handling of keyboard events.
5555+ Scrolls when the selection reaches the lower third
5656+ Only handles up and down keyboard events. Use [~custom_handler] to do handle confirming your selection and such *)
5757+val multi_selection_list_custom
5858+ : ?focus:Nottui_main.Focus.handle
5959+ -> ?on_selection_change:(hovered:'a -> selected:'a list -> unit)
6060+ -> custom_handler:
6161+ (selected:'a MyMap.t
6262+ -> selectable_items:(int * 'a multi_selectable_item) array
6363+ -> Nottui_main.Ui.key
6464+ -> Nottui_main.Ui.may_handle)
6565+ -> 'a multi_selectable_item list Lwd.t
6666+ -> Nottui_main.ui Lwd.t
29673068(** Selection list that allows for custom handling of keyboard events.
3169 Scrolls when the selection reaches the lower third
···3472 : ?focus:Nottui_main.Focus.handle
3573 -> ?on_selection_change:('a -> unit)
3674 -> custom_handler:
3737- ('a selectable_item -> Nottui_main.Ui.key -> Nottui_main.Ui.may_handle)
3838- -> 'a selectable_item list Lwd.t
7575+ ('a multi_selectable_item -> Nottui_main.Ui.key -> Nottui_main.Ui.may_handle)
7676+ -> 'a multi_selectable_item list Lwd.t
3977 -> Nottui_main.ui Lwd.t
40784179(** A filterable selectable list.
···4785 : ?focus:Nottui_main.Focus.handle
4886 -> filter_predicate:(string -> 'a -> bool)
4987 -> custom_handler:
5050- ('a selectable_item -> Nottui_main.Ui.key -> Nottui_main.Ui.may_handle)
8888+ ('a multi_selectable_item -> Nottui_main.Ui.key -> Nottui_main.Ui.may_handle)
5189 -> filter_text_var:string Lwd.var
5252- -> 'a selectable_item list Lwd.t
9090+ -> 'a multi_selectable_item list Lwd.t
5391 -> Nottui_main.ui Lwd.t
54925593(** Filterable selection list
···67105 -> filter_predicate:(string -> 'a -> bool)
68106 -> ?on_esc:('a -> unit)
69107 -> on_confirm:('a -> unit)
7070- -> 'a selectable_item list Lwd.t
108108+ -> 'a multi_selectable_item list Lwd.t
71109 -> Nottui_main.ui Lwd.t
+35-21
jj_tui/bin/file_view.ml
···1010 open Jj_tui
1111 open Picos_std_structured
12121313- let selected_file = Lwd.var ""
1313+ let active_files= Lwd.var [""]
14141515 let rec command_mapping =
1616 [
···3232 ( "Revision to move file to"
3333 , fun rev ->
3434 Cmd
3535- [
3535+ ([
3636 "squash"
3737 ; "-u"
3838 ; "--keep-emptied"
3939 ; "--from"
4040- ; get_selected_rev ()
4040+ ; get_hovered_rev ()
4141 ; "--into"
4242 ; rev
4343- ; Lwd.peek selected_file
4444- ] )
4343+ ]
4444+ @
4545+ (Lwd.peek active_files))
4646+ )
4547 }
4648 ; {
4749 key = 'N'
···4951 ; cmd =
5052 Dynamic_r
5153 (fun rev ->
5252- Cmd
5454+ Cmd (
5355 [
5456 "squash"
5557 ; "-u"
···5860 ; rev
5961 ; "--into"
6062 ; rev ^ "+"
6161- ; Lwd.peek selected_file
6262- ])
6363+ ]@
6464+ Lwd.peek active_files
6565+ )
6666+ )
6367 }
6468 ; {
6569 key = 'P'
···6771 ; cmd =
6872 Dynamic_r
6973 (fun rev ->
7070- Cmd
7474+ Cmd(
7175 [
7276 "squash"
7377 ; "-u"
···7680 ; rev
7781 ; "--into"
7882 ; rev ^ "-"
7979- ; Lwd.peek selected_file
8080- ])
8383+ ]@
8484+ Lwd.peek active_files
8585+ )
8686+ )
8187 }
8288 ; {
8389 key = 'd'
···8591 ; cmd =
8692 Dynamic_r
8793 (fun rev ->
8888- let selected = Lwd.peek selected_file in
9494+ let selected = Lwd.peek active_files in
8995 confirm_prompt
9090- ("discard all changes to '" ^ selected ^ "' in rev " ^ rev)
9191- (Cmd [ "restore"; "--to"; rev; "--from"; rev ^ "-"; selected ]))
9696+ ("discard all changes to '" ^ (selected|>String.concat "\n") ^ "' in rev " ^ rev)
9797+ (Cmd (["restore"; "--to"; rev; "--from"; rev ^ "-"] @selected)))
9298 }
9399 ]
94100 ;;
···98104 let$ files = Lwd.get Vars.ui_state.jj_change_files in
99105 files
100106 |> List.map (fun (_modifier, file) ->
101101- W.Lists.{ data = file; ui = W.Lists.selectable_item (W.string file) })
107107+ W.Lists.
108108+ {
109109+ data = file
110110+ ; id = file |> String.hash
111111+ ; ui = W.Lists.selectable_item (W.string file)
112112+ })
102113 in
103114 (*TODO:
104115 This should be redesigned completely
···106117 It will have a cancellation system just like this one.
107118 when any of the dependencies change, selected file, selected rev, focus etc, it will re-render if needed and cancel the current rendering.
108119 *)
109109- W.Lists.selection_list_custom
110110- ~on_selection_change:(fun selected ->
111111- Lwd.set selected_file selected;
120120+ file_uis|>
121121+ W.Lists.multi_selection_list_custom
122122+ ~on_selection_change:(fun ~hovered ~selected ->
123123+ let active=
124124+ if selected|>List.length =0 then [hovered] else selected
125125+ in
126126+ Lwd.set active_files active;
112127 if Focus.peek_has_focus focus
113113- then Show_view.(pushStatus (File_preview (Vars.get_selected_rev (),selected))))
114114- ~custom_handler:(fun _ key ->
128128+ then Show_view.(pushStatus (File_preview (Vars.get_hovered_rev (), hovered))))
129129+ ~custom_handler:(fun ~selected:_ ~selectable_items:_ key ->
115130 match key with `ASCII k, [] -> handleInputs command_mapping k | _ -> `Unhandled)
116116- file_uis
117131 ;;
118132end
+7-13
jj_tui/bin/global_funcs.ml
···2929 This should be called after any command that performs a change *)
3030let update_status ?(update_graph = true) ?(cause_snapshot = false) () =
3131 safe_jj (fun () ->
3232- let rev = Lwd.peek Vars.ui_state.selected_revision in
3232+ let rev = Lwd.peek Vars.ui_state.hovered_revision in
3333 let log_res = jj_no_log ~snapshot:cause_snapshot [ "log" ] |> colored_string in
3434 (* TODO: chagne this because it makes us always a frame behind *)
3535 if update_graph then Vars.ui_state.trigger_update $= ())
···3939 This should be called after any command that performs a change *)
4040let update_views ?(cause_snapshot = false) () =
4141 safe_jj (fun () ->
4242- let rev = Vars.get_selected_rev () in
4343- Flock.join_after @@ fun () ->
4444- let tree =
4545- jj_no_log ~snapshot:cause_snapshot [ "log"; "-r"; rev ] |> colored_string
4242+ let rev = Vars.get_hovered_rev () in
4343+ let branches =
4444+ jj_no_log ~snapshot:cause_snapshot [ "branch"; "list"; "-a" ] |> colored_string
4645 in
4746 (* From now on we use ignore-working-copy so we don't re-snapshot the state and so
4847 we can operate in paralell *)
4948 (* TODO: stop using dop last twice *)
5050- Show_view.reRender();
5151- let branches =
5252- Flock.fork_as_promise (fun _ ->
5353- jj_no_log ~snapshot:false [ "branch"; "list"; "-a" ] |> colored_string)
5454- and files_list = Flock.fork_as_promise (fun _ -> list_files ~rev ()) in
4949+ Show_view.reRender ();
5050+ let files_list = Flock.fork_as_promise (fun _ -> list_files ~rev ()) in
5551 (*wait for all our tasks*)
5656- let files_list = Promise.await files_list
5757- and branches = Promise.await branches in
5252+ let files_list = Promise.await files_list in
5853 (*now we can assign our results*)
5954 (* Vars.ui_state.jj_show $= log_res; *)
6055 Vars.ui_state.jj_branches $= branches;
6161- Vars.ui_state.jj_tree $= tree;
6256 Vars.ui_state.jj_change_files $= files_list)
6357;;
+44-13
jj_tui/bin/global_vars.ml
···2323 (* rev_id maybe_unique W.Overlay.filterable_selection_list_prompt_data option Lwd.var *)
2424 ; show_string_selection_prompt :
2525 string W.Overlay.filterable_selection_list_prompt_data option Lwd.var
2626- ; graph_revs : rev_id maybe_unique W.Lists.selectable_item array Lwd.var
2626+ ; graph_revs : rev_id maybe_unique W.Lists.multi_selectable_item array Lwd.var
2727 ; command_log : string list Lwd.var
2828- ; jj_tree : I.t Lwd.var
2928 ; jj_show : I.t Lwd.var
3030- ; jj_show_promise : (unit Promise.t) ref
2929+ ; jj_show_promise : unit Promise.t ref
3130 ; jj_branches : I.t Lwd.var
3231 ; jj_change_files : (string * string) list Lwd.var
3333- ; selected_revision : rev_id maybe_unique Lwd.var
3232+ ; hovered_revision : rev_id maybe_unique Lwd.var
3333+ ; selected_revisions : rev_id maybe_unique list Lwd.var
3434 ; revset : string option Lwd.var
3535 ; trigger_update : unit Lwd.var
3636}
···6363 val render_mutex : Eio.Mutex.t
64646565 (**returns either a change_id or if their are change_id conflicts, a commit_id *)
6666- val get_selected_rev : unit -> string
6666+ val get_hovered_rev : unit -> string
67676868 (**returns either a change_id or if their are change_id conflicts, a commit_id *)
6969- val get_selected_rev_lwd : unit -> string Lwd.t
6969+ val get_hovered_rev_lwd : unit -> string Lwd.t
7070+7171+ val get_selected_revs : unit -> string list
7272+ val get_selected_revs_lwd : unit -> string list Lwd.t
7373+ val get_active_revs : unit -> string list
7474+ val get_active_revs_lwd : unit -> string list Lwd.t
7075end
71767277module Vars : Vars = struct
···8388 let ui_state =
8489 {
8590 view = Lwd.var `Main
8686- ; jj_tree = Lwd.var I.empty
8791 ; jj_show = Lwd.var I.empty
8892 ; jj_show_promise = ref @@ Promise.of_value ()
8993 ; jj_branches = Lwd.var I.empty
9094 ; jj_change_files = Lwd.var []
9191- ; selected_revision = Lwd.var (Unique { change_id = "@"; commit_id = "@" })
9595+ ; hovered_revision = Lwd.var (Unique { change_id = "@"; commit_id = "@" })
9696+ ; selected_revisions = Lwd.var [ Unique { change_id = "@"; commit_id = "@" } ]
9297 ; revset = Lwd.var None
9398 ; graph_revs = Lwd.var [||]
9499 ; input = Lwd.var `Normal
···120125 let get_eio_vars () = Option.get !eio
121126 let get_term () = Option.get !term
122127123123- (**Gets an id for the selected revision. If the change_id is unique we use that, if it's not we return a commit_id instead*)
124124- let get_selected_rev () = Lwd.peek ui_state.selected_revision |> get_unique_id
128128+ (**Gets an id for the currently hovered revision. If the change_id is unique we use that, if it's not we return a commit_id instead*)
129129+ let get_hovered_rev () = Lwd.peek ui_state.hovered_revision |> get_unique_id
125130126126- (**see [get_selected_rev]*)
127127- let get_selected_rev_lwd () =
128128- let$ a = Lwd.get ui_state.selected_revision in
131131+ (**see [get_hovered_rev]*)
132132+ let get_hovered_rev_lwd () =
133133+ let$ a = Lwd.get ui_state.hovered_revision in
129134 a |> get_unique_id
135135+ ;;
136136+137137+ (**Gets all currently selected revisions*)
138138+ let get_selected_revs () =
139139+ Lwd.peek ui_state.selected_revisions |> List.map get_unique_id
140140+ ;;
141141+142142+ (**see [get_selected_revs]*)
143143+ let get_selected_revs_lwd () =
144144+ let$ a = Lwd.get ui_state.selected_revisions in
145145+ a |> List.map get_unique_id
146146+ ;;
147147+148148+ (**Gets selected revs, if nothing is selected gets the hovered rev*)
149149+ let get_active_revs () =
150150+ let selected = get_selected_revs () in
151151+ if selected |> List.length == 0 then [ get_hovered_rev () ] else selected
152152+ ;;
153153+154154+ (**See [get_active_revs]*)
155155+ let get_active_revs_lwd () =
156156+ let$ hovered = Lwd.get ui_state.hovered_revision
157157+ and$ selected = Lwd.get ui_state.selected_revisions in
158158+ if selected |> List.length == 0
159159+ then [ hovered |> get_unique_id ]
160160+ else selected |> List.map get_unique_id
130161 ;;
131162end
+18-9
jj_tui/bin/graph_view.ml
···7272 ; cmd =
7373 Fun
7474 (fun _ ->
7575- let rev = Vars.get_selected_rev () in
7575+ let rev = Vars.get_hovered_rev() in
7676 let source_msg, dest_msg = get_messages rev (rev ^ "-") in
7777 let new_msg =
7878 [ dest_msg; source_msg ] |> String.concat_non_empty "\n"
···311311 ("Select the branch to set to rev: " ^ rev)
312312 (fun branch ->
313313 Cmd
314314- [ "branch"; "set"; "-r"; get_selected_rev (); "-B"; branch ]))
314314+ [ "branch"; "set"; "-r"; get_hovered_rev (); "-B"; branch ]))
315315 }
316316 ; {
317317 key = 't'
···392392 |> Jj_tui.AnsiReverse.colored_string
393393 |> Ui.atom)
394394 in
395395- let data = W.Lists.{ ui; data = rev_ids.(!selectable_idx) } in
395395+ let id = rev_ids.(!selectable_idx) in
396396+ let data =
397397+ W.Lists.
398398+ {
399399+ ui
400400+ ; id = id |> Global_vars.get_unique_id |> String.hash
401401+ ; data = rev_ids.(!selectable_idx)
402402+ }
403403+ in
396404 (*Add to our selectable array*)
397405 Array.set selectable_items !selectable_idx data;
398406 selectable_idx := !selectable_idx + 1;
···416424 in
417425 let list_ui =
418426 items
419419- |> W.Lists.selection_list_exclusions
420420- ~on_selection_change:(fun revision ->
427427+ |> W.Lists.multi_selection_list_exclusions
428428+ ~on_selection_change:(fun ~hovered ~selected ->
421429 (*Respond to change in selected revision*)
422422- Lwd.set Vars.ui_state.selected_revision revision;
423423- Show_view.(pushStatus (Graph_preview (Vars.get_selected_rev ())));
424424- [%log debug "Selected revision: '%s'" (Global_vars.get_unique_id revision)];
430430+ Lwd.set Vars.ui_state.hovered_revision hovered;
431431+ Lwd.set Vars.ui_state.selected_revisions selected;
432432+ Show_view.(pushStatus (Graph_preview (Vars.get_hovered_rev ())));
433433+ [%log debug "Hovered revision: '%s'" (Global_vars.get_unique_id hovered)];
425434 Picos_std_structured.Flock.fork (fun () -> Global_funcs.update_views ()))
426426- ~custom_handler:(fun _ key -> handleKeys key)
435435+ ~custom_handler:(fun ~selected ~selectable_items key -> handleKeys key)
427436 in
428437 let final_ui =
429438 let$ list_ui = list_ui
+28-6
jj_tui/bin/jj_commands.ml
···88module Shared = struct
99 type cmd_args = string list [@@deriving show]
10101111- (** Regular jj command *)
1111+ type 'a revision_type =
1212+ | Hovered of 'a
1313+ | Selected of 'a
1414+ | Active of 'a (** Regular jj command *)
1515+ [@@deriving show]
1616+1217 type 'a command_variant =
1318 | Cmd of cmd_args (** Regular jj command *)
1419 | Cmd_r of cmd_args
1515- (** Regular jj command that should operate on the selected revison *)
2020+ (** Regular jj command that should operate on the hovered revison *)
2121+ | Cmd_ of cmd_args revision_type
2222+ (** Regular jj command that should operate on active revisions*)
1623 | Dynamic of (unit -> 'a command_variant)
1724 | Dynamic_r of (string -> 'a command_variant)
1825 (** Wraps a command so that the content will be regenerated each time it's run. Usefull if you wish to read some peice of ui state *)
···2128 | Prompt of string * cmd_args
2229 | Selection_prompt of
2330 string
2424- * (unit -> 'a Nottui.W.Lists.selectable_item list Lwd.t)
3131+ * (unit -> 'a Nottui.W.Lists.multi_selectable_item list Lwd.t)
2532 * (string -> 'a -> bool)
2633 * ('a -> 'a command_variant)
2734 | Prompt_r of string * cmd_args
···5663 open! Jj_tui.Util
57645865 exception Handled
6666+6767+ let get_revs rev_type =
6868+ match rev_type with
6969+ | Hovered a ->
7070+ a, [ get_hovered_rev () ]
7171+ | Selected a ->
7272+ a, get_selected_revs ()
7373+ | Active a ->
7474+ a, get_active_revs ()
7575+ ;;
59766077 let render_command_line ~indent_level key desc =
6178 let indent = String.init (indent_level * 2) (fun _ -> ' ') in
···170187 raise Handled
171188 | Cmd_r args ->
172189 ui_state.show_popup $= None;
173173- noOut (args @ [ "-r"; Vars.get_selected_rev () ]);
190190+ noOut (args @ [ "-r"; Vars.get_hovered_rev () ]);
191191+ raise Handled
192192+ | Cmd_ rev_type ->
193193+ let args, revs = get_revs rev_type in
194194+ ui_state.show_popup $= None;
195195+ noOut (args @ ("-r" :: revs));
174196 raise Handled
175197 | Prompt (str, args) ->
176198 ui_state.show_popup $= None;
···178200 raise Handled
179201 | Prompt_r (str, args) ->
180202 ui_state.show_popup $= None;
181181- prompt str (`Cmd (args @ [ "-r"; Vars.get_selected_rev () ]));
203203+ prompt str (`Cmd (args @ [ "-r"; Vars.get_hovered_rev() ]));
182204 raise Handled
183205 | PromptThen (label, next) ->
184206 ui_state.show_popup $= None;
···206228 | Dynamic f ->
207229 f () |> handleCommand description
208230 | Dynamic_r f ->
209209- f (Vars.get_selected_rev ()) |> handleCommand description
231231+ f (Vars.get_hovered_rev ()) |> handleCommand description
210232211233 (** Try mapching the command mapping to the provided key and run the command if it matches *)
212234 and command_input ~is_sub keymap key =
+1
jj_tui/bin/jj_widgets.ml
···3535 W.Lists.
3636 {
3737 data = name
3838+ ; id = name |> String.hash
3839 ; ui =
3940 str ^ "\n"
4041 |> Jj_tui.AnsiReverse.colored_string