terminal user interface to jujutsu. Focused on speed and clarity
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

multi-select

+477 -131
+6 -8
forks/nottui/lib/nottui/widgets/overlays.ml
··· 128 128 129 129 type 'a selection_list_prompt_data = 130 130 { label : string 131 - ; items : 'a Selection_list.selectable_item list Lwd.t 131 + ; items : 'a Selection_list.multi_selectable_item list Lwd.t 132 132 ; on_exit : [ `Closed | `Finished of 'a ] -> unit 133 133 } 134 134 ··· 177 177 178 178 type 'a filterable_selection_list_prompt_data = 179 179 { label : string 180 - ; items : 'a Selection_list.selectable_item list Lwd.t 181 - ;filter_predicate:(string-> 'a-> bool) 180 + ; items : 'a Selection_list.multi_selectable_item list Lwd.t 181 + ; filter_predicate : string -> 'a -> bool 182 182 ; on_exit : [ `Closed | `Finished of 'a ] -> unit 183 183 } 184 184 ··· 195 195 let$ show_prompt_val = Lwd.get show_prompt_var in 196 196 show_prompt_val 197 197 |> Option.map 198 - @@ fun { label; items;filter_predicate; on_exit } -> 198 + @@ fun { label; items; filter_predicate; on_exit } -> 199 199 let on_exit result = 200 200 Focus.release_reversable focus; 201 201 show_prompt_var $= None; ··· 206 206 Selection_list.filterable_selection_list 207 207 ~filter_predicate 208 208 ~focus 209 - ~on_confirm:(fun item -> 210 - (`Finished item) |> on_exit; 211 - ) 209 + ~on_confirm:(fun item -> `Finished item |> on_exit) 212 210 items 213 211 |> modify_body 214 212 in ··· 227 225 match show_popup with 228 226 | Some (content, label) -> 229 227 let prompt_field = content in 230 - prompt_field |>$ Ui.resize ~w:5 ~sw:1 |> BB.box ~label_top:label |> clear_bg 228 + prompt_field |>$ Ui.resize ~w:5 ~sw:1 |> BB.box ~label_top:label |> clear_bg 231 229 | None -> Ui.empty |> Lwd.pure 232 230 in 233 231 W.zbox [ ui; popup_ui |>$ Ui.resize ~crop:neutral_grav ~pad:neutral_grav ]
+6 -6
forks/nottui/lib/nottui/widgets/overlays.mli
··· 19 19 on_exit : [ `Closed | `Finished of string ] -> unit; 20 20 } 21 21 22 - (** Text box prompt that takes user input then calls [on_exit] with the result. 22 + (** Text box prompt that takes user input then calls [on_exit] with the result. 23 23 24 24 This will display ontop of any ui it is passed when show_prompt_var is [Some].*) 25 25 ··· 35 35 (** Config for a selection_list_prompt*) 36 36 type 'a selection_list_prompt_data = { 37 37 label : string; 38 - items : 'a Selection_list.selectable_item list Lwd.t; 38 + items : 'a Selection_list.multi_selectable_item list Lwd.t; 39 39 on_exit : [ `Closed | `Finished of 'a ] -> unit; 40 40 } 41 41 42 42 (** Selection_list prompt. 43 43 44 44 This will display ontop of any ui it is passed when show_prompt_var is [Some]. 45 - @param modify_body Function that takes the completed body of the prompt, incase you want to resize it or otherwise change it 45 + @param modify_body Function that takes the completed body of the prompt, incase you want to resize it or otherwise change it 46 46 *) 47 47 val selection_list_prompt : 48 48 ?pad_w:int -> ··· 55 55 56 56 type 'a filterable_selection_list_prompt_data = 57 57 { label : string 58 - ; items : 'a Selection_list.selectable_item list Lwd.t 58 + ; items : 'a Selection_list.multi_selectable_item list Lwd.t 59 59 ;filter_predicate:(string-> 'a-> bool) 60 60 ; on_exit : [ `Closed | `Finished of 'a ] -> unit 61 61 } 62 62 (** Selection_list prompt that is filterable. 63 63 64 64 This will display ontop of any ui it is passed when show_prompt_var is [Some]. 65 - @param modify_body Function that takes the completed body of the prompt, incase you want to resize it or otherwise change it 65 + @param modify_body Function that takes the completed body of the prompt, incase you want to resize it or otherwise change it 66 66 *) 67 67 val selection_list_prompt_filterable : 68 68 ?pad_w:int -> ··· 71 71 ?focus:Nottui_main.Focus.handle -> 72 72 show_prompt_var:'a filterable_selection_list_prompt_data option Lwd.var -> 73 73 Nottui_main.ui Lwd.t -> Nottui_main.ui Lwd.t 74 - 74 + 75 75 76 76 (**This is a simple popup that can show ontop of other ui elements *) 77 77 val popup :
+267 -32
forks/nottui/lib/nottui/widgets/selection_list.ml
··· 3 3 open Lwd_infix 4 4 open Shared 5 5 6 - type 'a selectable_item = 6 + let thrd (_, _, x) = x 7 + 8 + type 'a multi_selectable_item = 7 9 { data : 'a 8 - ; ui : bool -> Ui.t Lwd.t 10 + ; id : int 11 + ; ui : selected:bool -> hovered:bool -> Ui.t Lwd.t 9 12 } 10 13 11 - type 'a maybeSelectable = 12 - | Selectable of 'a selectable_item 14 + type 'a maybe_multi_selectable = 15 + | Selectable of 'a multi_selectable_item 13 16 | Filler of Ui.t Lwd.t 14 17 15 - let selection_list_exclusions 16 - ?(focus = Focus.make ()) 17 - ?(on_selection_change = fun _ -> ()) 18 - ~custom_handler 19 - (items : 'a maybeSelectable array Lwd.t) 20 - = 21 - (* 22 - The rough overview is: 23 - 1. Make a new list that only contains our selectable items 24 - 2. Render the items, making sure to tell the selected one to render as selected. 25 - 3. Calculate how much we should scroll by. 26 - 4. offset by the scroll amount, apply size sensors and output final ui 27 - *) 28 - let selected_var = Lwd.var 0 in 29 - let selected_position = Lwd.var (0, 0) in 18 + module MyMap = Map.Make (Int) 19 + 20 + (** Get a map of all the selectable items*) 21 + let get_selectable_items_map (items : 'a maybe_multi_selectable array Lwd.t) = 22 + let selectable_items = 23 + let$ items = items in 24 + (*Map of selectable items with their id as key*) 25 + items 26 + |> Array.fold_left 27 + (fun map item -> 28 + match item with 29 + | Selectable item -> MyMap.add item.id (item.id, item) map 30 + | Filler _ -> map) 31 + MyMap.empty 32 + in 33 + selectable_items 34 + ;; 35 + 36 + let get_selectable_items (items : 'a maybe_multi_selectable array Lwd.t) = 30 37 let selectable_items = 31 38 let$ items = items in 32 39 (*Array of selectable items and their idx in the original array*) ··· 48 55 in 49 56 Array.sub selectable_items 0 final_len 50 57 in 58 + selectable_items 59 + ;; 60 + 61 + let multi_selection_list_exclusions 62 + ?(focus = Focus.make ()) 63 + ?(on_selection_change = fun ~hovered ~selected -> ()) 64 + ~custom_handler 65 + (items : 'a maybe_multi_selectable array Lwd.t) 66 + = 67 + (* 68 + The rough overview is: 69 + 1. Make a new list that only contains our selectable items 70 + 2. Render the items, making sure to tell the selected one to render as selected. 71 + 3. Calculate how much we should scroll by. 72 + 4. offset by the scroll amount, apply size sensors and output final ui 73 + *) 74 + let selected_items_var = Lwd.var MyMap.empty in 75 + (*hovered var is a tuple of (id, overall_idx,selection_idx)*) 76 + (*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*) 77 + let hovered_var = ref (0, 0, 0) in 78 + let hover_changed = Lwd.var () in 79 + let selected_position = Lwd.var (0, 0) in 80 + let selectable_items_map = get_selectable_items_map items in 81 + let selectable_items = get_selectable_items items in 51 82 (*handle selections*) 52 83 let render_items = 53 84 let$* focus = focus |> Focus.status 54 - and$ items, selected, selectable_items = 85 + and$ items, selectable_items = 86 + (* This doesn't depend on changes in focus but it should update whenever there are new items or a selection change*) 87 + let$ items = items 88 + and$ selectable_items = selectable_items in 89 + (*We are only beeking this one because we only want this run when the items list changes*) 90 + let hovered_id, hovered_ovearll_idx, hovered_selection_idx = !hovered_var in 91 + (*TODO: This is obviously very slow*) 92 + selected_items_var 93 + $= (Lwd.peek selected_items_var 94 + |> MyMap.filter (fun selected value -> 95 + selectable_items |> Array.exists (fun (_, item) -> item.id = selected))); 96 + (*first we handle upading our selection if the list of items has changed*) 97 + (* We do this here to ensure that the selected var is updated before we render to avoid double rendering*) 98 + let hovered_id, hovered_ovearll_idx, hovered_selection_idx = 99 + (*If the id is not in the list anymore then we should just pick the closest item by index*) 100 + (* We have a few progressively less ideal states here: 101 + 1. We found our exact same id item in a new place 102 + 2. We found an item that's in the same location as the previous one 103 + 3. We just return something 104 + *) 105 + selectable_items 106 + |> Array.fold_left 107 + (fun (count, acc) (idx, item) -> 108 + let nCount = count + 1 in 109 + match acc with 110 + | `Found _ -> nCount, acc 111 + | `Same_idx _ -> 112 + if item.id = hovered_id 113 + then nCount, `Found (hovered_id, idx, count) 114 + else nCount, acc 115 + | `Searching _ -> 116 + if item.id = hovered_id 117 + then nCount, `Found (hovered_id, idx, count) 118 + else if count == hovered_selection_idx 119 + then nCount, `Same_idx (hovered_id, idx, count) 120 + else nCount, `Searching (hovered_id, idx, count)) 121 + (0, `Searching (0, 0, 0)) 122 + |> snd 123 + |> function 124 + | `Found (id, idx, count) -> id, idx, count 125 + | `Same_idx (id, idx, count) -> id, idx, count 126 + | `Searching (id, idx, count) -> id, idx, count 127 + in 128 + hovered_var := hovered_id, hovered_ovearll_idx, hovered_selection_idx; 129 + if Array.length selectable_items > 0 130 + then ( 131 + let item_idx, item = selectable_items.(hovered_selection_idx) in 132 + on_selection_change 133 + ~hovered:item.data 134 + ~selected: 135 + (Lwd.peek selected_items_var |> MyMap.to_list |> List.map (fun (_,a) -> a)); 136 + items, selectable_items) 137 + else items, selectable_items 138 + and$ _ = Lwd.get hover_changed 139 + and$ selected_items = Lwd.get selected_items_var in 140 + let _, _, hovered = !hovered_var in 141 + (*==== Rendering The list ====*) 142 + (* Ui.vcat can be a little weird when the *) 143 + if items |> Array.length = 0 144 + then Ui.empty |> Lwd.pure 145 + else 146 + items 147 + |> Array.mapi (fun i x -> 148 + match x with 149 + | Filler ui -> ui 150 + | Selectable x -> 151 + let hovered = hovered == i in 152 + let selected = selected_items |> MyMap.mem x.id in 153 + if hovered 154 + then 155 + x.ui ~hovered ~selected 156 + |>$ Ui.transient_sensor (fun ~x ~y ~w:_ ~h:_ () -> 157 + if (x, y) <> Lwd.peek selected_position then selected_position $= (x, y)) 158 + else x.ui ~hovered ~selected) 159 + |> Array.to_list 160 + |> Shared.vbox 161 + |>$ Ui.keyboard_area ~focus (function 162 + | `Arrow `Up, [] -> 163 + let hovered_idx = max ((!hovered_var |> thrd) - 1) 0 in 164 + let hovered = (selectable_items.(hovered_idx) |> snd).id, 0, hovered_idx in 165 + hovered_var := hovered; 166 + Lwd.set hover_changed (); 167 + on_selection_change 168 + ~hovered:(selectable_items.(hovered_idx) |> snd).data 169 + ~selected: 170 + (Lwd.peek selected_items_var |> MyMap.to_list |> List.map (fun (_,a) -> a)); 171 + `Handled 172 + | `Arrow `Down, [] -> 173 + let hovered_idx = 174 + Int.max 175 + (min ((!hovered_var |> thrd) + 1) ((selectable_items |> Array.length) - 1)) 176 + 0 177 + in 178 + let hovered = (selectable_items.(hovered_idx) |> snd).id, 0, hovered_idx in 179 + hovered_var := hovered; 180 + Lwd.set hover_changed (); 181 + on_selection_change 182 + ~hovered:(selectable_items.(hovered_idx) |> snd).data 183 + ~selected: 184 + (Lwd.peek selected_items_var |> MyMap.to_list |> List.map (fun (_,a) -> a)); 185 + `Handled 186 + | `ASCII ' ', [] -> 187 + let hovered_id, _, hovered_idx = !hovered_var in 188 + let data= 189 + (selectable_items.(hovered_idx) |> snd).data in let selected = Lwd.peek selected_items_var in 190 + if selected |> MyMap.mem hovered_id 191 + then Lwd.set selected_items_var (MyMap.remove hovered_id selected) 192 + else Lwd.set selected_items_var (MyMap.add hovered_id data selected); 193 + (* TODO: make sure this actually apllies, there is some chance the peek will no update*) 194 + on_selection_change 195 + ~hovered:data 196 + ~selected: 197 + (Lwd.peek selected_items_var |> MyMap.to_list |> List.map (fun (_,a)-> a)); 198 + `Handled 199 + | a -> custom_handler ~selected:(Lwd.peek selected_items_var) ~selectable_items a) 200 + in 201 + let rendered_size_var = Lwd.var (0, 0) in 202 + (*Handle scrolling*) 203 + let scrollitems = 204 + let size_var = Lwd.var (0, 0) in 205 + let shift_amount = 206 + (*get the actual idx not just the selection number*) 207 + let$ selected_idx = 208 + Lwd.map2 (Lwd.get hover_changed) selectable_items ~f:(fun () selectable -> 209 + let _, _, hovered_idx = !hovered_var in 210 + if Array.length selectable > hovered_idx 211 + then selectable.(hovered_idx) |> fst 212 + else 0) 213 + and$ size = Lwd.get size_var 214 + and$ length = items |>$ Array.length 215 + and$ ren_size = Lwd.get rendered_size_var in 216 + (*portion of the total size of the element that is rendered*) 217 + let size_ratio = 218 + (ren_size |> snd |> float_of_int) /. (size |> snd |> float_of_int) 219 + in 220 + (*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 )*) 221 + let offset = size_ratio *. ((length |> float_of_int) /. 2.9) in 222 + (*portion of the list that is behind the selection*) 223 + let list_ratio = 224 + ((selected_idx |> float_of_int) +. offset) /. (length |> float_of_int) 225 + in 226 + (*if our position is further down the list than the portion that is shown we will shift by that amoumt *) 227 + Float.max (list_ratio -. size_ratio) 0.0 *. (size |> snd |> float_of_int) 228 + |> int_of_float 229 + in 230 + let$ items = render_items 231 + and$ shift_amount = shift_amount in 232 + items 233 + |> Ui.shift_area 0 shift_amount 234 + |> Ui.resize ~sh:1 235 + |> simpleSizeSensor ~size_var 236 + |> Ui.resize ~w:3 ~sw:1 ~h:0 237 + |> simpleSizeSensor ~size_var:rendered_size_var 238 + in 239 + scrollitems 240 + ;; 241 + 242 + let selection_list_exclusions 243 + ?(focus = Focus.make ()) 244 + ?(on_selection_change = fun _ -> ()) 245 + ~custom_handler 246 + (items : 'a maybe_multi_selectable array Lwd.t) 247 + = 248 + (* 249 + The rough overview is: 250 + 1. Make a new list that only contains our selectable items 251 + 2. Render the items, making sure to tell the selected one to render as selected. 252 + 3. Calculate how much we should scroll by. 253 + 4. offset by the scroll amount, apply size sensors and output final ui 254 + *) 255 + let selected_var = Lwd.var 0 in 256 + let selected_position = Lwd.var (0, 0) in 257 + let selectable_items = get_selectable_items items in 258 + (*handle selections*) 259 + let render_items = 260 + let$* focus = focus |> Focus.status 261 + and$ items, hovered, selectable_items = 55 262 (* This doesn't depend on changes in focus but it should update whenever there are new items or a selection change*) 56 263 let$ items = items 57 264 and$ selectable_items = selectable_items ··· 83 290 match x with 84 291 | Filler ui -> ui 85 292 | Selectable x -> 86 - if selected == i 293 + if hovered == i 87 294 then 88 - x.ui true 295 + x.ui ~hovered:true ~selected:false 89 296 |>$ Ui.transient_sensor (fun ~x ~y ~w:_ ~h:_ () -> 90 297 if (x, y) <> Lwd.peek selected_position then selected_position $= (x, y)) 91 - else x.ui false) 298 + else x.ui ~hovered:false ~selected:false) 92 299 |> Array.to_list 93 300 |> Shared.vbox 94 301 |>$ Ui.keyboard_area ~focus (function ··· 144 351 scrollitems 145 352 ;; 146 353 147 - let selectable_item ui is_focused = 354 + let selectable_item ui ~selected ~hovered = 148 355 let height = Ui.layout_height ui in 149 356 let prefix = 150 - if is_focused then I.char A.(bg blue) '>' 1 height else I.char A.empty ' ' 1 height 357 + if selected && hovered (* (Uchar.of_int 0x2265) *) 358 + then I.uchar A.(bg A.cyan ++ fg black ++ st bold) (Uchar.of_char 'x') 1 height 359 + else if selected 360 + then I.uchar A.(bg A.cyan ++ fg black ++ st bold) (Uchar.of_char 'o') 1 height 361 + else if hovered 362 + then I.uchar A.(fg A.cyan ++ st bold) (Uchar.of_int 0x25b6) 1 height 363 + else I.char A.empty ' ' 1 height 151 364 in 152 365 Ui.hcat [ prefix |> Ui.atom; ui ] |> Lwd.pure 153 366 ;; 154 367 155 - let selectable_item_lwd ui is_focused = 368 + let selectable_item_lwd ui ~selected ~hovered = 156 369 let$ ui = ui in 157 370 let height = Ui.layout_height ui in 158 371 let prefix = 159 - if is_focused then I.char A.(bg blue) '>' 1 height else I.char A.empty ' ' 1 height 372 + if selected && hovered 373 + then I.uchar A.(bg blue) (Uchar.of_int 0x2265) 1 height 374 + else if selected 375 + then I.char A.(bg blue) '=' 1 height 376 + else if hovered 377 + then I.char A.(bg blue) '>' 1 height 378 + else I.char A.empty ' ' 1 height 160 379 in 161 380 Ui.hcat [ prefix |> Ui.atom; ui ] 162 381 ;; 163 382 383 + let multi_selection_list_custom 384 + ?(focus = Focus.make ()) 385 + ?(on_selection_change = fun ~hovered ~selected -> ()) 386 + ~custom_handler 387 + (items : 'a multi_selectable_item list Lwd.t) 388 + = 389 + multi_selection_list_exclusions 390 + ~focus 391 + ~on_selection_change 392 + ~custom_handler 393 + (items 394 + |>$ fun items -> 395 + let selectable_items = Array.make (List.length items) (Obj.magic ()) in 396 + items |> List.iteri (fun i x -> Array.set selectable_items i (Selectable x)); 397 + selectable_items) 398 + 164 399 let selection_list_custom 165 400 ?(focus = Focus.make ()) 166 401 ?(on_selection_change = fun _ -> ()) 167 402 ~custom_handler 168 - (items : 'a selectable_item list Lwd.t) 403 + (items : 'a multi_selectable_item list Lwd.t) 169 404 = 170 405 selection_list_exclusions 171 406 ~focus ··· 183 418 ~(filter_predicate : string -> 'a -> bool) 184 419 ~custom_handler 185 420 ~filter_text_var 186 - (items : 'a selectable_item list Lwd.t) 421 + (items : 'a multi_selectable_item list Lwd.t) 187 422 = 188 423 (*filter the list whenever the input changes*) 189 424 let items = ··· 204 439 ;; 205 440 206 441 let filterable_selection_list 207 - ?(pad_w=1) 208 - ?(pad_h=0) 442 + ?(pad_w = 1) 443 + ?(pad_h = 0) 209 444 ?(focus = Focus.make ()) 210 445 ~filter_predicate 211 446 ?(on_esc = fun _ -> ()) ··· 241 476 vbox 242 477 [ filter_text_ui |> Border_box.box ~pad_w ~pad_h 243 478 ; (list_ui 244 - |> Border_box.box ~pad_w ~pad_h 479 + |> Border_box.box ~pad_w ~pad_h 245 480 |>$ fun x -> 246 481 let mw = (x |> Ui.layout_spec).mw in 247 482 if mw > Lwd.peek max_width then max_width $= mw;
+52 -14
forks/nottui/lib/nottui/widgets/selection_list.mli
··· 1 + open Nottui_main 2 + module MyMap : Map.S with type key = int 3 + 1 4 (**Selectable list item with a ui and some data *) 2 - type 'a selectable_item = 5 + type 'a multi_selectable_item = 3 6 { data : 'a 4 7 (**info attached to each ui elment in the list, used for filtering and on_select callback *) 5 - ; ui : bool -> Nottui_main.ui Lwd.t 8 + ; id : int 9 + ; ui : selected:bool -> hovered:bool -> Ui.t Lwd.t 6 10 } 7 11 8 - type 'a maybeSelectable = 9 - | Selectable of 'a selectable_item 10 - | Filler of Nottui_main.ui Lwd.t 12 + type 'a maybe_multi_selectable = 13 + | Selectable of 'a multi_selectable_item 14 + | Filler of Ui.t Lwd.t 15 + 16 + (** multi_selectable exclusions *) 17 + val multi_selection_list_exclusions 18 + : ?focus:Nottui_main.Focus.handle 19 + -> ?on_selection_change:(hovered:'a -> selected:'a list -> unit) 20 + -> custom_handler: 21 + (selected:'a MyMap.t 22 + -> selectable_items:(int * 'a multi_selectable_item) array 23 + -> Nottui_main.Ui.key 24 + -> Nottui_main.Ui.may_handle) 25 + -> 'a maybe_multi_selectable array Lwd.t 26 + -> Nottui_main.ui Lwd.t 11 27 12 28 (** Same as [selection_list_custom] except that it supports not all element in the list being selectable *) 13 29 val selection_list_exclusions 14 30 : ?focus:Nottui_main.Focus.handle 15 31 -> ?on_selection_change:('a -> unit) 16 32 -> custom_handler: 17 - ('a selectable_item -> Nottui_main.Ui.key -> Nottui_main.Ui.may_handle) 18 - -> 'a maybeSelectable array Lwd.t 33 + ('a multi_selectable_item -> Nottui_main.Ui.key -> Nottui_main.Ui.may_handle) 34 + -> 'a maybe_multi_selectable array Lwd.t 19 35 -> Nottui_main.ui Lwd.t 20 36 21 37 (**Makes a ui element selectable. ··· 23 39 Takes [ui] and returns a function that appends '>' to the start when given [true] and ' ' when false 24 40 25 41 Used in conjuction with [selection_list_custom]*) 26 - val selectable_item : Nottui_main.ui -> bool -> Nottui_main.ui Lwd.t 42 + val selectable_item 43 + : Nottui_main.ui 44 + -> selected:bool 45 + -> hovered:bool 46 + -> Nottui_main.ui Lwd.t 27 47 28 - val selectable_item_lwd : Nottui_main.ui Lwd.t -> bool -> Nottui_main.ui Lwd.t 48 + val selectable_item_lwd 49 + : Nottui_main.ui Lwd.t 50 + -> selected:bool 51 + -> hovered:bool 52 + -> Nottui_main.ui Lwd.t 53 + 54 + (** multi selection list that allows for custom handling of keyboard events. 55 + Scrolls when the selection reaches the lower third 56 + Only handles up and down keyboard events. Use [~custom_handler] to do handle confirming your selection and such *) 57 + val multi_selection_list_custom 58 + : ?focus:Nottui_main.Focus.handle 59 + -> ?on_selection_change:(hovered:'a -> selected:'a list -> unit) 60 + -> custom_handler: 61 + (selected:'a MyMap.t 62 + -> selectable_items:(int * 'a multi_selectable_item) array 63 + -> Nottui_main.Ui.key 64 + -> Nottui_main.Ui.may_handle) 65 + -> 'a multi_selectable_item list Lwd.t 66 + -> Nottui_main.ui Lwd.t 29 67 30 68 (** Selection list that allows for custom handling of keyboard events. 31 69 Scrolls when the selection reaches the lower third ··· 34 72 : ?focus:Nottui_main.Focus.handle 35 73 -> ?on_selection_change:('a -> unit) 36 74 -> custom_handler: 37 - ('a selectable_item -> Nottui_main.Ui.key -> Nottui_main.Ui.may_handle) 38 - -> 'a selectable_item list Lwd.t 75 + ('a multi_selectable_item -> Nottui_main.Ui.key -> Nottui_main.Ui.may_handle) 76 + -> 'a multi_selectable_item list Lwd.t 39 77 -> Nottui_main.ui Lwd.t 40 78 41 79 (** A filterable selectable list. ··· 47 85 : ?focus:Nottui_main.Focus.handle 48 86 -> filter_predicate:(string -> 'a -> bool) 49 87 -> custom_handler: 50 - ('a selectable_item -> Nottui_main.Ui.key -> Nottui_main.Ui.may_handle) 88 + ('a multi_selectable_item -> Nottui_main.Ui.key -> Nottui_main.Ui.may_handle) 51 89 -> filter_text_var:string Lwd.var 52 - -> 'a selectable_item list Lwd.t 90 + -> 'a multi_selectable_item list Lwd.t 53 91 -> Nottui_main.ui Lwd.t 54 92 55 93 (** Filterable selection list ··· 67 105 -> filter_predicate:(string -> 'a -> bool) 68 106 -> ?on_esc:('a -> unit) 69 107 -> on_confirm:('a -> unit) 70 - -> 'a selectable_item list Lwd.t 108 + -> 'a multi_selectable_item list Lwd.t 71 109 -> Nottui_main.ui Lwd.t
+35 -21
jj_tui/bin/file_view.ml
··· 10 10 open Jj_tui 11 11 open Picos_std_structured 12 12 13 - let selected_file = Lwd.var "" 13 + let active_files= Lwd.var [""] 14 14 15 15 let rec command_mapping = 16 16 [ ··· 32 32 ( "Revision to move file to" 33 33 , fun rev -> 34 34 Cmd 35 - [ 35 + ([ 36 36 "squash" 37 37 ; "-u" 38 38 ; "--keep-emptied" 39 39 ; "--from" 40 - ; get_selected_rev () 40 + ; get_hovered_rev () 41 41 ; "--into" 42 42 ; rev 43 - ; Lwd.peek selected_file 44 - ] ) 43 + ] 44 + @ 45 + (Lwd.peek active_files)) 46 + ) 45 47 } 46 48 ; { 47 49 key = 'N' ··· 49 51 ; cmd = 50 52 Dynamic_r 51 53 (fun rev -> 52 - Cmd 54 + Cmd ( 53 55 [ 54 56 "squash" 55 57 ; "-u" ··· 58 60 ; rev 59 61 ; "--into" 60 62 ; rev ^ "+" 61 - ; Lwd.peek selected_file 62 - ]) 63 + ]@ 64 + Lwd.peek active_files 65 + ) 66 + ) 63 67 } 64 68 ; { 65 69 key = 'P' ··· 67 71 ; cmd = 68 72 Dynamic_r 69 73 (fun rev -> 70 - Cmd 74 + Cmd( 71 75 [ 72 76 "squash" 73 77 ; "-u" ··· 76 80 ; rev 77 81 ; "--into" 78 82 ; rev ^ "-" 79 - ; Lwd.peek selected_file 80 - ]) 83 + ]@ 84 + Lwd.peek active_files 85 + ) 86 + ) 81 87 } 82 88 ; { 83 89 key = 'd' ··· 85 91 ; cmd = 86 92 Dynamic_r 87 93 (fun rev -> 88 - let selected = Lwd.peek selected_file in 94 + let selected = Lwd.peek active_files in 89 95 confirm_prompt 90 - ("discard all changes to '" ^ selected ^ "' in rev " ^ rev) 91 - (Cmd [ "restore"; "--to"; rev; "--from"; rev ^ "-"; selected ])) 96 + ("discard all changes to '" ^ (selected|>String.concat "\n") ^ "' in rev " ^ rev) 97 + (Cmd (["restore"; "--to"; rev; "--from"; rev ^ "-"] @selected))) 92 98 } 93 99 ] 94 100 ;; ··· 98 104 let$ files = Lwd.get Vars.ui_state.jj_change_files in 99 105 files 100 106 |> List.map (fun (_modifier, file) -> 101 - W.Lists.{ data = file; ui = W.Lists.selectable_item (W.string file) }) 107 + W.Lists. 108 + { 109 + data = file 110 + ; id = file |> String.hash 111 + ; ui = W.Lists.selectable_item (W.string file) 112 + }) 102 113 in 103 114 (*TODO: 104 115 This should be redesigned completely ··· 106 117 It will have a cancellation system just like this one. 107 118 when any of the dependencies change, selected file, selected rev, focus etc, it will re-render if needed and cancel the current rendering. 108 119 *) 109 - W.Lists.selection_list_custom 110 - ~on_selection_change:(fun selected -> 111 - Lwd.set selected_file selected; 120 + file_uis|> 121 + W.Lists.multi_selection_list_custom 122 + ~on_selection_change:(fun ~hovered ~selected -> 123 + let active= 124 + if selected|>List.length =0 then [hovered] else selected 125 + in 126 + Lwd.set active_files active; 112 127 if Focus.peek_has_focus focus 113 - then Show_view.(pushStatus (File_preview (Vars.get_selected_rev (),selected)))) 114 - ~custom_handler:(fun _ key -> 128 + then Show_view.(pushStatus (File_preview (Vars.get_hovered_rev (), hovered)))) 129 + ~custom_handler:(fun ~selected:_ ~selectable_items:_ key -> 115 130 match key with `ASCII k, [] -> handleInputs command_mapping k | _ -> `Unhandled) 116 - file_uis 117 131 ;; 118 132 end
+7 -13
jj_tui/bin/global_funcs.ml
··· 29 29 This should be called after any command that performs a change *) 30 30 let update_status ?(update_graph = true) ?(cause_snapshot = false) () = 31 31 safe_jj (fun () -> 32 - let rev = Lwd.peek Vars.ui_state.selected_revision in 32 + let rev = Lwd.peek Vars.ui_state.hovered_revision in 33 33 let log_res = jj_no_log ~snapshot:cause_snapshot [ "log" ] |> colored_string in 34 34 (* TODO: chagne this because it makes us always a frame behind *) 35 35 if update_graph then Vars.ui_state.trigger_update $= ()) ··· 39 39 This should be called after any command that performs a change *) 40 40 let update_views ?(cause_snapshot = false) () = 41 41 safe_jj (fun () -> 42 - let rev = Vars.get_selected_rev () in 43 - Flock.join_after @@ fun () -> 44 - let tree = 45 - jj_no_log ~snapshot:cause_snapshot [ "log"; "-r"; rev ] |> colored_string 42 + let rev = Vars.get_hovered_rev () in 43 + let branches = 44 + jj_no_log ~snapshot:cause_snapshot [ "branch"; "list"; "-a" ] |> colored_string 46 45 in 47 46 (* From now on we use ignore-working-copy so we don't re-snapshot the state and so 48 47 we can operate in paralell *) 49 48 (* TODO: stop using dop last twice *) 50 - Show_view.reRender(); 51 - let branches = 52 - Flock.fork_as_promise (fun _ -> 53 - jj_no_log ~snapshot:false [ "branch"; "list"; "-a" ] |> colored_string) 54 - and files_list = Flock.fork_as_promise (fun _ -> list_files ~rev ()) in 49 + Show_view.reRender (); 50 + let files_list = Flock.fork_as_promise (fun _ -> list_files ~rev ()) in 55 51 (*wait for all our tasks*) 56 - let files_list = Promise.await files_list 57 - and branches = Promise.await branches in 52 + let files_list = Promise.await files_list in 58 53 (*now we can assign our results*) 59 54 (* Vars.ui_state.jj_show $= log_res; *) 60 55 Vars.ui_state.jj_branches $= branches; 61 - Vars.ui_state.jj_tree $= tree; 62 56 Vars.ui_state.jj_change_files $= files_list) 63 57 ;;
+44 -13
jj_tui/bin/global_vars.ml
··· 23 23 (* rev_id maybe_unique W.Overlay.filterable_selection_list_prompt_data option Lwd.var *) 24 24 ; show_string_selection_prompt : 25 25 string W.Overlay.filterable_selection_list_prompt_data option Lwd.var 26 - ; graph_revs : rev_id maybe_unique W.Lists.selectable_item array Lwd.var 26 + ; graph_revs : rev_id maybe_unique W.Lists.multi_selectable_item array Lwd.var 27 27 ; command_log : string list Lwd.var 28 - ; jj_tree : I.t Lwd.var 29 28 ; jj_show : I.t Lwd.var 30 - ; jj_show_promise : (unit Promise.t) ref 29 + ; jj_show_promise : unit Promise.t ref 31 30 ; jj_branches : I.t Lwd.var 32 31 ; jj_change_files : (string * string) list Lwd.var 33 - ; selected_revision : rev_id maybe_unique Lwd.var 32 + ; hovered_revision : rev_id maybe_unique Lwd.var 33 + ; selected_revisions : rev_id maybe_unique list Lwd.var 34 34 ; revset : string option Lwd.var 35 35 ; trigger_update : unit Lwd.var 36 36 } ··· 63 63 val render_mutex : Eio.Mutex.t 64 64 65 65 (**returns either a change_id or if their are change_id conflicts, a commit_id *) 66 - val get_selected_rev : unit -> string 66 + val get_hovered_rev : unit -> string 67 67 68 68 (**returns either a change_id or if their are change_id conflicts, a commit_id *) 69 - val get_selected_rev_lwd : unit -> string Lwd.t 69 + val get_hovered_rev_lwd : unit -> string Lwd.t 70 + 71 + val get_selected_revs : unit -> string list 72 + val get_selected_revs_lwd : unit -> string list Lwd.t 73 + val get_active_revs : unit -> string list 74 + val get_active_revs_lwd : unit -> string list Lwd.t 70 75 end 71 76 72 77 module Vars : Vars = struct ··· 83 88 let ui_state = 84 89 { 85 90 view = Lwd.var `Main 86 - ; jj_tree = Lwd.var I.empty 87 91 ; jj_show = Lwd.var I.empty 88 92 ; jj_show_promise = ref @@ Promise.of_value () 89 93 ; jj_branches = Lwd.var I.empty 90 94 ; jj_change_files = Lwd.var [] 91 - ; selected_revision = Lwd.var (Unique { change_id = "@"; commit_id = "@" }) 95 + ; hovered_revision = Lwd.var (Unique { change_id = "@"; commit_id = "@" }) 96 + ; selected_revisions = Lwd.var [ Unique { change_id = "@"; commit_id = "@" } ] 92 97 ; revset = Lwd.var None 93 98 ; graph_revs = Lwd.var [||] 94 99 ; input = Lwd.var `Normal ··· 120 125 let get_eio_vars () = Option.get !eio 121 126 let get_term () = Option.get !term 122 127 123 - (**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*) 124 - let get_selected_rev () = Lwd.peek ui_state.selected_revision |> get_unique_id 128 + (**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*) 129 + let get_hovered_rev () = Lwd.peek ui_state.hovered_revision |> get_unique_id 125 130 126 - (**see [get_selected_rev]*) 127 - let get_selected_rev_lwd () = 128 - let$ a = Lwd.get ui_state.selected_revision in 131 + (**see [get_hovered_rev]*) 132 + let get_hovered_rev_lwd () = 133 + let$ a = Lwd.get ui_state.hovered_revision in 129 134 a |> get_unique_id 135 + ;; 136 + 137 + (**Gets all currently selected revisions*) 138 + let get_selected_revs () = 139 + Lwd.peek ui_state.selected_revisions |> List.map get_unique_id 140 + ;; 141 + 142 + (**see [get_selected_revs]*) 143 + let get_selected_revs_lwd () = 144 + let$ a = Lwd.get ui_state.selected_revisions in 145 + a |> List.map get_unique_id 146 + ;; 147 + 148 + (**Gets selected revs, if nothing is selected gets the hovered rev*) 149 + let get_active_revs () = 150 + let selected = get_selected_revs () in 151 + if selected |> List.length == 0 then [ get_hovered_rev () ] else selected 152 + ;; 153 + 154 + (**See [get_active_revs]*) 155 + let get_active_revs_lwd () = 156 + let$ hovered = Lwd.get ui_state.hovered_revision 157 + and$ selected = Lwd.get ui_state.selected_revisions in 158 + if selected |> List.length == 0 159 + then [ hovered |> get_unique_id ] 160 + else selected |> List.map get_unique_id 130 161 ;; 131 162 end
+18 -9
jj_tui/bin/graph_view.ml
··· 72 72 ; cmd = 73 73 Fun 74 74 (fun _ -> 75 - let rev = Vars.get_selected_rev () in 75 + let rev = Vars.get_hovered_rev() in 76 76 let source_msg, dest_msg = get_messages rev (rev ^ "-") in 77 77 let new_msg = 78 78 [ dest_msg; source_msg ] |> String.concat_non_empty "\n" ··· 311 311 ("Select the branch to set to rev: " ^ rev) 312 312 (fun branch -> 313 313 Cmd 314 - [ "branch"; "set"; "-r"; get_selected_rev (); "-B"; branch ])) 314 + [ "branch"; "set"; "-r"; get_hovered_rev (); "-B"; branch ])) 315 315 } 316 316 ; { 317 317 key = 't' ··· 392 392 |> Jj_tui.AnsiReverse.colored_string 393 393 |> Ui.atom) 394 394 in 395 - let data = W.Lists.{ ui; data = rev_ids.(!selectable_idx) } in 395 + let id = rev_ids.(!selectable_idx) in 396 + let data = 397 + W.Lists. 398 + { 399 + ui 400 + ; id = id |> Global_vars.get_unique_id |> String.hash 401 + ; data = rev_ids.(!selectable_idx) 402 + } 403 + in 396 404 (*Add to our selectable array*) 397 405 Array.set selectable_items !selectable_idx data; 398 406 selectable_idx := !selectable_idx + 1; ··· 416 424 in 417 425 let list_ui = 418 426 items 419 - |> W.Lists.selection_list_exclusions 420 - ~on_selection_change:(fun revision -> 427 + |> W.Lists.multi_selection_list_exclusions 428 + ~on_selection_change:(fun ~hovered ~selected -> 421 429 (*Respond to change in selected revision*) 422 - Lwd.set Vars.ui_state.selected_revision revision; 423 - Show_view.(pushStatus (Graph_preview (Vars.get_selected_rev ()))); 424 - [%log debug "Selected revision: '%s'" (Global_vars.get_unique_id revision)]; 430 + Lwd.set Vars.ui_state.hovered_revision hovered; 431 + Lwd.set Vars.ui_state.selected_revisions selected; 432 + Show_view.(pushStatus (Graph_preview (Vars.get_hovered_rev ()))); 433 + [%log debug "Hovered revision: '%s'" (Global_vars.get_unique_id hovered)]; 425 434 Picos_std_structured.Flock.fork (fun () -> Global_funcs.update_views ())) 426 - ~custom_handler:(fun _ key -> handleKeys key) 435 + ~custom_handler:(fun ~selected ~selectable_items key -> handleKeys key) 427 436 in 428 437 let final_ui = 429 438 let$ list_ui = list_ui
+28 -6
jj_tui/bin/jj_commands.ml
··· 8 8 module Shared = struct 9 9 type cmd_args = string list [@@deriving show] 10 10 11 - (** Regular jj command *) 11 + type 'a revision_type = 12 + | Hovered of 'a 13 + | Selected of 'a 14 + | Active of 'a (** Regular jj command *) 15 + [@@deriving show] 16 + 12 17 type 'a command_variant = 13 18 | Cmd of cmd_args (** Regular jj command *) 14 19 | Cmd_r of cmd_args 15 - (** Regular jj command that should operate on the selected revison *) 20 + (** Regular jj command that should operate on the hovered revison *) 21 + | Cmd_ of cmd_args revision_type 22 + (** Regular jj command that should operate on active revisions*) 16 23 | Dynamic of (unit -> 'a command_variant) 17 24 | Dynamic_r of (string -> 'a command_variant) 18 25 (** 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 *) ··· 21 28 | Prompt of string * cmd_args 22 29 | Selection_prompt of 23 30 string 24 - * (unit -> 'a Nottui.W.Lists.selectable_item list Lwd.t) 31 + * (unit -> 'a Nottui.W.Lists.multi_selectable_item list Lwd.t) 25 32 * (string -> 'a -> bool) 26 33 * ('a -> 'a command_variant) 27 34 | Prompt_r of string * cmd_args ··· 56 63 open! Jj_tui.Util 57 64 58 65 exception Handled 66 + 67 + let get_revs rev_type = 68 + match rev_type with 69 + | Hovered a -> 70 + a, [ get_hovered_rev () ] 71 + | Selected a -> 72 + a, get_selected_revs () 73 + | Active a -> 74 + a, get_active_revs () 75 + ;; 59 76 60 77 let render_command_line ~indent_level key desc = 61 78 let indent = String.init (indent_level * 2) (fun _ -> ' ') in ··· 170 187 raise Handled 171 188 | Cmd_r args -> 172 189 ui_state.show_popup $= None; 173 - noOut (args @ [ "-r"; Vars.get_selected_rev () ]); 190 + noOut (args @ [ "-r"; Vars.get_hovered_rev () ]); 191 + raise Handled 192 + | Cmd_ rev_type -> 193 + let args, revs = get_revs rev_type in 194 + ui_state.show_popup $= None; 195 + noOut (args @ ("-r" :: revs)); 174 196 raise Handled 175 197 | Prompt (str, args) -> 176 198 ui_state.show_popup $= None; ··· 178 200 raise Handled 179 201 | Prompt_r (str, args) -> 180 202 ui_state.show_popup $= None; 181 - prompt str (`Cmd (args @ [ "-r"; Vars.get_selected_rev () ])); 203 + prompt str (`Cmd (args @ [ "-r"; Vars.get_hovered_rev() ])); 182 204 raise Handled 183 205 | PromptThen (label, next) -> 184 206 ui_state.show_popup $= None; ··· 206 228 | Dynamic f -> 207 229 f () |> handleCommand description 208 230 | Dynamic_r f -> 209 - f (Vars.get_selected_rev ()) |> handleCommand description 231 + f (Vars.get_hovered_rev ()) |> handleCommand description 210 232 211 233 (** Try mapching the command mapping to the provided key and run the command if it matches *) 212 234 and command_input ~is_sub keymap key =
+1
jj_tui/bin/jj_widgets.ml
··· 35 35 W.Lists. 36 36 { 37 37 data = name 38 + ; id = name |> String.hash 38 39 ; ui = 39 40 str ^ "\n" 40 41 |> Jj_tui.AnsiReverse.colored_string
+1 -1
jj_tui/bin/main.ml
··· 65 65 ;; 66 66 67 67 let start () = 68 - Picos_mux_multififo.run_on ~n_domains:8 (fun _ -> 68 + Picos_mux_multififo.run_on ~n_domains:1 (fun _ -> 69 69 Flock.join_after @@ fun () -> 70 70 init_logging (); 71 71 start_ui ())
+12 -8
jj_tui/widget-test/main.ml
··· 24 24 og 25 25 |> Lwd.pure 26 26 |> W.Scroll.area 27 - |> W.Box.box 27 + |> W.Box.box 28 28 |>$ Ui.resize ~sh:1 ~mh:1000 29 29 |> W.size_logger) 30 30 ; pString "| " ··· 116 116 og 117 117 |> Lwd.pure 118 118 |> W.Scroll.area 119 - |> W.Box.focusable 119 + |> W.Box.focusable 120 120 |> W.size_logger 121 121 |>$ Ui.resize ~pad:Gravity.default ~crop:Gravity.default) 122 122 ] ··· 444 444 let w_9 = 445 445 let items = 446 446 [ "hi"; "it's"; "meeeeeeeeeeeeeeeeeeeeeeeee" ] 447 - |> List.map (fun item -> W.Lists.{ data = item; ui = W.Lists.selectable_item (W.string item) }) 447 + |> List.map (fun item -> 448 + W.Lists. 449 + { 450 + data = item 451 + ; id = item |> String.hash 452 + ; ui = W.Lists.selectable_item (W.string item) 453 + }) 448 454 |> Lwd.pure 449 455 in 450 456 W.vbox ··· 452 458 items 453 459 |> W.Lists.selection_list_custom 454 460 ~on_selection_change:(fun x -> ()) 455 - ~custom_handler:(fun _ key -> match key with _ -> `Unhandled) 461 + ~custom_handler:(fun _ key -> match key with _ -> `Unhandled) 456 462 |>$ Ui.resize ~w:10 ~sw:1 457 463 |> W.Box.focusable ~pad_h:0 458 464 ; items 459 465 |> W.Lists.selection_list_custom 460 - 461 466 ~on_selection_change:(fun x -> ()) 462 - ~custom_handler:(fun _ key -> match key with _ -> `Unhandled) 467 + ~custom_handler:(fun _ key -> match key with _ -> `Unhandled) 463 468 |>$ Ui.resize ~w:10 ~sw:1 464 469 |> W.Box.focusable ~pad_h:0 465 470 ; items 466 471 |> W.Lists.selection_list_custom 467 - 468 472 ~on_selection_change:(fun x -> ()) 469 - ~custom_handler:(fun _ key -> match key with _ -> `Unhandled) 473 + ~custom_handler:(fun _ key -> match key with _ -> `Unhandled) 470 474 |>$ Ui.resize ~w:10 ~sw:1 471 475 |> W.Box.focusable ~pad_h:0 472 476 ]