···3434 ; lines_removed : int
3535 ; status : string
3636 }
3737+ [@@deriving equal, sexp_of]
3738 end
38393940 type t =
···4142 ; total_added : int
4243 ; total_removed : int
4344 }
4545+ [@@deriving equal, sexp_of]
4446end
45474648(** Parse structured diff stat output from a jj template. *)
···60626163(** Get the current user's email from jj config. *)
6264val get_user_email : unit -> string Lwt.t
6565+6666+(** Fetch a unified diff for a specific file in a change. *)
6767+val fetch_file_diff : change_id:string -> path:string -> string Lwt.t
+96-311
lib/tui/lens_tui.ml
···33open! Import
44open Bonsai.Let_syntax
5566-module Focus = struct
66+module Route = struct
77 type t =
88 | Log
99- | Edit_description of { change_id : string }
1010- | Request_review of { change_id : string }
99+ | Code_review of
1010+ { change_id : string
1111+ ; commit_id : string
1212+ }
1313+ [@@deriving equal, sexp_of]
1114end
12151313-type t =
1414- { focus : Focus.t Bonsai.t
1515- ; set_focus : (Focus.t -> unit Effect.t) Bonsai.t
1616- ; editor : Editor.t
1717- ; log_view : Log_view.t
1818- ; textbox : Bonsai_tui_textbox.t Bonsai.t
1919- ; review_view : Review_view.t
2020- ; store : Lens_backend.Store.t
2121- ; exit : unit -> unit Effect.t
2222- }
2323-2424-let handler t =
2525- let%arr focus = t.focus
2626- and set_focus = t.set_focus
2727- and editor_handler = Editor.handler t.editor
2828- and editor_set_text = t.editor.set_text
2929- and refresh_log_changes = t.log_view.refresh_changes
3030- and refresh_log_reviews = t.log_view.refresh_reviews
3131- and log_handler = t.log_view.handler
3232- and selected_row = t.log_view.selected_row
3333- and set_selected_idx = t.log_view.set_selected_idx
3434- and textbox = t.textbox
3535- and refresh_review_view = t.review_view.refresh in
3636- fun (event : Event.t) ->
3737- match focus with
3838- | Request_review { change_id } ->
3939- (match event with
4040- | Key_press { key = Escape; mods = [] } ->
4141- let open Effect.Let_syntax in
4242- let%bind () = textbox.set "" in
4343- set_focus Log
4444- | Key_press { key = Enter; mods = [] } ->
4545- let reviewer_email = String.strip textbox.string in
4646- if String.is_empty reviewer_email
4747- then Effect.Ignore
4848- else
4949- let open Effect.Let_syntax in
5050- let%bind () =
5151- Effect.of_lwt_thunk (fun () ->
5252- Lens_backend.Store.request
5353- t.store
5454- (Lens_backend.Change_id.of_string change_id)
5555- (Lens_backend.User_id.of_string reviewer_email))
5656- in
5757- let%bind () = textbox.set "" in
5858- let%bind () = refresh_review_view in
5959- let%bind () = refresh_log_reviews in
6060- set_focus Log
6161- | event -> textbox.handler event)
6262- | Edit_description { change_id } ->
6363- let open Effect.Let_syntax in
6464- let%bind action = editor_handler event in
6565- (match action with
6666- | None -> return ()
6767- | Some Exit_without_saving -> set_focus Log
6868- | Some (Save_and_exit message) ->
6969- let%bind () = Effect.of_lwt_thunk (fun () -> Jj.describe ~change_id ~message) in
7070- let%bind () = refresh_log_changes in
7171- set_focus Log)
7272- | Log ->
7373- let%bind.Ui_effect captured = log_handler event in
7474- (match captured with
7575- | Captured -> Ui_effect.Ignore
7676- | Ignored ->
7777- (match event with
7878- | Key_press { key = ASCII 'q'; mods = [] }
7979- | Key_press { key = ASCII ('c' | 'C'); mods = [ Ctrl ] } -> t.exit ()
8080- | Key_press { key = ASCII 'd'; mods = [] } ->
8181- (* Open editor for selected change *)
8282- (match selected_row with
8383- | None -> Effect.Ignore
8484- | Some (row : Graph_layout.Row.t) ->
8585- let open Effect.Let_syntax in
8686- let%bind () = editor_set_text row.change.description in
8787- set_focus (Edit_description { change_id = row.change.change_id }))
8888- | Key_press { key = ASCII 'r'; mods = [] } ->
8989- (* Open reviewer input for selected change *)
9090- (match selected_row with
9191- | None -> Effect.Ignore
9292- | Some (row : Graph_layout.Row.t) ->
9393- let open Effect.Let_syntax in
9494- let%bind () = textbox.set "" in
9595- set_focus (Request_review { change_id = row.change.change_id }))
9696- | Key_press { key = ASCII 'n'; mods = [] } ->
9797- (* Create new change, refresh log, open editor *)
9898- let open Effect.Let_syntax in
9999- let%bind () = Effect.of_lwt_thunk Jj.new_change in
100100- let%bind new_changes = Effect.of_lwt_thunk Jj.fetch_log in
101101- let%bind () = refresh_log_changes in
102102- let wc_idx =
103103- List.findi new_changes ~f:(fun _ (c : Jj.Change.t) ->
104104- c.current_working_copy)
105105- in
106106- (match wc_idx with
107107- | Some (idx, change) ->
108108- let%bind () = set_selected_idx (Fn.const idx) in
109109- let%bind () = editor_set_text "" in
110110- set_focus (Edit_description { change_id = change.change_id })
111111- | None -> Effect.Ignore)
112112- | _ -> Effect.Ignore))
113113-;;
114114-11516let app ~store =
11617 Staged.stage
11718 @@ fun ~exit ~(dimensions : Dimensions.t Bonsai.t) (graph @ local) ->
···11920 Catppuccin.set_flavor_within_app
12021 flavor
12122 (fun (graph @ local) ->
122122- let focus, set_focus = Bonsai.state (Log : Focus.t) graph in
123123- let pad_w = 2 in
124124- let pad_h = 1 in
125125- let shortcuts_height = 1 in
126126- let separator_height = 1 in
127127- let review_info_height = 1 in
128128- let top_dims =
129129- let%arr dimensions in
130130- let top_h = (dimensions.height * 60 / 100) - pad_h in
131131- { Dimensions.width = dimensions.width - (2 * pad_w); height = top_h }
2323+ let route, set_route = Bonsai.state (Route.Log : Route.t) graph in
2424+ (* Review progress survives route changes *)
2525+ let reviewed_files_map, set_reviewed_files_map =
2626+ Bonsai.state String.Map.empty graph
13227 in
133133- let bottom_dims =
134134- let%arr dimensions in
135135- let top_h = (dimensions.height * 60 / 100) - pad_h in
136136- { Dimensions.width = dimensions.width - (2 * pad_w)
137137- ; height =
138138- dimensions.height
139139- - pad_h
140140- - top_h
141141- - separator_height
142142- - review_info_height
143143- - pad_h
144144- - shortcuts_height
145145- - pad_h
146146- }
147147- in
148148- let log_view = Log_view.component ~dimensions:top_dims ~store graph in
149149- let editor = Editor.component ~dimensions graph in
150150- let selected_change =
151151- let%arr selected_row = log_view.selected_row in
152152- Option.map selected_row ~f:Graph_layout.Row.change
153153- in
154154- let bottom_view =
155155- Diff_view.component ~dimensions:bottom_dims ~selected_change graph
156156- in
157157- let review_view = Review_view.component ~selected_change ~store graph in
158158- let textbox_focused =
159159- let%arr focus in
160160- match focus with
161161- | Request_review _ -> true
162162- | Log | Edit_description _ -> false
163163- in
164164- let textbox_cursor_attrs =
165165- let%arr flavor = Catppuccin.flavor graph in
166166- [ Attr.bg (Catppuccin.color ~flavor Text) ]
167167- in
168168- let textbox_text_attrs =
169169- let%arr flavor = Catppuccin.flavor graph in
170170- [ Attr.fg (Catppuccin.color ~flavor Text) ]
171171- in
172172- let textbox =
173173- Bonsai_tui_textbox.component
174174- ~cursor_attrs:textbox_cursor_attrs
175175- ~text_attrs:textbox_text_attrs
176176- ~is_focused:textbox_focused
177177- graph
178178- in
179179- let t = { set_focus; focus; editor; log_view; textbox; review_view; store; exit } in
180180- let set_cursor = Effect.set_cursor graph in
181181- let%sub view, cursor_pos =
182182- let%arr top_view = log_view.view
183183- and bottom_view
184184- and review_info_view = review_view.view
185185- and dimensions
186186- and focus
187187- and editor_view = editor.view
188188- and get_cursor_pos = editor.get_cursor_position
189189- and textbox = textbox
190190- and flavor = Catppuccin.flavor graph in
191191- let c color = Catppuccin.color ~flavor color in
192192- let content_width = dimensions.width - (2 * pad_w) in
193193- let separator =
194194- View.pad
195195- ~l:pad_w
196196- (View.text
197197- ~attrs:[ Attr.fg (c Overlay0) ]
198198- (String.concat (List.init content_width ~f:(fun _ -> "\xe2\x94\x80"))))
199199- in
200200- let crop_w v =
201201- View.crop ~r:(max 0 (View.width v - (dimensions.width - (2 * pad_w)))) v
202202- in
203203- let inset v = View.pad ~l:pad_w ~t:pad_h (crop_w v) in
204204- let inset_no_top v = View.pad ~l:pad_w (crop_w v) in
205205- let shortcut label desc =
206206- let label_color = c Subtext0 in
207207- View.hcat
208208- [ View.text
209209- ~attrs:[ Attr.fg (c Base); Attr.bg label_color; Attr.bold ]
210210- (" " ^ label ^ " ")
211211- ; View.text ~attrs:[ Attr.fg label_color ] (" " ^ desc)
212212- ]
213213- in
214214- let footer =
215215- View.pad
216216- ~l:pad_w
217217- (View.hcat
218218- (match focus with
219219- | Edit_description _ ->
220220- [ shortcut "ctrl-s" "save"; View.text " "; shortcut "esc" "cancel" ]
221221- | Request_review _ ->
222222- [ shortcut "enter" "submit"; View.text " "; shortcut "esc" "cancel" ]
223223- | Log ->
224224- [ shortcut "j/k" "navigate"
225225- ; View.text " "
226226- ; shortcut "n" "new"
227227- ; View.text " "
228228- ; shortcut "d" "describe"
229229- ; View.text " "
230230- ; shortcut "r" "review"
231231- ; View.text " "
232232- ; shortcut "q" "quit"
233233- ]))
234234- in
235235- let content =
236236- View.vcat
237237- [ inset top_view
238238- ; separator
239239- ; inset_no_top review_info_view
240240- ; inset_no_top bottom_view
241241- ]
242242- in
243243- let pinned_footer = View.pad ~t:(dimensions.height - 2) footer in
244244- let bg =
245245- View.rectangle
246246- ~attrs:[ Attr.bg (c Base) ]
247247- ~width:dimensions.width
248248- ~height:dimensions.height
249249- ()
250250- in
251251- let base_view = View.zcat [ pinned_footer; content; bg ] in
252252- match focus with
253253- | Log -> base_view, None
254254- | Request_review _ ->
255255- let tb_width = max 30 (dimensions.width - 40) in
256256- let tb_view = View.with_colors textbox.view ~fg:(c Text) ~bg:(c Mantle) in
257257- let tb_pad = max 0 (tb_width - View.width tb_view) in
258258- let padded_tb =
259259- View.hcat
260260- [ tb_view
261261- ; View.rectangle
262262- ~attrs:[ Attr.bg (c Mantle) ]
263263- ~width:tb_pad
264264- ~height:1
265265- ()
266266- ]
2828+ let%sub final_view, final_handler =
2929+ match%sub route with
3030+ | Log ->
3131+ let enter_review =
3232+ let%arr set_route in
3333+ fun (change : Jj.Change.t) ->
3434+ let open Effect.Let_syntax in
3535+ let%bind should_review =
3636+ Effect.of_lwt_thunk (fun () ->
3737+ let open Lwt.Syntax in
3838+ let* user_email = Jj.get_user_email () in
3939+ let change_id_t = Lens_backend.Change_id.of_string change.change_id in
4040+ let user_id_t = Lens_backend.User_id.of_string user_email in
4141+ let* reqs = Lens_backend.Store.pending_requests store change_id_t in
4242+ let is_reviewer =
4343+ List.exists reqs ~f:(fun (r : Lens_backend.Store.Request.t) ->
4444+ Lens_backend.User_id.equal r.user_id user_id_t)
4545+ in
4646+ if is_reviewer
4747+ then
4848+ let* already_approved =
4949+ Lens_backend.Store.is_approved_by
5050+ store
5151+ change_id_t
5252+ user_id_t
5353+ ~commit_id:change.commit_id
5454+ in
5555+ if already_approved then Lwt.return false else Lwt.return true
5656+ else Lwt.return false)
5757+ in
5858+ if should_review
5959+ then
6060+ set_route
6161+ (Route.Code_review
6262+ { change_id = change.change_id; commit_id = change.commit_id })
6363+ else Effect.Ignore
6464+ in
6565+ let ~view, ~handler =
6666+ Log_screen.component ~dimensions ~store ~exit ~enter_review graph
6767+ in
6868+ let%arr view and handler in
6969+ view, handler
7070+ | Code_review { change_id; commit_id } ->
7171+ let reviewed_files =
7272+ let%arr reviewed_files_map and change_id and commit_id in
7373+ let key = change_id ^ ":" ^ commit_id in
7474+ Map.find reviewed_files_map key |> Option.value ~default:String.Set.empty
7575+ in
7676+ let set_reviewed_files =
7777+ let%arr set_reviewed_files_map
7878+ and reviewed_files_map
7979+ and change_id
8080+ and commit_id in
8181+ fun new_set ->
8282+ let key = change_id ^ ":" ^ commit_id in
8383+ set_reviewed_files_map (Map.set reviewed_files_map ~key ~data:new_set)
26784 in
268268- let tb_box =
269269- Bonsai_tui_border_box.view
270270- ~line_type:Round_corners
271271- ~left_padding:1
272272- ~right_padding:1
273273- ~top_padding:0
274274- ~bottom_padding:0
275275- ~title:
276276- (View.text ~attrs:[ Attr.fg (c Mauve); Attr.bold ] " Request Review ")
277277- ~attrs:[ Attr.fg (c Overlay1); Attr.bg (c Mantle) ]
278278- padded_tb
8585+ let on_exit =
8686+ let%arr set_route in
8787+ set_route Route.Log
27988 in
280280- let overlay = View.center tb_box ~within:dimensions in
281281- View.zcat [ overlay; base_view ], None
282282- | Edit_description _ ->
283283- let editor_height = max 10 (dimensions.height / 3) in
284284- let editor_width = max 20 (dimensions.width - 20) in
285285- let padded_editor =
286286- let ev = View.with_colors editor_view ~fg:(c Text) ~bg:(c Mantle) in
287287- let pad_h = max 0 (editor_height - View.height ev) in
288288- View.vcat
289289- [ ev
290290- ; View.rectangle
291291- ~attrs:[ Attr.bg (c Mantle) ]
292292- ~width:editor_width
293293- ~height:pad_h
294294- ()
295295- ]
8989+ let on_approve =
9090+ let%arr set_route and change_id and commit_id in
9191+ let open Effect.Let_syntax in
9292+ let%bind () =
9393+ Effect.of_lwt_thunk (fun () ->
9494+ let open Lwt.Syntax in
9595+ let* user_email = Jj.get_user_email () in
9696+ Lens_backend.Store.approve
9797+ store
9898+ (Lens_backend.Change_id.of_string change_id)
9999+ (Lens_backend.User_id.of_string user_email)
100100+ (Lens_backend.Operation_id.of_string commit_id))
101101+ in
102102+ set_route Route.Log
296103 in
297297- let editor_box =
298298- Bonsai_tui_border_box.view
299299- ~line_type:Round_corners
300300- ~left_padding:1
301301- ~right_padding:1
302302- ~top_padding:0
303303- ~bottom_padding:0
304304- ~title:
305305- (View.text ~attrs:[ Attr.fg (c Mauve); Attr.bold ] " Edit Description ")
306306- ~attrs:[ Attr.fg (c Overlay1); Attr.bg (c Mantle) ]
307307- padded_editor
104104+ let ~view, ~handler =
105105+ Code_review_view.component
106106+ ~dimensions
107107+ ~change_id
108108+ ~commit_id
109109+ ~on_exit
110110+ ~on_approve
111111+ ~reviewed_files
112112+ ~set_reviewed_files
113113+ graph
308114 in
309309- let overlay = View.center editor_box ~within:dimensions in
310310- let final_view = View.zcat [ overlay; base_view ] in
311311- let cursor_pos = get_cursor_pos final_view in
312312- final_view, cursor_pos
115115+ let%arr view and handler in
116116+ view, handler
313117 in
314314- Bonsai.Edge.on_change
315315- cursor_pos
316316- ~trigger:`After_display
317317- ~equal:[%equal: Position.t option]
318318- ~callback:
319319- (let%arr set_cursor and focus in
320320- fun pos ->
321321- match focus with
322322- | Log | Request_review _ ->
323323- Effect.Many [ Effect.hide_cursor; set_cursor None ]
324324- | Edit_description _ ->
325325- (match pos with
326326- | Some pos ->
327327- Effect.Many
328328- [ Effect.show_cursor
329329- ; set_cursor (Some { position = pos; kind = Bar })
330330- ]
331331- | None -> Effect.Ignore))
332332- graph;
333333- ~view, ~handler:(handler t))
118118+ ~view:final_view, ~handler:final_handler)
334119 graph
335120;;
336121
+347
lib/tui/log_screen.ml
···11+open! Core
22+open! Bonsai_term
33+open! Import
44+open Bonsai.Let_syntax
55+66+module Focus = struct
77+ type t =
88+ | Log
99+ | Edit_description of { change_id : string }
1010+ | Request_review of { change_id : string }
1111+end
1212+1313+type t =
1414+ { focus : Focus.t Bonsai.t
1515+ ; set_focus : (Focus.t -> unit Effect.t) Bonsai.t
1616+ ; editor : Editor.t
1717+ ; log_view : Log_view.t
1818+ ; textbox : Bonsai_tui_textbox.t Bonsai.t
1919+ ; review_view : Review_view.t
2020+ ; store : Lens_backend.Store.t
2121+ ; exit : unit -> unit Effect.t
2222+ ; enter_review : (Jj.Change.t -> unit Effect.t) Bonsai.t
2323+ }
2424+2525+let handler t =
2626+ let%arr focus = t.focus
2727+ and set_focus = t.set_focus
2828+ and editor_handler = Editor.handler t.editor
2929+ and editor_set_text = t.editor.set_text
3030+ and refresh_log_changes = t.log_view.refresh_changes
3131+ and refresh_log_reviews = t.log_view.refresh_reviews
3232+ and log_handler = t.log_view.handler
3333+ and selected_row = t.log_view.selected_row
3434+ and set_selected_idx = t.log_view.set_selected_idx
3535+ and textbox = t.textbox
3636+ and refresh_review_view = t.review_view.refresh
3737+ and enter_review = t.enter_review in
3838+ fun (event : Event.t) ->
3939+ match focus with
4040+ | Request_review { change_id } ->
4141+ (match event with
4242+ | Key_press { key = Escape; mods = [] } ->
4343+ let open Effect.Let_syntax in
4444+ let%bind () = textbox.set "" in
4545+ set_focus Log
4646+ | Key_press { key = Enter; mods = [] } ->
4747+ let reviewer_email = String.strip textbox.string in
4848+ if String.is_empty reviewer_email
4949+ then Effect.Ignore
5050+ else
5151+ let open Effect.Let_syntax in
5252+ let%bind () =
5353+ Effect.of_lwt_thunk (fun () ->
5454+ Lens_backend.Store.request
5555+ t.store
5656+ (Lens_backend.Change_id.of_string change_id)
5757+ (Lens_backend.User_id.of_string reviewer_email))
5858+ in
5959+ let%bind () = textbox.set "" in
6060+ let%bind () = refresh_review_view in
6161+ let%bind () = refresh_log_reviews in
6262+ set_focus Log
6363+ | event -> textbox.handler event)
6464+ | Edit_description { change_id } ->
6565+ let open Effect.Let_syntax in
6666+ let%bind action = editor_handler event in
6767+ (match action with
6868+ | None -> return ()
6969+ | Some Exit_without_saving -> set_focus Log
7070+ | Some (Save_and_exit message) ->
7171+ let%bind () = Effect.of_lwt_thunk (fun () -> Jj.describe ~change_id ~message) in
7272+ let%bind () = refresh_log_changes in
7373+ set_focus Log)
7474+ | Log ->
7575+ let%bind.Ui_effect captured = log_handler event in
7676+ (match captured with
7777+ | Captured -> Ui_effect.Ignore
7878+ | Ignored ->
7979+ (match event with
8080+ | Key_press { key = ASCII 'q'; mods = [] }
8181+ | Key_press { key = ASCII ('c' | 'C'); mods = [ Ctrl ] } -> t.exit ()
8282+ | Key_press { key = ASCII 'd'; mods = [] } ->
8383+ (match selected_row with
8484+ | None -> Effect.Ignore
8585+ | Some (row : Graph_layout.Row.t) ->
8686+ let open Effect.Let_syntax in
8787+ let%bind () = editor_set_text row.change.description in
8888+ set_focus (Edit_description { change_id = row.change.change_id }))
8989+ | Key_press { key = ASCII 'r'; mods = [] } ->
9090+ (match selected_row with
9191+ | None -> Effect.Ignore
9292+ | Some (row : Graph_layout.Row.t) ->
9393+ let open Effect.Let_syntax in
9494+ let%bind () = textbox.set "" in
9595+ set_focus (Request_review { change_id = row.change.change_id }))
9696+ | Key_press { key = Enter; mods = [] } ->
9797+ (match selected_row with
9898+ | None -> Effect.Ignore
9999+ | Some (row : Graph_layout.Row.t) -> enter_review row.change)
100100+ | Key_press { key = ASCII 'n'; mods = [] } ->
101101+ let open Effect.Let_syntax in
102102+ let%bind () = Effect.of_lwt_thunk Jj.new_change in
103103+ let%bind new_changes = Effect.of_lwt_thunk Jj.fetch_log in
104104+ let%bind () = refresh_log_changes in
105105+ let wc_idx =
106106+ List.findi new_changes ~f:(fun _ (c : Jj.Change.t) ->
107107+ c.current_working_copy)
108108+ in
109109+ (match wc_idx with
110110+ | Some (idx, change) ->
111111+ let%bind () = set_selected_idx (Fn.const idx) in
112112+ let%bind () = editor_set_text "" in
113113+ set_focus (Edit_description { change_id = change.change_id })
114114+ | None -> Effect.Ignore)
115115+ | _ -> Effect.Ignore))
116116+;;
117117+118118+let component
119119+ ~(dimensions : Dimensions.t Bonsai.t)
120120+ ~(store : Lens_backend.Store.t)
121121+ ~(exit : unit -> unit Effect.t)
122122+ ~(enter_review : (Jj.Change.t -> unit Effect.t) Bonsai.t)
123123+ (graph @ local)
124124+ =
125125+ let focus, set_focus = Bonsai.state (Log : Focus.t) graph in
126126+ let pad_w = 2 in
127127+ let pad_h = 1 in
128128+ let shortcuts_height = 1 in
129129+ let separator_height = 1 in
130130+ let review_info_height = 1 in
131131+ let top_dims =
132132+ let%arr dimensions in
133133+ let top_h = (dimensions.height * 60 / 100) - pad_h in
134134+ { Dimensions.width = dimensions.width - (2 * pad_w); height = top_h }
135135+ in
136136+ let bottom_dims =
137137+ let%arr dimensions in
138138+ let top_h = (dimensions.height * 60 / 100) - pad_h in
139139+ { Dimensions.width = dimensions.width - (2 * pad_w)
140140+ ; height =
141141+ dimensions.height
142142+ - pad_h
143143+ - top_h
144144+ - separator_height
145145+ - review_info_height
146146+ - pad_h
147147+ - shortcuts_height
148148+ - pad_h
149149+ }
150150+ in
151151+ let log_view = Log_view.component ~dimensions:top_dims ~store graph in
152152+ let editor = Editor.component ~dimensions graph in
153153+ let selected_change =
154154+ let%arr selected_row = log_view.selected_row in
155155+ Option.map selected_row ~f:Graph_layout.Row.change
156156+ in
157157+ let bottom_view =
158158+ Diff_view.component ~dimensions:bottom_dims ~selected_change graph
159159+ in
160160+ let review_view = Review_view.component ~selected_change ~store graph in
161161+ let textbox_focused =
162162+ let%arr focus in
163163+ match focus with
164164+ | Request_review _ -> true
165165+ | Log | Edit_description _ -> false
166166+ in
167167+ let textbox_cursor_attrs =
168168+ let%arr flavor = Catppuccin.flavor graph in
169169+ [ Attr.bg (Catppuccin.color ~flavor Text) ]
170170+ in
171171+ let textbox_text_attrs =
172172+ let%arr flavor = Catppuccin.flavor graph in
173173+ [ Attr.fg (Catppuccin.color ~flavor Text) ]
174174+ in
175175+ let textbox =
176176+ Bonsai_tui_textbox.component
177177+ ~cursor_attrs:textbox_cursor_attrs
178178+ ~text_attrs:textbox_text_attrs
179179+ ~is_focused:textbox_focused
180180+ graph
181181+ in
182182+ let t =
183183+ { set_focus
184184+ ; focus
185185+ ; editor
186186+ ; log_view
187187+ ; textbox
188188+ ; review_view
189189+ ; store
190190+ ; exit
191191+ ; enter_review
192192+ }
193193+ in
194194+ let set_cursor = Effect.set_cursor graph in
195195+ let%sub view, cursor_pos =
196196+ let%arr top_view = log_view.view
197197+ and bottom_view
198198+ and review_info_view = review_view.view
199199+ and dimensions
200200+ and focus
201201+ and editor_view = editor.view
202202+ and get_cursor_pos = editor.get_cursor_position
203203+ and textbox
204204+ and flavor = Catppuccin.flavor graph in
205205+ let c color = Catppuccin.color ~flavor color in
206206+ let content_width = dimensions.width - (2 * pad_w) in
207207+ let separator =
208208+ View.pad
209209+ ~l:pad_w
210210+ (View.text
211211+ ~attrs:[ Attr.fg (c Overlay0) ]
212212+ (String.concat (List.init content_width ~f:(fun _ -> "\xe2\x94\x80"))))
213213+ in
214214+ let crop_w v =
215215+ View.crop ~r:(max 0 (View.width v - (dimensions.width - (2 * pad_w)))) v
216216+ in
217217+ let inset v = View.pad ~l:pad_w ~t:pad_h (crop_w v) in
218218+ let inset_no_top v = View.pad ~l:pad_w (crop_w v) in
219219+ let shortcut label desc =
220220+ let label_color = c Subtext0 in
221221+ View.hcat
222222+ [ View.text
223223+ ~attrs:[ Attr.fg (c Base); Attr.bg label_color; Attr.bold ]
224224+ (" " ^ label ^ " ")
225225+ ; View.text ~attrs:[ Attr.fg label_color ] (" " ^ desc)
226226+ ]
227227+ in
228228+ let footer =
229229+ View.pad
230230+ ~l:pad_w
231231+ (View.hcat
232232+ (match focus with
233233+ | Edit_description _ ->
234234+ [ shortcut "ctrl-s" "save"; View.text " "; shortcut "esc" "cancel" ]
235235+ | Request_review _ ->
236236+ [ shortcut "enter" "submit"; View.text " "; shortcut "esc" "cancel" ]
237237+ | Log ->
238238+ [ shortcut "j/k" "navigate"
239239+ ; View.text " "
240240+ ; shortcut "n" "new"
241241+ ; View.text " "
242242+ ; shortcut "d" "describe"
243243+ ; View.text " "
244244+ ; shortcut "r" "request review"
245245+ ; View.text " "
246246+ ; shortcut "enter" "review"
247247+ ; View.text " "
248248+ ; shortcut "q" "quit"
249249+ ]))
250250+ in
251251+ let content =
252252+ View.vcat
253253+ [ inset top_view
254254+ ; separator
255255+ ; inset_no_top review_info_view
256256+ ; inset_no_top bottom_view
257257+ ]
258258+ in
259259+ let pinned_footer = View.pad ~t:(dimensions.height - 2) footer in
260260+ let bg =
261261+ View.rectangle
262262+ ~attrs:[ Attr.bg (c Base) ]
263263+ ~width:dimensions.width
264264+ ~height:dimensions.height
265265+ ()
266266+ in
267267+ let base_view = View.zcat [ pinned_footer; content; bg ] in
268268+ match focus with
269269+ | Log -> base_view, None
270270+ | Request_review _ ->
271271+ let tb_width = max 30 (dimensions.width - 40) in
272272+ let tb_view = View.with_colors textbox.view ~fg:(c Text) ~bg:(c Mantle) in
273273+ let tb_pad = max 0 (tb_width - View.width tb_view) in
274274+ let padded_tb =
275275+ View.hcat
276276+ [ tb_view
277277+ ; View.rectangle ~attrs:[ Attr.bg (c Mantle) ] ~width:tb_pad ~height:1 ()
278278+ ]
279279+ in
280280+ let tb_box =
281281+ Bonsai_tui_border_box.view
282282+ ~line_type:Round_corners
283283+ ~left_padding:1
284284+ ~right_padding:1
285285+ ~top_padding:0
286286+ ~bottom_padding:0
287287+ ~title:
288288+ (View.text ~attrs:[ Attr.fg (c Mauve); Attr.bold ] " Request Review ")
289289+ ~attrs:[ Attr.fg (c Overlay1); Attr.bg (c Mantle) ]
290290+ padded_tb
291291+ in
292292+ let overlay = View.center tb_box ~within:dimensions in
293293+ View.zcat [ overlay; base_view ], None
294294+ | Edit_description _ ->
295295+ let editor_height = max 10 (dimensions.height / 3) in
296296+ let editor_width = max 20 (dimensions.width - 20) in
297297+ let padded_editor =
298298+ let ev = View.with_colors editor_view ~fg:(c Text) ~bg:(c Mantle) in
299299+ let pad_h = max 0 (editor_height - View.height ev) in
300300+ View.vcat
301301+ [ ev
302302+ ; View.rectangle
303303+ ~attrs:[ Attr.bg (c Mantle) ]
304304+ ~width:editor_width
305305+ ~height:pad_h
306306+ ()
307307+ ]
308308+ in
309309+ let editor_box =
310310+ Bonsai_tui_border_box.view
311311+ ~line_type:Round_corners
312312+ ~left_padding:1
313313+ ~right_padding:1
314314+ ~top_padding:0
315315+ ~bottom_padding:0
316316+ ~title:
317317+ (View.text ~attrs:[ Attr.fg (c Mauve); Attr.bold ] " Edit Description ")
318318+ ~attrs:[ Attr.fg (c Overlay1); Attr.bg (c Mantle) ]
319319+ padded_editor
320320+ in
321321+ let overlay = View.center editor_box ~within:dimensions in
322322+ let final_view = View.zcat [ overlay; base_view ] in
323323+ let cursor_pos = get_cursor_pos final_view in
324324+ final_view, cursor_pos
325325+ in
326326+ Bonsai.Edge.on_change
327327+ cursor_pos
328328+ ~trigger:`After_display
329329+ ~equal:[%equal: Position.t option]
330330+ ~callback:
331331+ (let%arr set_cursor and focus in
332332+ fun pos ->
333333+ match focus with
334334+ | Log | Request_review _ ->
335335+ Effect.Many [ Effect.hide_cursor; set_cursor None ]
336336+ | Edit_description _ ->
337337+ (match pos with
338338+ | Some pos ->
339339+ Effect.Many
340340+ [ Effect.show_cursor
341341+ ; set_cursor (Some { position = pos; kind = Bar })
342342+ ]
343343+ | None -> Effect.Ignore))
344344+ graph;
345345+ let log_handler = handler t in
346346+ ~view, ~handler:log_handler
347347+;;