|
| 1 | +module Command = Minttea.Command |
| 2 | +module Event = Minttea.Event |
| 3 | +module Input = Text_input |
| 4 | + |
| 5 | +type t = { |
| 6 | + elements : (bool * string) list; |
| 7 | + shown : int list option; |
| 8 | + cursor : int; |
| 9 | + max_height : int; |
| 10 | + cursor_string : string; |
| 11 | + style_selected : Spices.style; |
| 12 | + style_unselected : Spices.style; |
| 13 | + predicate : int -> string -> bool; |
| 14 | +} |
| 15 | + |
| 16 | +let default_cursor = ">" |
| 17 | + |
| 18 | +let make (elements : string list) ?(cursor = default_cursor) |
| 19 | + ?(style_selected = Spices.default) ?(style_unselected = Spices.default) |
| 20 | + ?(max_height = 10) () = |
| 21 | + { |
| 22 | + elements = List.map (fun e -> (false, e)) elements; |
| 23 | + cursor = 0; |
| 24 | + shown = None; |
| 25 | + cursor_string = cursor; |
| 26 | + predicate = (fun _ _ -> true); |
| 27 | + max_height; |
| 28 | + style_selected; |
| 29 | + style_unselected; |
| 30 | + } |
| 31 | + |
| 32 | +let rec last = function |
| 33 | + | [] -> raise Not_found |
| 34 | + | [ e ] -> e |
| 35 | + | _ :: rest -> last rest |
| 36 | + |
| 37 | +let show_pred model predicate = |
| 38 | + let indices = |
| 39 | + (* list of shown indices *) |
| 40 | + model.elements |
| 41 | + |> List.mapi (fun idx (_, e) -> (idx, predicate idx e)) |
| 42 | + |> List.filter (fun (_, selected) -> selected) |
| 43 | + |> List.map fst |
| 44 | + in |
| 45 | + let cursor = |
| 46 | + (* set the cursor to the closest visible element *) |
| 47 | + if List.length indices = 0 then 0 |
| 48 | + else |
| 49 | + match List.find (( <= ) model.cursor) indices with |
| 50 | + | exception Not_found -> last indices |
| 51 | + | idx -> idx |
| 52 | + in |
| 53 | + { model with cursor; predicate; shown = Some indices } |
| 54 | + |
| 55 | +let show_string_contains model s = |
| 56 | + (* return true if the filter matches the element *) |
| 57 | + let match_filter filter _ element = |
| 58 | + match Str.search_forward (Str.regexp filter) element 0 with |
| 59 | + | exception Not_found -> false |
| 60 | + | _ -> true |
| 61 | + in |
| 62 | + show_pred model (match_filter s) |
| 63 | + |
| 64 | +let show_all model = { model with shown = None } |
| 65 | + |
| 66 | +(* move the cursor in the list of visible elements, eventually wrapping *) |
| 67 | +let prev_visible cur shown = |
| 68 | + if shown = [] then 0 |
| 69 | + else |
| 70 | + let last = last shown in |
| 71 | + let rec loop cur shown = |
| 72 | + match shown with |
| 73 | + | a :: _ when a < cur -> a |
| 74 | + | _ :: rest -> loop cur rest |
| 75 | + | [] -> last |
| 76 | + in |
| 77 | + loop cur (List.rev shown) |
| 78 | + |
| 79 | +(* move the cursor in the list of visible elements, eventually wrapping *) |
| 80 | +let next_visible cur shown = |
| 81 | + let first = match shown with a :: _ -> a | [] -> 0 in |
| 82 | + let rec loop cur shown = |
| 83 | + match shown with |
| 84 | + | a :: _ when a > cur -> a |
| 85 | + | _ :: rest -> loop cur rest |
| 86 | + | [] -> first |
| 87 | + in |
| 88 | + loop cur shown |
| 89 | + |
| 90 | +let update event (model : t) = |
| 91 | + match event with |
| 92 | + | Event.KeyDown (Key "s" | Space) -> |
| 93 | + (* select current element *) |
| 94 | + { |
| 95 | + model with |
| 96 | + elements = |
| 97 | + List.mapi |
| 98 | + (fun idx (s, e) -> |
| 99 | + if idx = model.cursor then (not s, e) else (s, e)) |
| 100 | + model.elements; |
| 101 | + } |
| 102 | + | Event.KeyDown (Up | Key "k") -> |
| 103 | + let len = List.length model.elements in |
| 104 | + if len = 0 then model |
| 105 | + else |
| 106 | + { |
| 107 | + model with |
| 108 | + cursor = |
| 109 | + (match model.shown with |
| 110 | + | None -> (model.cursor + len - 1) mod len |
| 111 | + | Some shown -> prev_visible model.cursor shown); |
| 112 | + } |
| 113 | + | Event.KeyDown (Down | Key "j") -> |
| 114 | + let len = List.length model.elements in |
| 115 | + if len = 0 then model |
| 116 | + else |
| 117 | + { |
| 118 | + model with |
| 119 | + cursor = |
| 120 | + (match model.shown with |
| 121 | + | None -> (model.cursor + 1) mod len |
| 122 | + | Some shown -> next_visible model.cursor shown); |
| 123 | + } |
| 124 | + | Event.KeyDown (Left | Key "h") -> |
| 125 | + (* previous page, not wrapping *) |
| 126 | + { model with cursor = max (model.cursor - model.max_height) 0 } |
| 127 | + | Event.KeyDown (Right | Key "l") -> |
| 128 | + (* next page, not wrapping *) |
| 129 | + { |
| 130 | + model with |
| 131 | + cursor = |
| 132 | + min |
| 133 | + (model.cursor + model.max_height) |
| 134 | + (max 0 (List.length model.elements - 1)); |
| 135 | + } |
| 136 | + | _ -> model |
| 137 | + |
| 138 | +(* drop the first n elements of the list *) |
| 139 | +let rec drop n lst = |
| 140 | + if n = 0 then lst |
| 141 | + else match lst with _ :: rest -> drop (n - 1) rest | [] -> [] |
| 142 | + |
| 143 | +(* keep the first n elements of the list *) |
| 144 | +let take n lst = |
| 145 | + let rec aux lst n acc = |
| 146 | + if n = 0 then List.rev acc |
| 147 | + else |
| 148 | + match lst with |
| 149 | + | x :: rest -> aux rest (n - 1) (x :: acc) |
| 150 | + | [] -> List.rev acc |
| 151 | + in |
| 152 | + aux lst n [] |
| 153 | + |
| 154 | +(* only keep visible items, counting on the order of shown and elems *) |
| 155 | +let pick_visible shown elems = |
| 156 | + let rec loop shown elems acc = |
| 157 | + match (shown, elems) with |
| 158 | + | [], _ -> List.rev acc |
| 159 | + | _, [] -> List.rev acc |
| 160 | + | idxa :: shown, (idxb, s, e) :: elems when idxa = idxb -> |
| 161 | + loop shown elems ((idxa, s, e) :: acc) |
| 162 | + | shown, _ :: elems -> loop shown elems acc |
| 163 | + in |
| 164 | + loop shown elems [] |
| 165 | + |
| 166 | +let visible_cursor model = |
| 167 | + match model.shown with |
| 168 | + | None -> model.cursor |
| 169 | + | Some shown -> |
| 170 | + let rec loop rest acc = |
| 171 | + match rest with |
| 172 | + | [] -> acc |
| 173 | + | idx :: _ when idx = model.cursor -> acc |
| 174 | + | _ :: rest -> loop rest (acc + 1) |
| 175 | + in |
| 176 | + loop shown 0 |
| 177 | + |
| 178 | +let view (model : t) = |
| 179 | + let npages = |
| 180 | + (match model.shown with |
| 181 | + | Some shown -> List.length shown / model.max_height |
| 182 | + | None -> List.length model.elements / model.max_height) |
| 183 | + + 1 |
| 184 | + in |
| 185 | + let page = 1 + (visible_cursor model / model.max_height) in |
| 186 | + let elems = |
| 187 | + model.elements |
| 188 | + |> List.mapi (fun idx (selected, element) -> (idx, selected, element)) |
| 189 | + |> (match model.shown with |
| 190 | + | None -> fun x -> x |
| 191 | + | Some shown -> pick_visible shown) |
| 192 | + |> drop ((page - 1) * model.max_height) |
| 193 | + |> take model.max_height |
| 194 | + in |
| 195 | + |
| 196 | + (* Represent rows with cursor, index and selection marker *) |
| 197 | + let format_row (idx, selected, element) = |
| 198 | + let cursor = |
| 199 | + if model.cursor = idx then model.cursor_string |
| 200 | + else String.make (String.length model.cursor_string) ' ' |
| 201 | + in |
| 202 | + let bullet = |
| 203 | + if selected then Format.sprintf "[%2d]" (idx + 1) |
| 204 | + else Format.sprintf " %2d " (idx + 1) |
| 205 | + in |
| 206 | + let style = |
| 207 | + if selected then model.style_selected else model.style_unselected |
| 208 | + in |
| 209 | + Spices.build style "%s %s %s" cursor bullet element |
| 210 | + in |
| 211 | + |
| 212 | + let rows = List.map format_row elems in |
| 213 | + let lst = |
| 214 | + if List.length rows < model.max_height then |
| 215 | + String.concat "\n" |
| 216 | + (rows @ List.init (model.max_height - List.length rows) (fun _ -> "")) |
| 217 | + else String.concat "\n" rows |
| 218 | + in |
| 219 | + let page_indicator = |
| 220 | + String.concat " " |
| 221 | + @@ List.init npages (fun idx -> if idx + 1 = page then "*" else ".") |
| 222 | + in |
| 223 | + lst ^ "\n\n" ^ page_indicator |
| 224 | + |
| 225 | +(* Append more elements at the end of the list *) |
| 226 | +let append model elements = |
| 227 | + let model = |
| 228 | + { |
| 229 | + model with |
| 230 | + elements = model.elements @ List.map (fun e -> (false, e)) elements; |
| 231 | + } |
| 232 | + in |
| 233 | + (* reapply the predicate with currently active *) |
| 234 | + match model.shown with |
| 235 | + | None -> model |
| 236 | + | Some _ -> show_pred model model.predicate |
| 237 | + |
| 238 | +let filter model predicate = |
| 239 | + let model = |
| 240 | + { |
| 241 | + model with |
| 242 | + elements = List.filteri (fun idx (_, e) -> predicate idx e) model.elements; |
| 243 | + } |
| 244 | + in |
| 245 | + (* reapply the filter predicate with currently active *) |
| 246 | + match model.shown with |
| 247 | + | None -> model |
| 248 | + | Some _ -> show_pred model model.predicate |
| 249 | + |
| 250 | +(* Return the selected elements of the list *) |
| 251 | +let get_selection model = |
| 252 | + model.elements |> List.filter (fun (selected, _) -> selected) |> List.map snd |
0 commit comments