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.

Merge pull request #3 from faldor20/selectable_graph

Selectable graph implemented

authored by

Eli Dowling and committed by
GitHub
4c9027dd 6a32129f

+449 -176
+25 -17
jj_tui/bin/file_view.ml
··· 14 14 let rec command_mapping = 15 15 [ 16 16 { 17 + key = 'h' 18 + ; description = "Show help" 19 + ; cmd = 20 + Fun 21 + (fun _ -> 22 + ui_state.show_popup $= Some (commands_list_ui command_mapping, "Help"); 23 + ui_state.input $= `Mode (fun _ -> `Unhandled)) 24 + } 25 + ; { 17 26 key = 'm' 18 27 ; description = "Move file to other commit" 19 28 ; cmd = ··· 21 30 ( "Revision to move file to" 22 31 , fun rev -> 23 32 Cmd 24 - [ "squash"; "-u"; "--from"; "@"; "--into"; rev; Lwd.peek selected_file ] 25 - ) 33 + [ 34 + "squash" 35 + ; "-u" 36 + ; "--from" 37 + ; Lwd.peek ui_state.selected_revision 38 + ; "--into" 39 + ; rev 40 + ; Lwd.peek selected_file 41 + ] ) 26 42 } 27 43 ; { 28 44 key = 'd' 29 45 ; description = "Restore to previous revision (git discard)" 30 46 ; cmd = 31 - Dynamic 32 - (fun _ -> 47 + Dynamic_r 48 + (fun rev -> 33 49 let selected = Lwd.peek selected_file in 34 50 confirm_prompt 35 - ("discard all changes to '" ^ selected ^ "' in this revision") 36 - (Cmd [ "restore"; selected ])) 37 - } 38 - ; { 39 - key = 'h' 40 - ; description = "Show help" 41 - ; cmd = 42 - Fun 43 - (fun _ -> 44 - ui_state.show_popup $= Some (commands_list_ui command_mapping, "Help"); 45 - ui_state.input $= `Mode (fun _ -> `Unhandled)) 51 + ("discard all changes to '" ^ selected ^ "' in rev " ^ rev) 52 + (Cmd [ "restore"; "--to"; rev; "--from"; rev ^ "-"; selected ])) 46 53 } 47 54 ] 48 55 ;; ··· 65 72 66 73 (**Get the status for the currently selected file*) 67 74 let file_status () = 68 - let$ selected = Lwd.get selected_file in 69 - if selected != "" then jj_no_log [ "diff"; selected ] else "" 75 + let$ selected = Lwd.get selected_file 76 + and$ rev = Lwd.get Vars.ui_state.selected_revision in 77 + if selected != "" then jj_no_log [ "diff"; "-r"; rev; selected ] else "" 70 78 ;; 71 79 end
+16 -4
jj_tui/bin/global_funcs.ml
··· 35 35 36 36 (**Updates the status windows; Without snapshotting the working copy by default 37 37 This should be called after any command that performs a change *) 38 - let update_status ?(cause_snapshot = false) () = 38 + let update_status ?(update_graph = true) ?(cause_snapshot = false) () = 39 + let rev = Lwd.peek Vars.ui_state.selected_revision in 40 + let log_res = 41 + jj_no_log ~snapshot:cause_snapshot [ "log" ] 42 + |> colored_string 43 + in 44 + if update_graph then Vars.ui_state.trigger_update $= (); 45 + ;; 46 + (**Updates the status windows; Without snapshotting the working copy by default 47 + This should be called after any command that performs a change *) 48 + let update_views ?(cause_snapshot = false) () = 49 + let rev = Lwd.peek Vars.ui_state.selected_revision in 39 50 Eio.Switch.run @@ fun sw -> 40 51 let log_res = 41 - jj_no_log ~snapshot:cause_snapshot [ "show"; "-s"; "--color-words" ] |> colored_string 52 + jj_no_log ~snapshot:cause_snapshot [ "show"; "-s"; "--color-words"; "-r"; rev ] 53 + |> colored_string 42 54 in 43 55 (* From now on we use ignore-working-copy so we don't re-snapshot the state and so 44 56 we can operate in paralell *) 45 57 let tree = 46 58 Eio.Fiber.fork_promise ~sw (fun _ -> 47 - jj_no_log ~snapshot:false [ "log" ] |> colored_string) 59 + jj_no_log ~snapshot:false [ "log"; "-r"; rev ] |> colored_string) 48 60 (* TODO: stop using dop last twice *) 49 61 and branches = 50 62 Eio.Fiber.fork_promise ~sw (fun _ -> 51 63 jj_no_log ~snapshot:false [ "branch"; "list"; "-a" ] |> colored_string) 52 - and files_list = Eio.Fiber.fork_promise ~sw (fun _ -> list_files ()) in 64 + and files_list = Eio.Fiber.fork_promise ~sw (fun _ -> list_files ~rev ()) in 53 65 (*wait for all our tasks*) 54 66 let tree = Eio.Promise.await_exn tree 55 67 and files_list = Eio.Promise.await_exn files_list
+4
jj_tui/bin/global_vars.ml
··· 22 22 ; jj_show : I.t Lwd.var 23 23 ; jj_branches : I.t Lwd.var 24 24 ; jj_change_files : (string * string) list Lwd.var 25 + ; selected_revision : string Lwd.var 26 + ; trigger_update : unit Lwd.var 25 27 } 26 28 27 29 (** Global variables for the ui. Here we keep anything that's just a pain to pipe around*) ··· 62 64 ; jj_show = Lwd.var I.empty 63 65 ; jj_branches = Lwd.var I.empty 64 66 ; jj_change_files = Lwd.var [] 67 + ; selected_revision = Lwd.var "@" 65 68 ; input = Lwd.var `Normal 66 69 ; show_popup = Lwd.var None 67 70 ; show_prompt = Lwd.var None 68 71 ; command_log = Lwd.var [] 72 + ; trigger_update = Lwd.var () 69 73 } 70 74 ;; 71 75
+95 -68
jj_tui/bin/graph_view.ml
··· 9 9 open! Jj_tui.Util 10 10 module Wd = Widgets 11 11 open Jj_commands.Make (Vars) 12 + open Jj_widgets.Make (Vars) 12 13 13 14 let rec command_mapping : command list = 14 15 [ ··· 22 23 ui_state.input $= `Mode (fun _ -> `Unhandled)) 23 24 } 24 25 ; { 25 - key = '5' 26 - ; description = "Show help2" 27 - ; cmd = 28 - SubCmd 29 - [ 30 - { 31 - key = '1' 32 - ; description = "Show help2" 33 - ; cmd = 34 - Fun 35 - (fun _ -> 36 - ui_state.show_popup 37 - $= Some (commands_list_ui command_mapping, "Help"); 38 - ui_state.input $= `Mode (fun _ -> `Unhandled)) 39 - } 40 - ] 41 - } 42 - ; { 43 26 key = 'P' 44 27 ; description = "Move the working copy to the previous child " 45 28 ; cmd = Cmd [ "prev" ] 46 29 } 47 30 ; { 48 - key = 'p' 49 - ; description = "Edit the previous child change" 50 - ; cmd = Cmd [ "prev"; "--edit" ] 51 - } 52 - ; { 53 31 key = 'N' 54 32 ; description = "Move the working copy to the next child " 55 33 ; cmd = Cmd [ "next" ] 56 34 } 57 35 ; { 58 36 key = 'n' 59 - ; description = "Edit the next child change" 60 - ; cmd = Cmd [ "next"; "--edit" ] 61 - } 62 - ; { 63 - key = 'i' 64 - ; cmd = 65 - SubCmd 66 - [ 67 - { key = 'i'; description = "Make a new empty change"; cmd = Cmd [ "new" ] } 68 - ; { 69 - key = 'a' 70 - ; description = "Insert a new empty change after a specific revision" 71 - ; cmd = Prompt ("New change after commit:", [ "new" ]) 72 - } 73 - ] 74 - ; description = "Make a new empty change" 37 + ; cmd = Dynamic_r (fun rev -> Cmd [ "new"; rev ]) 38 + ; description = "Make a new empty change as a child of the selected rev" 75 39 } 76 40 ; { 77 41 key = 'c' 78 - ; description = "Describe this change and move on (same as `describe` then `new`) " 42 + ; description = 43 + "Describe this change and start working on a new rev (same as `describe` then \ 44 + `new`) " 79 45 ; cmd = Prompt ("commit msg", [ "commit"; "-m" ]) 80 46 } 81 47 ; { 82 48 key = 'S' 83 49 ; description = "Split the current commit interacively" 84 - ; cmd = Cmd_I [ "split"; "-i" ] 50 + ; cmd = Dynamic_r (fun rev -> Cmd_I [ "split"; "-r"; rev; "-i" ]) 85 51 } 86 52 ; { 87 53 key = 's' ··· 91 57 [ 92 58 { 93 59 key = 'S' 94 - ; cmd = Cmd_I [ "unsquash"; "-i" ] 60 + ; cmd = Dynamic_r (fun rev -> Cmd_I [ "unsquash"; "-r"; rev; "-i" ]) 95 61 ; description = "Interactivaly unsquash" 96 62 } 97 63 ; { ··· 102 68 (fun _ -> 103 69 let curr_msg, prev_msg = get_messages () in 104 70 let new_msg = prev_msg ^ curr_msg in 105 - jj [ "squash"; "--quiet"; "-m"; new_msg ] |> ignore) 71 + let rev = Lwd.peek Vars.ui_state.selected_revision in 72 + jj [ "squash"; "--quiet"; "-r"; rev; "-m"; new_msg ] |> ignore) 106 73 } 107 74 ; { 108 75 key = 'S' ··· 113 80 , fun str -> 114 81 let curr_msg, prev_msg = get_messages () in 115 82 let new_msg = prev_msg ^ curr_msg in 116 - Cmd [ "squash"; "--quiet"; "-m"; new_msg; "--into"; str ] ) 83 + Cmd_r [ "squash"; "--quiet"; "-m"; new_msg; "--into"; str ] ) 117 84 } 118 85 ; { 119 86 key = 'i' 120 87 ; description = "Interactively choose what to squash into parent" 121 - ; cmd = Cmd_I [ "squash"; "-i" ] 88 + ; cmd = Dynamic_r (fun rev -> Cmd_I [ "squash"; "-r"; rev; "-i" ]) 122 89 } 123 90 ; { 124 91 key = 'I' 125 92 ; description = "Interactively choose what to squash into a commit" 126 - ; cmd = Prompt_I ("target revision", [ "squash"; "-i"; "--into" ]) 93 + ; cmd = 94 + Dynamic_r 95 + (fun rev -> 96 + Prompt_I 97 + ("target revision", [ "squash"; "-i"; "--from"; rev; "--into" ])) 127 98 } 128 99 ] 129 100 } 130 101 ; { 131 102 key = 'e' 132 - ; cmd = Prompt ("revision", [ "edit" ]) 133 - ; description = "Edit a particular revision" 103 + ; cmd = Dynamic_r (fun rev -> Cmd [ "edit"; rev ]) 104 + ; description = "Edit the selected revision" 134 105 } 135 106 ; { 136 107 key = 'd' 137 - ; cmd = Prompt ("description", [ "describe"; "-m" ]) 108 + ; cmd = 109 + Dynamic_r (fun rev -> Prompt ("description", [ "describe"; "-r"; rev; "-m" ])) 138 110 ; description = "Describe this revision" 139 111 } 140 112 ; { 141 113 key = 'R' 142 - ; cmd = Cmd_I [ "resolve" ] 114 + ; cmd = Dynamic_r (fun rev -> Cmd_I [ "resolve"; "-r"; rev ]) 143 115 ; description = "Resolve conflicts at this revision" 144 116 } 145 117 ; { ··· 150 122 [ 151 123 { 152 124 key = 'r' 153 - ; description = "Rebase single revision" 125 + ; description = "Rebase single revision " 154 126 ; cmd = 155 - Prompt ("destination for revision rebase", [ "rebase"; "-r"; "@"; "-d" ]) 127 + Dynamic_r 128 + (fun rev -> 129 + Prompt ("Dest rev for " ^ rev, [ "rebase"; "-r"; rev; "-d" ])) 156 130 } 157 131 ; { 158 132 key = 's' 159 133 ; description = "Rebase revision and its decendents" 160 134 ; cmd = 161 - Prompt 162 - ("Destination for decendent rebase", [ "rebase"; "-s"; "@"; "-d" ]) 135 + Dynamic_r 136 + (fun rev -> 137 + Prompt 138 + ( Printf.sprintf "Dest rev for %s and it's decendents" rev 139 + , [ "rebase"; "-s"; rev; "-d" ] )) 163 140 } 164 141 ; { 165 142 key = 'b' 166 143 ; description = "Rebase revision and all other revissions on its branch" 167 144 ; cmd = 168 - Prompt ("Destination for branch rebase", [ "rebase"; "-b"; "@"; "-d" ]) 145 + Dynamic_r 146 + (fun rev -> 147 + Prompt 148 + ( "Dest rev for branch including " ^ rev 149 + , [ "rebase"; "-b"; rev; "-d" ] )) 169 150 } 170 151 ] 171 152 } ··· 175 156 ; cmd = 176 157 SubCmd 177 158 [ 178 - { key = 'p'; description = "git push branch"; cmd = Cmd [ "git"; "push" ] } 159 + { key = 'p'; description = "git push"; cmd = Cmd [ "git"; "push" ] } 179 160 ; { key = 'f'; description = "git fetch"; cmd = Cmd [ "git"; "fetch" ] } 180 161 ] 181 162 } ··· 192 173 ; { 193 174 key = 'a' 194 175 ; description = "Abandon this change(removes just this change and rebases parents)" 195 - ; cmd = Cmd [ "abandon" ] |> confirm_prompt "abandon the change" 176 + ; cmd = 177 + Dynamic_r 178 + (fun rev -> 179 + Cmd_r [ "abandon" ] |> confirm_prompt ("abandon the revision:" ^ rev)) 196 180 } 197 181 ; { 198 182 key = 'b' ··· 207 191 PromptThen 208 192 ( "Branch names to create" 209 193 , fun x -> 210 - Cmd ([ "branch"; "create" ] @ (x |> String.split_on_char ' ')) ) 194 + Cmd_r ([ "branch"; "create" ] @ (x |> String.split_on_char ' ')) 195 + ) 211 196 } 212 197 ; { 213 198 key = 'd' ··· 226 211 ; { 227 212 key = 's' 228 213 ; description = "set branch to this change" 229 - ; cmd = Prompt ("Branch to set to this commit ", [ "branch"; "set"; "-B" ]) 214 + ; cmd = 215 + Dynamic_r 216 + (fun rev -> 217 + Prompt 218 + ( "Branch to set to this commit " 219 + , [ "branch"; "set"; "-r"; rev; "-B" ] )) 230 220 } 231 221 ; { 232 222 key = 't' ··· 243 233 ] 244 234 ;; 245 235 246 - (*Renders the commit graph from the UI state*) 247 - let graph_view = 248 - (*pad one on the left because it's hard to see the graph if it's too close*) 249 - Wd.scroll_area (ui_state.jj_tree $-> (I.pad ~l:1 >> Ui.atom)) 250 - |>$ Ui.keyboard_area (function 251 - | `ASCII k, [] -> 252 - handleInputs command_mapping k 253 - | _ -> 254 - `Unhandled) 236 + (*TODO:make a custom widget the renders the commit with and without selection. 237 + with selection replace the dot with a blue version and slightly blue tint the background *) 238 + let graph_view ~sw () = 239 + let ui = 240 + let$ graph, rev_ids = 241 + (*TODO I think this ads a slight delay to everything becasue it makes things need to be renedered twice. maybe I could try getting rid of it*) 242 + Vars.ui_state.trigger_update |> Lwd.get |> Lwd.map ~f:(fun _ -> seperate_revs ()) 243 + in 244 + let selectable_idx = ref 0 in 245 + graph 246 + |> Array.map (fun x -> 247 + match x with 248 + | `Selectable x -> 249 + let ui is_focused = 250 + (*hightlight blue when selection is true*) 251 + let prefix = 252 + if is_focused then I.char A.(bg A.blue) '>' 1 2 else I.char A.empty ' ' 1 2 253 + in 254 + I.hcat 255 + [ 256 + prefix 257 + ; x ^ "\n" 258 + (* TODO This won't work if we are on a branch, because that puts the @ further out*) 259 + |> Jj_tui.AnsiReverse.colored_string 260 + ] 261 + |> Ui.atom 262 + in 263 + let data = Wd.{ ui; data = rev_ids.(!selectable_idx) } in 264 + selectable_idx := !selectable_idx + 1; 265 + Wd.(Selectable data) 266 + | `Filler x -> 267 + Wd.(Filler (" " ^ x ^ "\n" |> Jj_tui.AnsiReverse.colored_string |> Ui.atom))) 268 + in 269 + ui 270 + |> Wd.selection_list_exclusions 271 + ~on_selection_change:(fun revision -> 272 + Eio.Fiber.fork ~sw @@ fun _ -> 273 + Vars.update_ui_state @@ fun _ -> 274 + Lwd.set Vars.ui_state.selected_revision revision; 275 + Global_funcs.update_views ()) 276 + ~custom_handler:(fun _ _ key -> 277 + match key with 278 + | `ASCII k, [] -> 279 + handleInputs command_mapping k 280 + | _ -> 281 + `Unhandled) 255 282 ;; 256 283 end
+53 -15
jj_tui/bin/jj_commands.ml
··· 9 9 (** Regular jj command *) 10 10 type command_variant = 11 11 | Cmd of cmd_args (** Regular jj command *) 12 + | Cmd_r of cmd_args (** Regular jj command that should operate on the selected revison *) 12 13 | Dynamic of (unit -> command_variant) 14 + | Dynamic_r of (string-> command_variant) 13 15 (** 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 *) 14 16 | Cmd_I of cmd_args 15 17 (** Command that will open interactively. Used for diff editing to hand control over to the jj process *) 16 18 | Prompt of string * cmd_args 19 + | Prompt_r of string * cmd_args 17 20 (** Creates a prompt and then runs the command with the prompt result appended as the last arg *) 18 21 | PromptThen of string * (string -> command_variant) 19 22 (** Same as prompt except you can run another command after. Useful if you want multiple prompts *) ··· 46 49 47 50 exception Handled 48 51 49 - let rec render_commands ?(sub_level = 0) commands = 50 - let indent = String.init (sub_level * 2) (fun _ -> ' ') in 51 - let line key desc = 52 - I.hcat 53 - [ 54 - I.string A.empty indent 55 - ; I.char (A.fg A.lightblue) key 1 1 56 - ; I.strf " " 57 - ; desc |> String.split_on_char '\n' |> List.map (I.string A.empty) |> I.vcat 58 - ] 59 - in 52 + let render_command_line ~indent_level key desc = 53 + let indent = String.init (indent_level * 2) (fun _ -> ' ') in 54 + I.hcat 55 + [ 56 + I.string A.empty indent 57 + ; I.uchars (A.fg A.lightblue) key 58 + ; I.strf " " 59 + ; desc |> String.split_on_char '\n' |> List.map (I.string A.empty) |> I.vcat 60 + ] 61 + ;; 62 + 63 + let rec render_commands ?(indent_level = 0) commands = 60 64 commands 61 65 |> List.concat_map @@ fun command -> 62 66 match command with 63 67 | { 64 68 key 65 69 ; description 66 - ; cmd = Cmd _ | Cmd_I _ | Prompt _ | Prompt_I _ | Fun _ | PromptThen _ | Dynamic _ 70 + ; cmd = Cmd _ | Cmd_I _ | Prompt _ | Prompt_I _ | Fun _ | PromptThen _ | Dynamic _ |Cmd_r _|Prompt_r _|Dynamic_r _ 67 71 } -> 68 - [ line key description ] 72 + [ render_command_line ~indent_level [| key |> Uchar.of_char |] description ] 69 73 | { key; description; cmd = SubCmd subs } -> 70 - line key description :: render_commands ~sub_level:(sub_level + 1) subs 74 + render_command_line ~indent_level [| key |> Uchar.of_char |] description 75 + :: render_commands ~indent_level:(indent_level + 1) subs 71 76 ;; 72 77 73 78 (*handle exception from jj*) ··· 88 93 let safe_jj f = try f () with JJError error -> handle_jj_error error 89 94 90 95 let commands_list_ui commands = 91 - commands |> render_commands |> I.vcat |> Ui.atom |> Lwd.pure |> Wd.scroll_area 96 + let move_command = 97 + render_command_line 98 + ~indent_level:0 99 + ("Alt+Arrows" |> String.to_seq |> Seq.map Uchar.of_char |> Array.of_seq) 100 + "navigation between windows" 101 + in 102 + (commands |> render_commands) @ [ move_command ] 103 + |> I.vcat 104 + |> Ui.atom 105 + |> Lwd.pure 106 + |> Wd.scroll_area 92 107 ;; 93 108 94 109 let rec handleCommand description cmd = ··· 129 144 ui_state.show_popup $= None; 130 145 noOut args; 131 146 raise Handled 147 + | Cmd_r args -> 148 + ui_state.show_popup $= None; 149 + noOut (args@["-r";Lwd.peek ui_state.selected_revision]); 150 + raise Handled 132 151 | Prompt (str, args) -> 133 152 ui_state.show_popup $= None; 134 153 prompt str (`Cmd args); 135 154 raise Handled 155 + | Prompt_r (str, args) -> 156 + ui_state.show_popup $= None; 157 + prompt str (`Cmd (args@["-r";Lwd.peek ui_state.selected_revision])); 158 + raise Handled 136 159 | PromptThen (label, next) -> 137 160 ui_state.show_popup $= None; 138 161 (*We run a prompt that then runs our next command when finished*) ··· 153 176 raise Handled 154 177 | Dynamic f -> 155 178 f () |> handleCommand description 179 + | Dynamic_r f -> 180 + f (Lwd.peek Vars.ui_state.selected_revision) |> handleCommand description 156 181 157 182 (** Try mapching the command mapping to the provided key and run the command if it matches *) 158 183 and command_input ~is_sub keymap key = ··· 193 218 open Intern (Vars) 194 219 module Wd = Jj_tui.Widgets 195 220 include Shared 221 + (** A handy command_list that just has this help command for areas that don't have any commands to still show help*) 222 + let rec default_list= 223 + [ 224 + { 225 + key = 'h' 226 + ; description = "Show help" 227 + ; cmd = 228 + Fun 229 + (fun _ -> 230 + ui_state.show_popup $= Some (commands_list_ui default_list, "Help"); 231 + ui_state.input $= `Mode (fun _ -> `Unhandled)) 232 + } 233 + ] 196 234 197 235 (**Generate a UI object with all the commands nicely formatted and layed out. Useful for help text*) 198 236 let commands_list_ui = commands_list_ui
+6 -1
jj_tui/bin/jj_ui.ml
··· 95 95 File_view.file_view sw () 96 96 |>$ Ui.resize ~w:5 ~sw:1 ~mw:1000 97 97 |> Wd.border_box_focusable ~focus:file_focus ~pad_h:0 ~pad_w:1 98 - ; Graph_view.graph_view 98 + ; Graph_view.graph_view ~sw () 99 99 |>$ Ui.resize ~sh:3 ~w:5 ~sw:1 ~mw:1000 ~h:10 ~mh:1000 100 100 |> Wd.border_box_focusable ~focus:graph_focus ~pad_h:0 ~pad_w:1 101 101 ; Wd.scroll_area (ui_state.jj_branches $-> Ui.atom) 102 102 |> Wd.is_focused ~focus:branch_focus (fun ui focused -> 103 103 ui 104 + |> Ui.keyboard_area (function 105 + | `ASCII k, [] -> 106 + Jj_commands.handleInputs Jj_commands.default_list k 107 + | _ -> 108 + `Unhandled) 104 109 |> Ui.resize 105 110 ~w:5 106 111 ~sw:1
+111 -65
jj_tui/bin/jj_widgets.ml
··· 12 12 open Vars 13 13 open Jj_process.Make (Vars) 14 14 15 - exception Found 15 + exception FoundStart 16 + exception FoundFiller 16 17 17 - let elieded_symbol = 18 - let a = String.get_utf_8_uchar "◌" 0 in 18 + let make_uchar str = 19 + let a = String.get_utf_8_uchar str 0 in 19 20 if a |> Uchar.utf_decode_is_valid 20 21 then a |> Uchar.utf_decode_uchar 21 22 else failwith "not a unicode string" 22 23 ;; 23 24 25 + let elieded_symbol = make_uchar "◌" 26 + let rev_symbol = make_uchar "◉" 27 + 28 + let is_whitespace_char (code_point : int) : bool = 29 + match code_point with 30 + | 0x0009 (* Tab *) 31 + | 0x000A (* Line Feed *) 32 + | 0x000B (* Vertical Tab *) 33 + | 0x000C (* Form Feed *) 34 + | 0x000D (* Carriage Return *) 35 + | 0x0020 (* Space *) 36 + | 0x0085 (* Next Line *) 37 + | 0x00A0 (* No-Break Space *) 38 + | 0x1680 (* Ogham Space Mark *) 39 + | 0x2000 (* En Quad *) 40 + | 0x2001 (* Em Quad *) 41 + | 0x2002 (* En Space *) 42 + | 0x2003 (* Em Space *) 43 + | 0x2004 (* Three-Per-Em Space *) 44 + | 0x2005 (* Four-Per-Em Space *) 45 + | 0x2006 (* Six-Per-Em Space *) 46 + | 0x2007 (* Figure Space *) 47 + | 0x2008 (* Punctuation Space *) 48 + | 0x2009 (* Thin Space *) 49 + | 0x200A (* Hair Space *) 50 + | 0x2028 (* Line Separator *) 51 + | 0x2029 (* Paragraph Separator *) 52 + | 0x202F (* Narrow No-Break Space *) 53 + | 0x205F (* Medium Mathematical Space *) 54 + | 0x3000 (* Ideographic Space *) -> 55 + true 56 + | _ -> 57 + false 58 + ;; 59 + 60 + let is_graph_start_char char = 61 + let i = Uchar.to_int char in 62 + (*chars like these: ├─╮*) 63 + let is_pipe = i > 0x2500 && i < 0x259f in 64 + let is_whitespace = is_whitespace_char i in 65 + is_pipe || is_whitespace 66 + ;; 67 + 24 68 let test_data = 25 69 {|◉ yzquvpvl eli.jambu@gmail.com 2024-05-23 15:04:24 3565237c 26 70 ├─╮ merger ··· 78 122 [] (* If list is empty or has only one element, return empty list *) 79 123 ;; 80 124 125 + (* 126 + 1. Make the graph 127 + *) 128 + let is_line_filler line = 129 + line 130 + (* We will iterate through skipping any chars like pipes and whitespace untill we find either: 131 + a) A rev start char,which would make the line a rev. 132 + b) Nothing, which would make the the line filler 133 + *) 134 + |> String.iteri (fun i char -> 135 + let uchar = String.get_utf_8_uchar line i |> Uchar.utf_decode_uchar in 136 + (*I've removed the part that tries to precisely skip all the start chars. this is becasue it gets all stuffed up by the terminal escape codes 137 + FIXME currently this will get stuffed up if a line has that rev symbol in it 138 + *) 139 + 140 + (* if not (uchar |> is_graph_start_char) *) 141 + (* then *) 142 + if uchar |> Uchar.equal rev_symbol || char == '@' 143 + then raise FoundStart 144 + (* else raise FoundFiller *) 145 + (* else () *) 146 + ) 147 + ;; 148 + 81 149 let seperate_revs () = 82 150 let graph = 83 151 jj_no_log [ "log" ] 84 152 |> String.split_on_char '\n' 85 153 (* filter out any lines that contain *) 86 - |> List.filter (fun x -> 154 + |> Base.List.fold ~init:([], None) ~f:(fun (new_list, last) x -> 87 155 if x |> String.length <= 1 88 - then false 156 + then `Filler x :: new_list, None 89 157 else ( 90 - try 91 - x 92 - |> String.iteri (fun i char -> 93 - let char = String.get_utf_8_uchar x i |> Uchar.utf_decode_uchar in 94 - if char |> Uchar.equal elieded_symbol then raise Found else ()); 95 - true 96 - with 97 - | _ -> 98 - false)) 99 - |> pairwise (fun (top, descr) -> top ^ "\n" ^ descr) ~f_last:(fun last -> [ last ]) 158 + match last with 159 + | Some last_line -> 160 + `Selectable (String.concat "\n" [ last_line; x ]) :: new_list, None 161 + | None -> 162 + (try 163 + is_line_filler x; 164 + `Filler x :: new_list, None 165 + with 166 + | FoundStart -> 167 + new_list, Some x 168 + | FoundFiller -> 169 + `Filler x :: new_list, None))) 170 + |> fst 171 + |> List.rev 172 + |> Array.of_list 100 173 in 101 174 let revs = 102 175 jj_no_log ~color:false [ "log"; "--no-graph"; "-T"; {|change_id++"\n"|} ] 103 176 |> String.split_on_char '\n' 104 177 |> List.filter (fun x -> x |> String.trim <> "") 178 + |> Array.of_list 105 179 in 106 - if List.length graph <> List.length revs 107 - then 108 - failwith 109 - @@ "When getting list of revs the graph had a different number of items to the \ 110 - revs. This shouldn't be possible and is a bug. " 111 - ^ Printf.sprintf "revs:%d graph:%d" (List.length revs) (List.length graph) 112 - ^ "\n" 113 - ^ String.concat "\n" revs 114 - ^ String.concat "\n" graph; 115 180 graph, revs 116 181 ;; 117 182 ··· 156 221 else string 157 222 ;; 158 223 159 - (*TODO:make a custom widget the renders the commit with and without selection. 160 - with selection replace the dot with a blue version and slightly blue tint the background *) 161 - let revs_list () = 162 - seperate_revs () 163 - |> fst 164 - |> List.map (fun x is_focused -> 165 - (*hightlight blue when selection is true*) 166 - let prefix = 167 - if is_focused then I.char A.(bg A.blue) '>' 1 2 else I.char A.empty ' ' 1 2 168 - in 169 - I.hcat 170 - [ 171 - prefix 172 - ; x ^ "\n" |> unsafe_blit_start_if "@" "◉" |> Jj_tui.AnsiReverse.colored_string 173 - ] 174 - |> Ui.atom) 175 - |> Lwd.pure 176 - |> selection_list 177 - ;; 178 224 179 - (*TODO:make a custom widget the renders the commit with and without selection. 180 - with selection replace the dot with a blue version and slightly blue tint the background *) 181 - let revs_list_filtered () = 182 - let graph_items, revs = seperate_revs () in 183 - graph_items 184 - |> List.map (fun x is_focused -> 185 - (* hightlight blue when selection is true *) 186 - let prefix = 187 - if is_focused then I.char A.(bg A.blue) '>' 1 2 else I.char A.empty ' ' 1 2 188 - in 189 - I.hcat 190 - [ 191 - prefix 192 - ; x ^ "\n" |> unsafe_blit_start_if "@" "◉" |> Jj_tui.AnsiReverse.colored_string 193 - ] 194 - |> Ui.atom) 195 - |> List.map2 (fun rev ui -> Wd.{ ui; data = rev }) revs 196 - |> Lwd.pure 197 - |> Wd.filterable_selection_list 198 - ~on_confirm:(fun _ -> ()) 199 - ~filter_predicate:(fun input rev -> rev |> String.starts_with ~prefix:input) 200 - ;; 225 + (* (*TODO:make a custom widget the renders the commit with and without selection. *) 226 + (* with selection replace the dot with a blue version and slightly blue tint the background *) *) 227 + (* let revs_list_filtered () = *) 228 + (* let graph_items, revs = seperate_revs () in *) 229 + (* graph_items *) 230 + (* |> List.map (fun x is_focused -> *) 231 + (* hightlight blue when selection is true *) 232 + (* let prefix = *) 233 + (* if is_focused then I.char A.(bg A.blue) '>' 1 2 else I.char A.empty ' ' 1 2 *) 234 + (* in *) 235 + (* I.hcat *) 236 + (* [ *) 237 + (* prefix *) 238 + (* ; x ^ "\n" |> unsafe_blit_start_if "@" "◉" |> Jj_tui.AnsiReverse.colored_string *) 239 + (* ] *) 240 + (* |> Ui.atom) *) 241 + (* |> List.map2 (fun rev ui -> Wd.{ ui; data = rev }) revs *) 242 + (* |> Lwd.pure *) 243 + (* |> Wd.filterable_selection_list *) 244 + (* ~on_confirm:(fun _ -> ()) *) 245 + (* ~filter_predicate:(fun input rev -> rev |> String.starts_with ~prefix:input) *) 246 + (* ;; *) 201 247 202 248 (** Start a process that will take full control of both stdin and stdout. 203 249 This is used for interactive diffs and such*)
-2
jj_tui/lib/widgets/Shared.ml
··· 1 - 2 1 open Notty 3 2 open Nottui 4 3 open Lwd_infix ··· 47 46 let node = Lwd.map2 ~f:(update focus) (Focus.status focus) start_state in 48 47 node 49 48 ;; 50 -
+137 -1
jj_tui/lib/widgets/selection_list.ml
··· 4 4 open! Util 5 5 open Shared 6 6 open Border_box 7 + 7 8 (**Selectable list item with a ui and some data *) 8 9 type 'a selectable_item = { 9 10 data : 'a ··· 50 51 List.nth_opt items selected |> Option.iter (fun x -> on_selection_change x.data); 51 52 items, selected 52 53 in 53 - (*Ui.vcat can be a little weird when the*) 54 + (* Ui.vcat can be a little weird when the *) 54 55 items 55 56 |> List.mapi (fun i x -> 56 57 if selected == i ··· 92 93 (*portion of the list that is behind the selection*) 93 94 let list_ratio = 94 95 ((selected |> float_of_int) +. offset) /. (length |> float_of_int) 96 + in 97 + (*if our position is further down the list than the portion that is shown we will shift by that amoumt *) 98 + Float.max (list_ratio -. size_ratio) 0.0 *. (size |> snd |> float_of_int) 99 + |> int_of_float 100 + in 101 + let$ items = render_items 102 + and$ shift_amount = shift_amount in 103 + items 104 + |> Ui.shift_area 0 shift_amount 105 + |> Ui.resize ~sh:1 106 + |> simpleSizeSensor ~size_var 107 + |> Ui.resize ~w:3 ~sw:1 ~h:0 108 + |> simpleSizeSensor ~size_var:rendered_size_var 109 + in 110 + scrollitems 111 + ;; 112 + 113 + type 'a maybeSelectable = 114 + | Selectable of 'a selectable_item 115 + | Filler of Ui.t 116 + 117 + (** Same as [selection_list_custom] except that it supports not all element in the list being selectable *) 118 + let selection_list_exclusions 119 + ?(focus = Focus.make ()) 120 + ?(on_selection_change = fun _ -> ()) 121 + ~custom_handler 122 + (items : 'a maybeSelectable array Lwd.t) 123 + = 124 + (* 125 + The rough overview is: 126 + 1. Make a lookup list that has the indexes of all the selectable items within the overall list, we will be selecting from those 127 + 2. Render the items, making sure to tell the selected one to render as selected. 128 + 3. Calculate how much we should scroll by. 129 + 4. offset by the scroll amount, apply size sensors and output final ui 130 + *) 131 + let selected_var = Lwd.var 0 in 132 + let selected_position = Lwd.var (0, 0) in 133 + let selectable_item_indexes = 134 + let$ items = items in 135 + let lut = Array.make (Array.length items) 0 in 136 + let final_len = 137 + items 138 + |> Base.Array.foldi ~init:0 ~f:(fun i selectable_count item -> 139 + match item with 140 + | Selectable _ -> 141 + Array.set lut selectable_count i; 142 + selectable_count + 1 143 + | Filler _ -> 144 + selectable_count) 145 + in 146 + Array.sub lut 0 final_len 147 + in 148 + (*handle selections*) 149 + let render_items = 150 + let$ focus = focus |> Focus.status 151 + and$ items, selected, selectable_item_indexes = 152 + (* This doesn't depend on changes in focus but it should update whenever there are new items or a selection change*) 153 + let$ items = items 154 + and$ selectable_item_indexes = selectable_item_indexes 155 + and$ selected = Lwd.get selected_var in 156 + (* First ensure if our list has gotten shorter we haven't selected off the list*) 157 + (* We do this here to ensure that the selected var is updated before we render to avoid double rendering*) 158 + let max_selected = Int.max 0 (Array.length selectable_item_indexes - 1) in 159 + if Int.min selected max_selected <> selected then selected_var $= max_selected; 160 + let selected = Lwd.peek selected_var in 161 + (* We lookup the index of the selected item in the list of items, remeber our list isn't entirely selectable items*) 162 + if Array.length selectable_item_indexes > 0 163 + then ( 164 + let item_idx = selectable_item_indexes.(selected) in 165 + items.(item_idx) |> fun x -> 166 + (match x with 167 + | Selectable s -> 168 + on_selection_change s.data 169 + | Filler _ -> 170 + failwith "selected an item that wasn't selectable. This shouldn't be possible"); 171 + items, item_idx, selectable_item_indexes) 172 + else items, 0, selectable_item_indexes 173 + in 174 + (* Ui.vcat can be a little weird when the *) 175 + items 176 + |> Array.mapi (fun i x -> 177 + match x with 178 + | Filler ui -> 179 + ui 180 + | Selectable x -> 181 + if selected == i 182 + then 183 + x.ui true 184 + |> Ui.transient_sensor (fun ~x ~y ~w:_ ~h:_ () -> 185 + if (x, y) <> Lwd.peek selected_position then selected_position $= (x, y)) 186 + else x.ui false) 187 + |> Array.to_list 188 + |> Ui.vcat 189 + |> Ui.keyboard_area ~focus (function 190 + | `Arrow `Up, [] -> 191 + let selected = max (Lwd.peek selected_var - 1) 0 in 192 + selected_var $= selected; 193 + `Handled 194 + | `Arrow `Down, [] -> 195 + let selected = 196 + Int.max 197 + (min 198 + (Lwd.peek selected_var + 1) 199 + ((selectable_item_indexes |> Array.length) - 1)) 200 + 0 201 + in 202 + selected_var $= selected; 203 + `Handled 204 + | a -> 205 + custom_handler items selected_var a) 206 + in 207 + let rendered_size_var = Lwd.var (0, 0) in 208 + (*Handle scrolling*) 209 + let scrollitems = 210 + let size_var = Lwd.var (0, 0) in 211 + let shift_amount = 212 + (*get the actual idx not just the selection number*) 213 + let$ selected_idx = 214 + Lwd.map2 215 + (Lwd.get selected_var) 216 + selectable_item_indexes 217 + ~f:(fun selected indexes -> 218 + if Array.length indexes > 0 then indexes.(selected) else 0) 219 + and$ size = Lwd.get size_var 220 + and$ length = items |>$ Array.length 221 + and$ ren_size = Lwd.get rendered_size_var in 222 + (*portion of the total size of the element that is rendered*) 223 + let size_ratio = 224 + (ren_size |> snd |> float_of_int) /. (size |> snd |> float_of_int) 225 + in 226 + (*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 )*) 227 + let offset = size_ratio *. ((length |> float_of_int) /. 2.9) in 228 + (*portion of the list that is behind the selection*) 229 + let list_ratio = 230 + ((selected_idx |> float_of_int) +. offset) /. (length |> float_of_int) 95 231 in 96 232 (*if our position is further down the list than the portion that is shown we will shift by that amoumt *) 97 233 Float.max (list_ratio -. size_ratio) 0.0 *. (size |> snd |> float_of_int)
+2 -2
jj_tui/lib/widgets/widgets.ml
··· 305 305 match show_popup with 306 306 | Some (content, label) -> 307 307 let prompt_field = content in 308 - prompt_field |>$ Ui.resize ~w:5 |> border_box ~label_top:label|> clear_bg 308 + prompt_field |>$ Ui.resize ~w:5 |> border_box ~label_top:label |> clear_bg 309 309 | None -> 310 310 Ui.empty |> Lwd.pure 311 311 in ··· 323 323 ;; 324 324 325 325 let is_focused ~focus f ui = 326 - Lwd.map2 ui (focus |> Focus.status) ~f:(fun ui focus -> f ui (focus |> Focus.has_focus) ) 326 + Lwd.map2 ui (focus |> Focus.status) ~f:(fun ui focus -> f ui (focus |> Focus.has_focus)) 327 327 ;;
-1
jj_tui/widget-test/main.ml
··· 261 261 `Unhandled) 262 262 in 263 263 let outerFocus = Focus.make () in 264 - 265 264 (*We wrap the button in some more UI*) 266 265 let$ outer = W.vbox [ button "I'm a button"; Lwd.get output |>$ W.string ] 267 266 and$ focus = Focus.status outerFocus in