Source file selection_modal.ml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
open! Core
open Bonsai_term
open Bonsai.Let_syntax
module Item_view = struct
type t =
{ icon : View.t
; label : View.t
}
end
type 'a t =
{ view : View.t
; handler : Event.t -> unit Effect.t
; reset : unit Effect.t
}
let component
(type a)
~(items : (string * a) list Bonsai.t)
~(render_item : (string -> a -> is_selected:bool -> Item_view.t) Bonsai.t)
~(on_select : (a -> unit Effect.t) Bonsai.t)
~(dimensions : Dimensions.t Bonsai.t)
(local_ graph)
=
let is_focused = Bonsai.return true in
let cursor_attrs =
let%arr flavor = Bonsai_term_color_scheme.flavor graph in
let mauve = Bonsai_term_color_scheme.color ~flavor Mauve in
[ Attr.bg mauve ]
in
let text_attrs =
let%arr flavor = Bonsai_term_color_scheme.flavor graph in
let surface0 = Bonsai_term_color_scheme.color ~flavor Surface0 in
[ Attr.bg surface0 ]
in
let search_input = Filter_input.component ~cursor_attrs ~text_attrs ~is_focused graph in
let search_text =
let%arr { Filter_input.text; _ } = search_input in
text
in
let selected_idx, set_selected_idx = Bonsai.state 0 graph in
let () =
Bonsai.Edge.on_change
~trigger:`After_display
~equal:[%equal: string]
search_text
~callback:
(let%arr set_selected_idx in
fun (_ : string) -> set_selected_idx 0)
graph
in
let filtered_items =
let%arr items and search_text in
let query = String.lowercase search_text in
if String.is_empty query
then items
else
List.filter items ~f:(fun (name, _) ->
String.is_substring (String.lowercase name) ~substring:query)
in
let clamped_selected_idx =
let%arr selected_idx and filtered_items in
let len = List.length filtered_items in
if len = 0 then 0 else Int.min selected_idx (len - 1)
in
let view =
let%arr flavor = Bonsai_term_color_scheme.flavor graph
and { Dimensions.height; width } = dimensions
and { Filter_input.view = search_input_view; _ } = search_input
and filtered_items
and clamped_selected_idx
and render_item in
let mauve = Bonsai_term_color_scheme.color ~flavor Mauve in
let subtext_color = Bonsai_term_color_scheme.color ~flavor Subtext0 in
let surface0 = Bonsai_term_color_scheme.color ~flavor Surface0 in
let lavender = Bonsai_term_color_scheme.color ~flavor Lavender in
let selection_bg = surface0 in
let modal_bg = surface0 in
let modal_width = Int.min (width - 6) 60 |> Int.max 30 in
let modal_height = Int.min (height - 4) 20 |> Int.max 5 in
let content_width = modal_width - 2 in
let search_prefix =
View.text ~attrs:[ Attr.fg mauve; Attr.bold; Attr.bg surface0 ] "❯ "
in
let search_line = View.hcat [ search_prefix; search_input_view ] in
let search_line_width = View.width search_line in
let search_padding = Int.max 0 (content_width - search_line_width) in
let search_line_padded =
View.hcat
[ search_line
; View.text ~attrs:[ Attr.bg surface0 ] (String.make search_padding ' ')
]
in
let separator =
View.text
~attrs:[ Attr.fg subtext_color ]
(String.concat (List.init content_width ~f:(fun _ -> "─")))
in
let max_visible_results = modal_height - 4 in
let num_results = List.length filtered_items in
let scroll_offset =
if clamped_selected_idx < max_visible_results / 2
then 0
else if clamped_selected_idx > num_results - (max_visible_results / 2)
then Int.max 0 (num_results - max_visible_results)
else Int.max 0 (clamped_selected_idx - (max_visible_results / 2))
in
let visible_results =
List.drop filtered_items scroll_offset |> List.take _ max_visible_results
in
let result_views =
if List.is_empty visible_results
then [ View.text ~attrs:[ Attr.fg subtext_color ] "(no matching items)" ]
else
List.mapi visible_results ~f:(fun display_idx (name, value) ->
let actual_idx = display_idx + scroll_offset in
let is_selected = actual_idx = clamped_selected_idx in
let { Item_view.icon; label } = render_item name value ~is_selected in
let line = View.hcat [ icon; label ] in
let line_width = View.width line in
let padding = Int.max 0 (content_width - line_width) in
let padded_line = View.hcat [ line; View.text (String.make padding ' ') ] in
if is_selected
then View.with_colors' ~bg:selection_bg padded_line
else padded_line)
in
let count_text =
if num_results = List.length filtered_items
then sprintf "%d items" num_results
else sprintf "%d / %d" num_results (List.length filtered_items)
in
let count_view = View.text ~attrs:[ Attr.fg subtext_color ] count_text in
let content =
View.vcat ([ search_line_padded; separator ] @ result_views @ [ count_view ])
in
let padded_content = View.pad ~l:1 ~r:1 content in
let bordered_content =
Bonsai_term_border_box.view
~line_type:Round_corners
~attrs:[ Attr.fg lavender ]
padded_content
in
let bw = View.width bordered_content in
let bh = View.height bordered_content in
let top_pad = Int.max 0 ((height - bh) / 2) in
let left_pad = Int.max 0 ((width - bw) / 2) in
let background = View.rectangle ~height:bh ~width:bw () in
let modal_with_bg = View.zcat [ bordered_content; background ] in
View.pad ~t:top_pad ~l:left_pad modal_with_bg |> View.with_colors' ~bg:modal_bg
in
let handler =
let%arr { Filter_input.handler = input_handler; set_text; _ } = search_input
and set_selected_idx
and clamped_selected_idx
and filtered_items
and on_select in
let reset = Effect.Many [ set_text ""; set_selected_idx 0 ] in
let handler (event : Event.t) =
match event with
| Key_press { key = Arrow `Up; mods = [] }
| Key_press { key = ASCII ('p' | 'P'); mods = [ Ctrl ] } ->
let new_idx = Int.max 0 (clamped_selected_idx - 1) in
set_selected_idx new_idx
| Key_press { key = Arrow `Down; mods = [] }
| Key_press { key = ASCII ('n' | 'N'); mods = [ Ctrl ] } ->
let len = List.length filtered_items in
let new_idx = Int.min (len - 1) (clamped_selected_idx + 1) in
set_selected_idx new_idx
| Key_press { key = Enter; mods = [] } ->
(match List.nth filtered_items clamped_selected_idx with
| None -> Effect.Ignore
| Some (_, value) -> Effect.Many [ reset; on_select value ])
| _ -> input_handler event
in
handler, reset
in
let%arr view and handler in
let handler, reset = handler in
{ view; handler; reset }
;;