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.

big file changes

+941 -715
+94
jj_tui/bin/file_commands.ml
··· 1 + open Jj_tui.Logging 2 + 3 + module Make (Vars : Global_vars.Vars) = struct 4 + open Lwd_infix 5 + open Vars 6 + open Jj_process.Make (Vars) 7 + open Notty 8 + open Nottui 9 + open! Jj_tui.Util 10 + open Jj_commands.Make (Vars) 11 + open Jj_commands.Shared 12 + open Global_vars 13 + open Jj_tui 14 + 15 + (* Define all file commands *) 16 + let get_command_registry active_files get_commands = [ 17 + { 18 + id = "show_help"; 19 + description = "Show help"; 20 + make_cmd = (fun () -> Fun (fun _ -> 21 + ui_state.show_popup 22 + $= Some (commands_list_ui ~include_arrows:true (get_commands()), "Help"); 23 + ui_state.input $= `Mode (fun _ -> `Unhandled))) 24 + }; 25 + { 26 + id = "move_to_rev"; 27 + description = "Move file to other commit"; 28 + make_cmd = (fun () -> 29 + PromptThen ( 30 + "Revision to move file to", 31 + fun rev -> 32 + Cmd ( 33 + ["squash"; "-u"; "--keep-emptied"; "--from"; get_hovered_rev (); "--into"; rev] 34 + @ Lwd.peek active_files 35 + ) 36 + ) 37 + ) 38 + }; 39 + { 40 + id = "move_to_child"; 41 + description = "Move file to child commit"; 42 + make_cmd = (fun () -> 43 + Dynamic_r (fun rev -> 44 + Cmd ( 45 + ["squash"; "-u"; "--keep-emptied"; "--from"; rev; "--into"; rev ^ "+"] 46 + @ Lwd.peek active_files 47 + ) 48 + ) 49 + ) 50 + }; 51 + { 52 + id = "move_to_parent"; 53 + description = "Move file to parent commit"; 54 + make_cmd = (fun () -> 55 + Dynamic_r (fun rev -> 56 + Cmd ( 57 + ["squash"; "-u"; "--keep-emptied"; "--from"; rev; "--into"; rev ^ "-"] 58 + @ Lwd.peek active_files 59 + ) 60 + ) 61 + ) 62 + }; 63 + { 64 + id = "discard"; 65 + description = "Restore to previous revision (git discard)"; 66 + make_cmd = (fun () -> 67 + Dynamic_r (fun rev -> 68 + let selected = Lwd.peek active_files in 69 + confirm_prompt 70 + ("discard all changes to:\n" 71 + ^ (selected |> String.concat "\n") 72 + ^ "\nin rev " 73 + ^ rev) 74 + (Cmd (["restore"; "--to"; rev; "--from"; rev ^ "-"] @ selected)) 75 + ) 76 + ) 77 + }; 78 + { 79 + id = "absorb"; 80 + description = "Absorb changes from index to working copy"; 81 + make_cmd = (fun () -> 82 + Dynamic_r (fun rev -> 83 + let selected = Lwd.peek active_files in 84 + confirm_prompt 85 + ("absorb all changes to:\n" 86 + ^ (selected |> String.concat "\n") 87 + ^ "\nin rev " 88 + ^ rev) 89 + (Cmd (["absorb"; "--from";rev] @ selected)) 90 + ) 91 + ) 92 + }; 93 + ]|>List.to_seq|>Seq.map (fun x -> x.id,x)|>Hashtbl.of_seq 94 + end
+12 -70
jj_tui/bin/file_view.ml
··· 10 10 open Jj_tui 11 11 open Picos_std_structured 12 12 13 + (* Import file commands *) 14 + module FileCommands = File_commands.Make (Vars) 15 + 13 16 open Jj_tui.Key_map 14 17 let active_files = Lwd.var [ "" ] 15 18 16 - let rec make_command_mapping (key_map: Key_map.file_keys) = 17 - [ 18 - { 19 - key = key_map.show_help 20 - ; description = "Show help" 21 - ; cmd = 22 - Fun 23 - (fun _ -> 24 - ui_state.show_popup 25 - $= Some (commands_list_ui ~include_arrows:true (get_command_mapping ()), "Help"); 26 - ui_state.input $= `Mode (fun _ -> `Unhandled)) 27 - } 28 - ; { 29 - key = key_map.move_to_rev 30 - ; description = "Move file to other commit" 31 - ; cmd = 32 - PromptThen 33 - ( "Revision to move file to" 34 - , fun rev -> 35 - Cmd 36 - ([ 37 - "squash" 38 - ; "-u" 39 - ; "--keep-emptied" 40 - ; "--from" 41 - ; get_hovered_rev () 42 - ; "--into" 43 - ; rev 44 - ] 45 - @ Lwd.peek active_files) ) 46 - } 47 - ; { 48 - key = key_map.move_to_child 49 - ; description = "Move file to child commit" 50 - ; cmd = 51 - Dynamic_r 52 - (fun rev -> 53 - Cmd 54 - ([ "squash"; "-u"; "--keep-emptied"; "--from"; rev; "--into"; rev ^ "+" ] 55 - @ Lwd.peek active_files)) 56 - } 57 - ; { 58 - key = key_map.move_to_parent 59 - ; description = "Move file to parent commit" 60 - ; cmd = 61 - Dynamic_r 62 - (fun rev -> 63 - Cmd 64 - ([ "squash"; "-u"; "--keep-emptied"; "--from"; rev; "--into"; rev ^ "-" ] 65 - @ Lwd.peek active_files)) 66 - } 67 - ; { 68 - key = key_map.discard 69 - ; description = "Restore to previous revision (git discard)" 70 - ; cmd = 71 - Dynamic_r 72 - (fun rev -> 73 - let selected = Lwd.peek active_files in 74 - confirm_prompt 75 - ("discard all changes to:\n" 76 - ^ (selected |> String.concat "\n") 77 - ^ "\nin rev " 78 - ^ rev) 79 - (Cmd ([ "restore"; "--to"; rev; "--from"; rev ^ "-" ] @ selected))) 80 - } 81 - ] 82 - and command_mapping = ref None 83 - and get_command_mapping () = 19 + (* Remove the hardcoded make_command_mapping function and use the dynamic one *) 20 + let command_mapping = ref None 21 + 22 + let rec get_command_mapping () = 84 23 match !command_mapping with 85 24 | Some mapping -> mapping 86 - | None -> 87 - let mapping = make_command_mapping (Lwd.peek ui_state.config).key_map.file in 25 + | None -> 26 + let key_map = (Lwd.peek ui_state.config).key_map.file in 27 + let registry = FileCommands.get_command_registry active_files get_command_mapping in 28 + let mapping = build_command_list key_map registry in 88 29 command_mapping := Some mapping; 89 30 mapping 90 31 ;; 32 + 91 33 let hovered_var = ref "./" 92 34 93 35 let file_view ~focus summary_focus =
+412
jj_tui/bin/graph_commands.ml
··· 1 + open Jj_tui.Logging 2 + 3 + module Make (Vars : Global_vars.Vars) = struct 4 + open Lwd_infix 5 + open Vars 6 + open Notty 7 + open Jj_tui 8 + open Nottui 9 + open! Jj_tui.Util 10 + open Jj_commands.Shared 11 + open Jj_commands.Make (Vars) 12 + open Jj_widgets.Make (Vars) 13 + module Process = Jj_process.Make (Vars) 14 + open Process 15 + open Jj_tui.Process_wrappers.Make (Process) 16 + 17 + (* Helper functions from graph_view *) 18 + let bookmark_select_prompt get_bookmark_list name func = 19 + Selection_prompt 20 + ( name 21 + , (fun () -> get_bookmark_list () |> Lwd.pure) 22 + , (fun x bookmark_name -> bookmark_name |> Base.String.is_substring ~substring:x) 23 + , func ) 24 + ;; 25 + 26 + let custom_commit ?(edit = true) msg = 27 + let rev = Vars.get_hovered_rev () in 28 + jj [ "describe"; "-r"; rev; "-m"; msg ] |> ignore; 29 + (jj @@ [ "new"; "--insert-after"; rev ] @ if edit then [] else [ "--no-edit" ]) 30 + |> ignore 31 + ;; 32 + 33 + (* Define all graph commands *) 34 + let get_command_registry get_commands = 35 + [ 36 + { 37 + id = "show_help" 38 + ; description = "Show help" 39 + ; make_cmd = 40 + (fun () -> 41 + Fun 42 + (fun _ -> 43 + ui_state.show_popup 44 + $= Some (commands_list_ui ~include_arrows:true (get_commands ()), "Help"); 45 + ui_state.input $= `Mode (fun _ -> `Unhandled))) 46 + } 47 + ; { 48 + id = "prev" 49 + ; description = "Move the working copy to the previous child" 50 + ; make_cmd = (fun () -> Cmd [ "prev" ]) 51 + } 52 + ; { 53 + id = "new_base" 54 + ; description = "Make new child commit" 55 + ; make_cmd = (fun () -> Cmd_with_revs (Active [ "new" ])) 56 + } 57 + ; { 58 + id = "new_no_edit" 59 + ; description = "Same as 'new', but without editing the new commit" 60 + ; make_cmd = (fun () -> Cmd_with_revs (Active [ "new"; "--no-edit" ])) 61 + } 62 + ; { 63 + id = "new_inline" 64 + ; description = "Make a new change and insert it after the selected rev" 65 + ; make_cmd = 66 + (fun () -> 67 + Dynamic 68 + (fun () -> Cmd ([ "new"; "--insert-after" ] @ Vars.get_active_revs ()))) 69 + } 70 + ; { 71 + id = "new_inline_no_edit" 72 + ; description = "Same as 'new insert', but without editing the new commit" 73 + ; make_cmd = 74 + (fun () -> 75 + Dynamic 76 + (fun () -> 77 + Cmd ([ "new"; "--no-edit"; "--insert-after" ] @ Vars.get_active_revs ()))) 78 + } 79 + ; { 80 + id = "duplicate" 81 + ; description = "Duplicate the current selected commits " 82 + ; make_cmd = 83 + (fun () -> Dynamic (fun () -> Cmd ([ "duplicate" ] @ Vars.get_active_revs ()))) 84 + } 85 + ; { 86 + id = "undo" 87 + ; description = "Undo the last operation" 88 + ; make_cmd = (fun () -> Cmd [ "undo" ]) 89 + } 90 + ; { 91 + id = "commit_base" 92 + ; description = 93 + "Describe this change and start working on a new rev (same as `describe` then \ 94 + `new`)" 95 + ; make_cmd = 96 + (fun () -> 97 + PromptThen ("commit msg", fun msg -> Fun (fun () -> custom_commit msg))) 98 + } 99 + ; { 100 + id = "commit_no_edit" 101 + ; description = "Same as commit but without editing the new commit" 102 + ; make_cmd = 103 + (fun () -> 104 + PromptThen 105 + ("commit msg", fun msg -> Fun (fun () -> custom_commit ~edit:false msg))) 106 + } 107 + ; { 108 + id = "split" 109 + ; description = "Split the current commit interacively" 110 + ; make_cmd = (fun () -> Dynamic_r (fun rev -> Cmd_I [ "split"; "-r"; rev; "-i" ])) 111 + } 112 + ; { 113 + id = "squash_into_parent" 114 + ; description = "Squash into parent" 115 + ; make_cmd = 116 + (fun () -> 117 + Fun 118 + (fun _ -> 119 + let rev = Vars.get_hovered_rev () in 120 + let source_msg, dest_msg = get_messages rev (rev ^ "-") in 121 + let new_msg = [ dest_msg; source_msg ] |> String.concat_non_empty "\n" in 122 + jj [ "squash"; "--quiet"; "-r"; rev; "-m"; new_msg ] |> ignore)) 123 + } 124 + ; { 125 + id = "squash_into_rev" 126 + ; description = "Squash into any commit" 127 + ; make_cmd = 128 + (fun () -> 129 + PromptThen 130 + ( "target revision" 131 + , fun target -> 132 + Dynamic_r 133 + (fun rev -> 134 + let src_msg, dest_msg = get_messages rev target in 135 + let new_msg = 136 + [ dest_msg; src_msg ] |> String.concat_non_empty "\n" 137 + in 138 + Cmd 139 + [ 140 + "squash" 141 + ; "--quiet" 142 + ; "-m" 143 + ; new_msg 144 + ; "--from" 145 + ; rev 146 + ; "--into" 147 + ; target 148 + ]) )) 149 + } 150 + ; { 151 + id = "squash_unsquash" 152 + ; description = "Interactivaly unsquash" 153 + ; make_cmd = 154 + (fun () -> Dynamic_r (fun rev -> Cmd_I [ "unsquash"; "-r"; rev; "-i" ])) 155 + } 156 + ; { 157 + id = "squash_interactive_parent" 158 + ; description = "Interactively choose what to squash into parent" 159 + ; make_cmd = (fun () -> Dynamic_r (fun rev -> Cmd_I [ "squash"; "-r"; rev; "-i" ])) 160 + } 161 + ; { 162 + id = "squash_interactive_rev" 163 + ; description = "Interactively choose what to squash into a commit" 164 + ; make_cmd = 165 + (fun () -> 166 + Dynamic_r 167 + (fun rev -> 168 + Prompt_I ("target revision", [ "squash"; "-i"; "--from"; rev; "--into" ]))) 169 + } 170 + ; { 171 + id = "edit" 172 + ; description = "Edit the selected revision" 173 + ; make_cmd = (fun () -> Dynamic_r (fun rev -> Cmd [ "edit"; rev ])) 174 + } 175 + ; { 176 + id = "describe" 177 + ; description = "Describe this revision" 178 + ; make_cmd = 179 + (fun () -> 180 + Dynamic_r (fun rev -> Prompt ("description", [ "describe"; "-r"; rev; "-m" ]))) 181 + } 182 + ; { 183 + id = "describe_editor" 184 + ; description = "Describe this revision using an editor" 185 + ; make_cmd = (fun () -> Dynamic_r (fun rev -> Cmd_I [ "describe"; "-r"; rev ])) 186 + } 187 + ; { 188 + id = "resolve" 189 + ; description = "Resolve conflicts at this revision" 190 + ; make_cmd = (fun () -> Dynamic_r (fun rev -> Cmd_I [ "resolve"; "-r"; rev ])) 191 + } 192 + ; { 193 + id = "rebase_single" 194 + ; description = "Rebase single revision " 195 + ; make_cmd = 196 + (fun () -> 197 + Dynamic_r 198 + (fun rev -> Prompt ("Dest rev for " ^ rev, [ "rebase"; "-r"; rev; "-d" ]))) 199 + } 200 + ; { 201 + id = "rebase_with_descendants" 202 + ; description = "Rebase revision and its decendents" 203 + ; make_cmd = 204 + (fun () -> 205 + Dynamic_r 206 + (fun rev -> 207 + Prompt 208 + ( Printf.sprintf "Dest rev for %s and it's decendents" rev 209 + , [ "rebase"; "-s"; rev; "-d" ] ))) 210 + } 211 + ; { 212 + id = "rebase_with_bookmark" 213 + ; description = "Rebase revision and all other revissions on its bookmark" 214 + ; make_cmd = 215 + (fun () -> 216 + Dynamic_r 217 + (fun rev -> 218 + Prompt 219 + ("Dest rev for bookmark including " ^ rev, [ "rebase"; "-b"; rev; "-d" ]))) 220 + } 221 + ; { 222 + id = "git_push" 223 + ; description = "git push" 224 + ; make_cmd = 225 + (fun () -> 226 + Fun 227 + (fun _ -> 228 + let revs = Vars.get_active_revs () in 229 + let subcmds = 230 + [ 231 + { 232 + key = Key.key_of_string_exn "y" 233 + ; description = "proceed" 234 + ; cmd = 235 + Cmd 236 + ([ "git"; "push"; "--allow-new" ] 237 + @ (revs |> List.concat_map (fun x -> [ "-r"; x ]))) 238 + } 239 + ; { 240 + key = Key.key_of_string_exn "n" 241 + ; description = "exit" 242 + ; cmd = 243 + Fun 244 + (fun _ -> 245 + ui_state.input $= `Normal; 246 + ui_state.show_popup $= None) 247 + } 248 + ] 249 + |> List.map (fun x -> x.key, x) 250 + |> Key_map.Key_Map.of_list 251 + in 252 + let log = 253 + jj_no_log 254 + ~get_stderr:true 255 + ([ "git"; "push"; "--allow-new"; "--dry-run" ] 256 + @ (revs |> List.concat_map (fun x -> [ "-r"; x ]))) 257 + |> AnsiReverse.colored_string 258 + |> Ui.atom 259 + |> Lwd.pure 260 + in 261 + let ui = W.vbox [ log; commands_list_ui subcmds ] in 262 + ui_state.show_popup $= Some (ui, "Git push will:"); 263 + ui_state.input $= `Mode (command_input ~is_sub:true subcmds))) 264 + } 265 + ; { 266 + id = "git_fetch" 267 + ; description = "git fetch" 268 + ; make_cmd = (fun () -> Cmd [ "git"; "fetch" ]) 269 + } 270 + ; { 271 + id = "git_fetch_all" 272 + ; description = "git fetch all remotes" 273 + ; make_cmd = (fun () -> Cmd [ "git"; "fetch"; "--all-remotes" ]) 274 + } 275 + ; { 276 + id = "parallelize" 277 + ; description = 278 + "Parallelize commits. Takes 2 commits and makes them have the\n\ 279 + same parent and child. Run `jj parallelize` --help for details" 280 + ; make_cmd = 281 + (fun () -> 282 + PromptThen 283 + ( "list commits to parallelize" 284 + , fun x -> Cmd ([ "paralellize" ] @ (x |> String.split_on_char ' ')) )) 285 + } 286 + ; { 287 + id = "abandon" 288 + ; description = "Abandon this change(removes just this change and rebases parents)" 289 + ; make_cmd = 290 + (fun () -> 291 + Dynamic 292 + (fun () -> 293 + let revs = Vars.get_active_revs () in 294 + Cmd ([ "abandon" ] @ revs) 295 + |> confirm_prompt 296 + ("abandon the revisions:\n" ^ (revs |> String.concat "\n")))) 297 + } 298 + ; { 299 + id = "bookmark_create" 300 + ; description = "Create new bookmark" 301 + ; make_cmd = 302 + (fun () -> 303 + PromptThen 304 + ( "Bookmark name to create" 305 + , fun x -> 306 + Cmd_r 307 + ([ "bookmark"; "create" ] 308 + @ [ x |> String.map (fun c -> if c = ' ' then '_' else c) ]) )) 309 + } 310 + ; { 311 + id = "bookmark_delete" 312 + ; description = "Delete bookmark" 313 + ; make_cmd = 314 + (fun () -> 315 + bookmark_select_prompt 316 + branches_no_remote 317 + "Bookmark to delete" 318 + (fun bookmark -> 319 + Cmd [ "bookmark"; "delete"; bookmark ] 320 + |> confirm_prompt 321 + (Printf.sprintf 322 + "delete the bookmark: '%s' This will also delete it on the \ 323 + remote next \"git push\"." 324 + bookmark))) 325 + } 326 + ; { 327 + id = "bookmark_forget" 328 + ; description = "Forget bookmark" 329 + ; make_cmd = 330 + (fun () -> 331 + bookmark_select_prompt 332 + branches_no_remote 333 + "Bookmark to forget" 334 + (fun bookmark -> 335 + Cmd [ "bookmark"; "forget"; bookmark ] 336 + |> confirm_prompt 337 + (Printf.sprintf 338 + "forget the bookmark: '%s' . This will not delete it on the \ 339 + remote." 340 + bookmark))) 341 + } 342 + ; { 343 + id = "bookmark_rename" 344 + ; description = "Rename bookmark" 345 + ; make_cmd = 346 + (fun () -> 347 + bookmark_select_prompt 348 + branches_no_remote 349 + "Select the bookmark to rename (only local/tracked bookmarks are shown)" 350 + (fun curr_name -> 351 + Prompt ("New bookmark name", [ "bookmark"; "rename"; curr_name ]))) 352 + } 353 + ; { 354 + id = "bookmark_set" 355 + ; description = "Set bookmark to this change" 356 + ; make_cmd = 357 + (fun () -> 358 + Dynamic_r 359 + (fun rev -> 360 + bookmark_select_prompt 361 + branches_no_remote 362 + ("Select the bookmark to set to rev: " ^ rev) 363 + (fun bookmark -> 364 + Cmd [ "bookmark"; "set"; "-r"; get_hovered_rev (); "-B"; bookmark ]))) 365 + } 366 + ; { 367 + id = "bookmark_track" 368 + ; description = "track given remote bookmark" 369 + ; make_cmd = 370 + (fun () -> 371 + bookmark_select_prompt 372 + branches_remotes_not_tracked 373 + "Select the bookmark to begin tracking" 374 + (fun bookmark -> Cmd [ "bookmark"; "track"; bookmark ])) 375 + } 376 + ; { 377 + id = "bookmark_untrack" 378 + ; description = "untrack given remote bookmark" 379 + ; make_cmd = 380 + (fun () -> 381 + bookmark_select_prompt 382 + branches_remotes_tracked 383 + "Select the bookmark to untrack" 384 + (fun bookmark -> Cmd [ "bookmark"; "untrack"; bookmark ])) 385 + } 386 + ; { 387 + id = "filter" 388 + ; description = "Filter using revset" 389 + ; make_cmd = 390 + (fun () -> 391 + PromptThen 392 + ( "Filter using revset" 393 + , fun revset -> 394 + Fun 395 + (fun () -> 396 + if revset = "" 397 + then Vars.ui_state.revset $= None 398 + else Vars.ui_state.revset $= Some revset) )) 399 + } 400 + ; { 401 + id = "absorb" 402 + ; description = 403 + "Absorb: Move changes of each file in this commit into the closest mutable \ 404 + parent that modified that file" 405 + ; make_cmd = (fun () -> Cmd_r [ "absorb"; "--from"; ]) 406 + } 407 + ] 408 + |> List.to_seq 409 + |> Seq.map (fun x -> x.id, x) 410 + |> Hashtbl.of_seq 411 + ;; 412 + end
+14 -413
jj_tui/bin/graph_view.ml
··· 12 12 module Process = Jj_process.Make (Vars) 13 13 open Process 14 14 open Jj_tui.Process_wrappers.Make (Process) 15 - 15 + 16 + (* Import graph commands *) 17 + module GraphCommands = Graph_commands.Make (Vars) 18 + 16 19 let bookmark_select_prompt get_bookmark_list name func = 17 20 Selection_prompt 18 21 ( name ··· 28 31 |> ignore 29 32 ;; 30 33 31 - let rec make_command_mapping (key_map_base : Key_map.t) : 'acommand list = 32 - let key_map = key_map_base.graph in 33 - [ 34 - { 35 - key = key_map.show_help 36 - ; description = "Show help" 37 - ; cmd = 38 - Fun 39 - (fun _ -> 40 - ui_state.show_popup 41 - $= Some 42 - (commands_list_ui ~include_arrows:true (get_command_mapping ()), "Help"); 43 - ui_state.input $= `Mode (fun _ -> `Unhandled)) 44 - } 45 - ; { 46 - key = key_map.prev 47 - ; description = "Move the working copy to the previous child " 48 - ; cmd = Cmd [ "prev" ] 49 - } 50 - ; { 51 - key = key_map.new_child.menu 52 - ; description = "Make a new change" 53 - ; cmd = 54 - SubCmd 55 - [ 56 - { 57 - key = key_map.new_child.base 58 - ; cmd = Cmd_with_revs (Active [ "new" ]) 59 - ; description = "Make new child commit" 60 - } 61 - ; { 62 - key = key_map.new_child.no_edit 63 - ; cmd = Cmd_with_revs (Active [ "new"; "--no-edit" ]) 64 - ; description = "Same as 'new', but without editing the new commit" 65 - } 66 - ; { 67 - key = key_map.new_child.inline 68 - ; description = "Make a new change and insert it after the selected rev" 69 - ; cmd = 70 - Dynamic 71 - (fun () -> 72 - Cmd ([ "new"; "--insert-after" ] @ Vars.get_active_revs ())) 73 - } 74 - ; { 75 - key = key_map.new_child.inline_no_edit 76 - ; description = "Same as 'new insert', but without editing the new commit" 77 - ; cmd = 78 - Dynamic 79 - (fun () -> 80 - Cmd 81 - ([ "new"; "--no-edit"; "--insert-after" ] 82 - @ Vars.get_active_revs ())) 83 - } 84 - ] 85 - } 86 - ; { 87 - key = key_map.duplicate 88 - ; description = "Duplicate the current selected commits " 89 - ; cmd = Dynamic (fun () -> Cmd ([ "duplicate" ] @ Vars.get_active_revs ())) 90 - } 91 - ; { 92 - key = key_map.undo 93 - ; description = "Undo the last operation" 94 - ; cmd = Cmd [ "undo" ] 95 - } 96 - ; { 97 - key = key_map.commit.menu 98 - ; description = "Commit" 99 - ; cmd = 100 - SubCmd 101 - [ 102 - { 103 - key = key_map.commit.base 104 - ; description = 105 - "Describe this change and start working on a new rev (same as \ 106 - `describe` then `new`)" 107 - ; cmd = 108 - PromptThen ("commit msg", fun msg -> Fun (fun () -> custom_commit msg)) 109 - } 110 - ; { 111 - key = key_map.commit.no_edit 112 - ; description = "Same as commit but without editing the new commit" 113 - ; cmd = 114 - PromptThen 115 - ( "commit msg" 116 - , fun msg -> Fun (fun () -> custom_commit ~edit:false msg) ) 117 - } 118 - ] 119 - } 120 - ; { 121 - key = key_map.split 122 - ; description = "Split the current commit interacively" 123 - ; cmd = Dynamic_r (fun rev -> Cmd_I [ "split"; "-r"; rev; "-i" ]) 124 - } 125 - ; { 126 - key = key_map.squash.menu 127 - ; description = "Squash/unsquash" 128 - ; cmd = 129 - SubCmd 130 - [ 131 - { 132 - key = key_map.squash.into_parent 133 - ; description = "Squash into parent" 134 - ; cmd = 135 - Fun 136 - (fun _ -> 137 - let rev = Vars.get_hovered_rev () in 138 - let source_msg, dest_msg = get_messages rev (rev ^ "-") in 139 - let new_msg = 140 - [ dest_msg; source_msg ] |> String.concat_non_empty "\n" 141 - in 142 - jj [ "squash"; "--quiet"; "-r"; rev; "-m"; new_msg ] |> ignore) 143 - } 144 - ; { 145 - key = key_map.squash.into_rev 146 - ; description = "Squash into any commit" 147 - ; cmd = 148 - PromptThen 149 - ( "target revision" 150 - , fun target -> 151 - Dynamic_r 152 - (fun rev -> 153 - let src_msg, dest_msg = get_messages rev target in 154 - let new_msg = 155 - [ dest_msg; src_msg ] |> String.concat_non_empty "\n" 156 - in 157 - Cmd 158 - [ 159 - "squash" 160 - ; "--quiet" 161 - ; "-m" 162 - ; new_msg 163 - ; "--from" 164 - ; rev 165 - ; "--into" 166 - ; target 167 - ]) ) 168 - } 169 - ; { 170 - key = key_map.squash.unsquash 171 - ; cmd = Dynamic_r (fun rev -> Cmd_I [ "unsquash"; "-r"; rev; "-i" ]) 172 - ; description = "Interactivaly unsquash" 173 - } 174 - ; { 175 - key = key_map.squash.interactive_parent 176 - ; description = "Interactively choose what to squash into parent" 177 - ; cmd = Dynamic_r (fun rev -> Cmd_I [ "squash"; "-r"; rev; "-i" ]) 178 - } 179 - ; { 180 - key = key_map.squash.interactive_rev 181 - ; description = "Interactively choose what to squash into a commit" 182 - ; cmd = 183 - Dynamic_r 184 - (fun rev -> 185 - Prompt_I 186 - ("target revision", [ "squash"; "-i"; "--from"; rev; "--into" ])) 187 - } 188 - ] 189 - } 190 - ; { 191 - key = key_map.edit 192 - ; cmd = Dynamic_r (fun rev -> Cmd [ "edit"; rev ]) 193 - ; description = "Edit the selected revision" 194 - } 195 - ; { 196 - key = key_map.describe 197 - ; cmd = 198 - Dynamic_r (fun rev -> Prompt ("description", [ "describe"; "-r"; rev; "-m" ])) 199 - ; description = "Describe this revision" 200 - } 201 - ; { 202 - key = key_map.describe_editor 203 - ; cmd = Dynamic_r (fun rev -> Cmd_I [ "describe"; "-r"; rev ]) 204 - ; description = "Describe this revision using an editor" 205 - } 206 - ; { 207 - key = key_map.resolve 208 - ; cmd = Dynamic_r (fun rev -> Cmd_I [ "resolve"; "-r"; rev ]) 209 - ; description = "Resolve conflicts at this revision" 210 - } 211 - ; { 212 - key = key_map.rebase.menu 213 - ; description = "Rebase revision " 214 - ; cmd = 215 - SubCmd 216 - [ 217 - { 218 - key = key_map.rebase.single 219 - ; description = "Rebase single revision " 220 - ; cmd = 221 - Dynamic_r 222 - (fun rev -> 223 - Prompt ("Dest rev for " ^ rev, [ "rebase"; "-r"; rev; "-d" ])) 224 - } 225 - ; { 226 - key = key_map.rebase.with_descendants 227 - ; description = "Rebase revision and its decendents" 228 - ; cmd = 229 - Dynamic_r 230 - (fun rev -> 231 - Prompt 232 - ( Printf.sprintf "Dest rev for %s and it's decendents" rev 233 - , [ "rebase"; "-s"; rev; "-d" ] )) 234 - } 235 - ; { 236 - key = key_map.rebase.with_bookmark 237 - ; description = "Rebase revision and all other revissions on its bookmark" 238 - ; cmd = 239 - Dynamic_r 240 - (fun rev -> 241 - Prompt 242 - ( "Dest rev for bookmark including " ^ rev 243 - , [ "rebase"; "-b"; rev; "-d" ] )) 244 - } 245 - ] 246 - } 247 - ; { 248 - key = key_map.git.menu 249 - ; description = "Git commands" 250 - ; cmd = 251 - SubCmd 252 - [ 253 - { 254 - key = key_map.git.push 255 - ; description = "git push" 256 - ; cmd = 257 - Fun 258 - (fun _ -> 259 - let revs = Vars.get_active_revs () in 260 - let subcmds = 261 - [ 262 - { 263 - key = key_map_base.confirm 264 - ; description = "proceed" 265 - ; cmd = 266 - Cmd 267 - ([ "git"; "push"; "--allow-new" ] 268 - @ (revs |> List.concat_map (fun x -> [ "-r"; x ]))) 269 - } 270 - ; { 271 - key = key_map_base.decline 272 - ; description = "exit" 273 - ; cmd = 274 - Fun 275 - (fun _ -> 276 - ui_state.input $= `Normal; 277 - ui_state.show_popup $= None) 278 - } 279 - ] 280 - in 281 - let log = 282 - jj_no_log 283 - ~get_stderr:true 284 - ([ "git"; "push"; "--allow-new"; "--dry-run" ] 285 - @ (revs |> List.concat_map (fun x -> [ "-r"; x ]))) 286 - |> AnsiReverse.colored_string 287 - |> Ui.atom 288 - |> Lwd.pure 289 - in 290 - let ui = W.vbox [ log; commands_list_ui subcmds ] in 291 - ui_state.show_popup $= Some (ui, "Git push will:"); 292 - ui_state.input $= `Mode (command_input ~is_sub:true subcmds)) 293 - } 294 - ; { 295 - key = key_map.git.fetch 296 - ; description = "git fetch" 297 - ; cmd = Cmd [ "git"; "fetch" ] 298 - } 299 - ; { 300 - key = key_map.git.fetch_all 301 - ; description = "git fetch all remotes" 302 - ; cmd = Cmd [ "git"; "fetch"; "--all-remotes" ] 303 - } 304 - ] 305 - } 306 - ; { 307 - key = key_map.parallelize 308 - ; description = 309 - "Parallelize commits. Takes 2 commits and makes them have the\n\ 310 - same parent and child. Run `jj parallelize` --help for details" 311 - ; cmd = 312 - PromptThen 313 - ( "list commits to parallelize" 314 - , fun x -> Cmd ([ "paralellize" ] @ (x |> String.split_on_char ' ')) ) 315 - } 316 - ; { 317 - key = key_map.abandon 318 - ; description = "Abandon this change(removes just this change and rebases parents)" 319 - ; cmd = 320 - Dynamic 321 - (fun () -> 322 - let revs = Vars.get_active_revs () in 323 - Cmd ([ "abandon" ] @ revs) 324 - |> confirm_prompt ("abandon the revisions:\n" ^ (revs |> String.concat "\n"))) 325 - } 326 - ; { 327 - key = key_map.bookmark.menu 328 - ; description = "Bookmark commands" 329 - ; cmd = 330 - SubCmd 331 - [ 332 - { 333 - key = key_map.bookmark.create 334 - ; description = "Create new bookmark" 335 - ; cmd = 336 - PromptThen 337 - ( "Bookmark name to create" 338 - , fun x -> 339 - Cmd_r 340 - ([ "bookmark"; "create" ] 341 - @ [ x |> String.map (fun c -> if c = ' ' then '_' else c) ]) ) 342 - } 343 - ; { 344 - key = key_map.bookmark.delete 345 - ; description = "Delete bookmark" 346 - ; cmd = 347 - bookmark_select_prompt 348 - branches_no_remote 349 - "Bookmark to delete" 350 - (fun bookmark -> 351 - Cmd [ "bookmark"; "delete"; bookmark ] 352 - |> confirm_prompt 353 - (Printf.sprintf 354 - "delete the bookmark: '%s' This will also delete it on \ 355 - the remote next \"git push\"." 356 - bookmark)) 357 - } 358 - ; { 359 - key = key_map.bookmark.forget 360 - ; description = "Forget bookmark" 361 - ; cmd = 362 - bookmark_select_prompt 363 - branches_no_remote 364 - "Bookmark to forget" 365 - (fun bookmark -> 366 - Cmd [ "bookmark"; "forget"; bookmark ] 367 - |> confirm_prompt 368 - (Printf.sprintf 369 - "forget the bookmark: '%s' . This will not delete it on \ 370 - the remote." 371 - bookmark)) 372 - } 373 - ; { 374 - key = key_map.bookmark.rename 375 - ; description = "Rename bookmark" 376 - ; cmd = 377 - bookmark_select_prompt 378 - branches_no_remote 379 - "Select the bookmark to rename (only local/tracked bookmarks are \ 380 - shown)" 381 - (fun curr_name -> 382 - Prompt ("New bookmark name", [ "bookmark"; "rename"; curr_name ])) 383 - } 384 - ; { 385 - key = key_map.bookmark.set 386 - ; description = "Set bookmark to this change" 387 - ; cmd = 388 - Dynamic_r 389 - (fun rev -> 390 - bookmark_select_prompt 391 - branches_no_remote 392 - ("Select the bookmark to set to rev: " ^ rev) 393 - (fun bookmark -> 394 - Cmd 395 - [ 396 - "bookmark"; "set"; "-r"; get_hovered_rev (); "-B"; bookmark 397 - ])) 398 - } 399 - ; { 400 - key = key_map.bookmark.track 401 - ; description = "track given remote bookmark" 402 - ; cmd = 403 - bookmark_select_prompt 404 - branches_remotes_not_tracked 405 - "Select the bookmark to begin tracking" 406 - (fun bookmark -> Cmd [ "bookmark"; "track"; bookmark ]) 407 - } 408 - ; { 409 - key = key_map.bookmark.untrack 410 - ; description = "untrack given remote bookmark" 411 - ; cmd = 412 - bookmark_select_prompt 413 - branches_remotes_tracked 414 - "Select the bookmark to untrack" 415 - (fun bookmark -> Cmd [ "bookmark"; "untrack"; bookmark ]) 416 - } 417 - ] 418 - } 419 - ; { 420 - key = key_map.filter 421 - ; description = "Filter using revset" 422 - ; cmd = 423 - PromptThen 424 - ( "Filter using revset" 425 - , fun revset -> 426 - Fun 427 - (fun () -> 428 - if revset = "" 429 - then Vars.ui_state.revset $= None 430 - else Vars.ui_state.revset $= Some revset) ) 431 - } 432 - ] 433 - 434 - and command_mapping = ref None 435 - 436 - and get_command_mapping () = 34 + (* Remove the hardcoded make_command_mapping function and use the dynamic one *) 35 + let command_mapping = ref None 36 + 37 + let rec get_command_mapping () = 437 38 match !command_mapping with 438 - | Some mapping -> 439 - mapping 39 + | Some mapping -> mapping 440 40 | None -> 441 - let mapping = make_command_mapping (Lwd.peek ui_state.config).key_map in 41 + let key_map = (Lwd.peek ui_state.config).key_map.graph in 42 + let registry = GraphCommands.get_command_registry get_command_mapping in 43 + let mapping = build_command_list key_map registry in 442 44 command_mapping := Some mapping; 443 45 mapping 444 46 ;; ··· 481 83 | `Selectable x -> 482 84 let ui = 483 85 W.Lists.selectable_item 484 - ( 485 - x ^ "\n" 86 + (x ^ "\n" 486 87 (* TODO This won't work if we are on a branch, because that puts the @ further out*) 487 88 |> Jj_tui.AnsiReverse.colored_string 488 89 |> Ui.atom) ··· 502 103 W.Lists.(Selectable data) 503 104 | `Filler x -> 504 105 W.Lists.( 505 - Filler ( x |> Jj_tui.AnsiReverse.colored_string |> Ui.atom |> Lwd.pure))) 106 + Filler (x |> Jj_tui.AnsiReverse.colored_string |> Ui.atom |> Lwd.pure))) 506 107 in 507 108 items 508 109 in
+71 -25
jj_tui/bin/jj_commands.ml
··· 4 4 5 5 open Jj_tui.Logging 6 6 open Jj_tui.Key_map 7 - open Jj_tui.Key_map 7 + open Jj_tui.Key 8 + open Jj_tui 9 + 8 10 (** Internal to this module. I'm trying this out as a way to avoid .mli files*) 9 11 module Shared = struct 10 12 type cmd_args = string list [@@deriving show] ··· 38 40 (** Same as prompt except you can run another command after. Useful if you want multiple prompts *) 39 41 | Prompt_I of string * cmd_args 40 42 (** Same as prompt but expects the command to be interactive same as [Cmd_I] *) 41 - | SubCmd of 'a command list 43 + | SubCmd of 'a command Key_Map.t 42 44 (** Allows nesting of commands, shows a popup with command options and waits for the user to press the appropriate key*) 43 45 | Fun of (unit -> unit) 44 46 (** Execute an arbitrary function. Prefer other command types if possible *) ··· 46 48 47 49 (** A command that should be run when it's key is pressed*) 48 50 and 'a command = { 49 - key : key 51 + key : Key.t 50 52 ; description : string 51 53 ; cmd : 'a command_variant 52 54 } 53 55 [@@deriving show] 56 + 57 + (* Common type for command definition registry *) 58 + type 'a command_definition = { 59 + id : string 60 + ; description : string 61 + ; make_cmd : unit -> 'a command_variant 62 + } 54 63 end 55 64 56 65 (** Internal to this module. I'm trying this out as a way to avoid .mli files*) ··· 99 108 100 109 let rec render_commands ?(indent_level = 0) commands = 101 110 commands 102 - |> List.concat_map @@ fun command -> 111 + |> Key_Map.to_list 112 + |> List.concat_map @@ fun (key,command) -> 103 113 match command with 104 114 | { 105 115 key ··· 126 136 127 137 let commands_list_ui ?(include_arrows = false) commands = 128 138 let move_command = 129 - render_command_line 130 - ~indent_level:0 131 - ("Arrows" ) 132 - "navigation between windows" 139 + render_command_line ~indent_level:0 "Arrows" "navigation between windows" 133 140 in 134 141 ((commands |> render_commands) @ if include_arrows then [ move_command ] else []) 135 142 |> I.vcat ··· 248 255 and command_input ~is_sub keymap key = 249 256 (* Use exceptions so we can break out of the list*) 250 257 let input = Lwd.peek ui_state.input in 251 - 252 258 try 253 - keymap 254 - |> List.iter (fun cmd -> 255 - (*log keys*) 256 259 match key with 257 - |`ASCII k,modifiers-> 258 - 259 - (*log keys*) 260 - [%log info "key: %s"(key_to_string {key=k;modifiers})]; 261 - if (`ASCII cmd.key.key,cmd.key.modifiers) = key then 262 - handleCommand cmd.description cmd.cmd; 263 - |_->() 264 - ); 265 - `Unhandled 260 + | `ASCII k, modifiers -> 261 + let key = { key = k; modifiers } in 262 + [%log info "key: %s" (key_to_string key)]; 263 + let cmd = keymap|>Key_Map.find_opt key in 264 + (match cmd with 265 + | Some cmd -> 266 + handleCommand cmd.description cmd.cmd; 267 + `Handled 268 + | None -> 269 + `Unhandled) 270 + | _ -> 271 + `Unhandled 266 272 with 267 273 | Handled -> 268 274 (*If this is a sub command and we didn't change to some other subcommand we should exit back to normal command operation*) ··· 272 278 handle_jj_error ~cmd ~error; 273 279 `Unhandled 274 280 275 - 276 281 and command_no_input description cmd = 277 282 (* Use exceptions so we can break out of the list*) 278 283 try ··· 296 301 include Shared 297 302 298 303 (** A handy command_list that just has this help command for areas that don't have any commands to still show help*) 299 - let rec default_list = 304 + let rec make_default_list (): string command Key_Map.t= 300 305 [ 301 306 { 302 307 key = { key = '?'; modifiers = [] } ··· 305 310 Fun 306 311 (fun _ -> 307 312 ui_state.show_popup 308 - $= Some (commands_list_ui ~include_arrows:true default_list, "Help"); 313 + $= Some (commands_list_ui ~include_arrows:true (make_default_list ()), "Help"); 309 314 ui_state.input $= `Mode (fun _ -> `Unhandled)) 310 315 } 311 316 ] 317 + |> List.to_seq 318 + |> Seq.map (fun x -> x.key, x) 319 + |> Key_Map.of_seq 312 320 ;; 321 + let default_list=make_default_list() 313 322 314 323 (**Generate a UI object with all the commands nicely formatted and layed out. Useful for help text*) 315 324 let commands_list_ui = commands_list_ui 316 325 317 326 (**`Prompt`:Allows running one command and then running another using the input of the first*) 318 327 let confirm_prompt prompt cmd = 319 - SubCmd [ {key=(Lwd.peek Vars.ui_state.config).key_map.confirm; description = "Yes I want to " ^ prompt; cmd } ] 328 + let key = key_of_string_exn "y" in 329 + 330 + let sub_cmd= Key_Map.of_list [key, { key; description = "Yes I want to " ^ prompt; cmd }] in 331 + SubCmd sub_cmd 320 332 ;; 321 333 322 334 (** Handles raw command mapping without regard for modes or the current intput state. Should be used when setting a new input mode*) ··· 330 342 | `Normal -> 331 343 command_input ~is_sub:false command_mapping 332 344 ;; 345 + 346 + (* Function to build command list from key_map and a command registry *) 347 + let build_command_list key_map command_registry = 348 + 349 + (* Process a key_map item *) 350 + let rec process_key_map_item key item = 351 + match item with 352 + | Command { command = id } -> 353 + (match Hashtbl.find_opt command_registry id with 354 + | Some cmd_def -> 355 + Some { key; description = cmd_def.description; cmd = cmd_def.make_cmd () } 356 + | None -> 357 + [%log warn "Unknown command ID: %s" id]; 358 + None) 359 + | Sub_menu { title; subcommands } -> 360 + (* Process submenu items *) 361 + let sub_cmds = 362 + subcommands 363 + |> Key_Map.to_seq 364 + |> Seq.filter_map (fun (k, v) -> 365 + process_key_map_item k v |> Option.map (fun x -> k, x)) 366 + |> Key_Map.of_seq 367 + in 368 + Some { key; description = title; cmd = SubCmd sub_cmds } 369 + in 370 + (* Process all items in the key_map *) 371 + key_map 372 + |> Key_Map.to_seq 373 + |> Seq.filter_map (fun (k, v) -> 374 + process_key_map_item k v |> Option.map (fun x -> k, x)) 375 + |> Key_Map.of_seq 376 + ;; 377 + 378 + (* List.rev !result *) 333 379 end
+2 -1
jj_tui/lib/config.ml
··· 1 1 open Util 2 2 open Logging 3 3 4 - type t = { key_map : Key_map.t[@updater] } [@@deriving yaml, record_updater ~derive: yaml] 4 + 5 + type t = { key_map : Key_map.key_config[@updater] } [@@deriving yaml, record_updater ~derive: yaml] 5 6 6 7 7 8 let default_config:t =
+2
jj_tui/lib/dune
··· 25 25 ppx_deriving_yojson 26 26 ppx_deriving_yaml 27 27 ppx_record_updater))) 28 + 29 +
+77
jj_tui/lib/key.ml
··· 1 + type modifier = [ `Meta | `Shift | `Ctrl ] 2 + 3 + type t = { 4 + key: char; 5 + modifiers: modifier list; 6 + } 7 + 8 + let sort_and_dedup_modifiers mods = 9 + let modifier_order = function 10 + | `Shift -> 0 11 + | `Meta -> 1 12 + | `Ctrl -> 2 13 + in 14 + mods 15 + |> List.sort_uniq (fun a b -> compare (modifier_order a) (modifier_order b)) 16 + 17 + let key_of_string str = 18 + let parts = String.split_on_char '+' str in 19 + let rec process_parts mods = function 20 + | [] -> Error "No key character provided" 21 + | [k] when String.length k = 1 -> 22 + let key = k.[0] in 23 + Ok { key = key; modifiers = sort_and_dedup_modifiers ( mods) } 24 + | mod_str :: rest -> 25 + let modifier = match String.uppercase_ascii mod_str with 26 + | "C" | "CTRL" -> Ok `Ctrl 27 + | "S" | "SHIFT" -> Ok `Shift 28 + | "A" | "ALT" -> Ok `Meta 29 + | other -> Error (Printf.sprintf "Unknown modifier: %s" other) 30 + in 31 + match modifier with 32 + | Ok m -> process_parts (m :: mods) rest 33 + | Error e -> Error e 34 + in 35 + process_parts [] parts 36 + 37 + let key_of_string_exn str= key_of_string str|>Result.get_ok 38 + 39 + let key_to_string { key; modifiers } = 40 + let modifier_str = 41 + modifiers 42 + |> List.map (function 43 + | `Shift -> "S" 44 + | `Meta -> "A" 45 + | `Ctrl -> "C") 46 + |> String.concat "+" 47 + in 48 + if modifier_str = "" then 49 + String.make 1 key 50 + else 51 + modifier_str ^ "+" ^ (String.make 1 key) 52 + 53 + let key_of_yaml = function 54 + | `String s -> 55 + (match key_of_string s with 56 + | Ok k -> Ok k 57 + | Error msg -> Error (`Msg("Invalid key format: " ^ msg))) 58 + | _ -> Error (`Msg "Expected string for key") 59 + 60 + let key_to_yaml k = 61 + `String (key_to_string k) 62 + 63 + let pp fmt k = Format.fprintf fmt "%s" (key_to_string k) 64 + 65 + let equal k1 k2 = k1.key = k2.key && k1.modifiers = k2.modifiers 66 + 67 + let hash k = Char.hash k.key + List.fold_left (fun acc m -> acc * 31 + match m with 68 + | `Meta -> 1 69 + | `Shift -> 2 70 + | `Ctrl -> 3 71 + ) 0 k.modifiers 72 + 73 + let compare k1 k2 = 74 + match compare k1.key k2.key with 75 + | 0 -> List.compare compare k1.modifiers k2.modifiers 76 + | c -> c 77 + ;;
+257 -206
jj_tui/lib/key_map.ml
··· 1 - type modifier = [ `Meta | `Shift | `Ctrl ] 1 + module Key_Map = struct 2 + include Map.Make ( Key) 3 + 4 + let pp inner_pp fmt this = 5 + Format.fprintf fmt "@[<v>"; 6 + this |> iter (fun k v -> Format.fprintf fmt "@[<h>%a@ %a@]" Key.pp k inner_pp v); 7 + Format.fprintf fmt "@]" 8 + ;; 9 + 10 + let show inner_pp this = 11 + pp inner_pp Format.str_formatter this; 12 + Format.flush_str_formatter () 13 + ;; 14 + end 15 + 16 + type command = { command : string } [@@deriving show] 2 17 3 - type key = { 4 - key: char; 5 - modifiers: modifier list; 18 + type sub_menu = { 19 + title : string 20 + ; subcommands : key_map 6 21 } 7 22 8 - let sort_and_dedup_modifiers mods = 9 - let modifier_order = function 10 - | `Shift -> 0 11 - | `Meta -> 1 12 - | `Ctrl -> 2 13 - in 14 - mods 15 - |> List.sort_uniq (fun a b -> compare (modifier_order a) (modifier_order b)) 23 + and key_map_item = 24 + | Sub_menu of sub_menu 25 + | Command of command 16 26 17 - let key_of_string str = 18 - let parts = String.split_on_char '+' str in 19 - let rec process_parts mods = function 20 - | [] -> Error "No key character provided" 21 - | [k] when String.length k = 1 -> 22 - let key = k.[0] in 23 - Ok { key = key; modifiers = sort_and_dedup_modifiers ( mods) } 24 - | mod_str :: rest -> 25 - let modifier = match String.uppercase_ascii mod_str with 26 - | "C" | "CTRL" -> Ok `Ctrl 27 - | "S" | "SHIFT" -> Ok `Shift 28 - | "A" | "ALT" -> Ok `Meta 29 - | other -> Error (Printf.sprintf "Unknown modifier: %s" other) 30 - in 31 - match modifier with 32 - | Ok m -> process_parts (m :: mods) rest 33 - | Error e -> Error e 34 - in 35 - process_parts [] parts 27 + and key_map = key_map_item Key_Map.t[@@deriving show] 28 + and key_map_update_t = key_map 36 29 37 - let key_of_string_exn str= key_of_string str|>Result.get_ok 30 + let ( let* ) = Base.Result.Let_syntax.( >>= ) 31 + let ( let+ ) = Base.Result.Let_syntax.( >>| ) 38 32 39 - let key_to_string { key; modifiers } = 40 - let modifier_str = 41 - modifiers 42 - |> List.map (function 43 - | `Shift -> "S" 44 - | `Meta -> "A" 45 - | `Ctrl -> "C") 46 - |> String.concat "+" 47 - in 48 - if modifier_str = "" then 49 - String.make 1 key 50 - else 51 - modifier_str ^ "+" ^ (String.make 1 key) 33 + let rec key_map_item_of_yaml (value : string * Yaml.value) = 34 + match value with 35 + | key, `O [ ("sub", `O sub); ("title", `String title) ] 36 + | key, `O [ ("title", `String title); ("sub", `O sub) ] -> 37 + let* sub_items = 38 + List.map key_map_item_of_yaml sub 39 + |> Base.Result.all 40 + |> Result.map (fun x -> Key_Map.of_seq (List.to_seq x)) 41 + |> Result.map_error (fun (`Msg msg) -> `Msg ("Invalid submenu: " ^ msg)) 42 + in 43 + let+ key = 44 + Key.key_of_string key |> Result.map_error (fun msg -> `Msg ("Invalid key: " ^ msg)) 45 + in 46 + key, Sub_menu { title; subcommands = sub_items } 47 + | key, `String command -> 48 + let+ key = 49 + Key.key_of_string key |> Result.map_error (fun msg -> `Msg ("Invalid key: " ^ msg)) 50 + in 51 + key, Command { command } 52 + | _ -> 53 + Error (`Msg "Invalid YAML structure") 54 + ;; 52 55 53 - let key_of_yaml = function 54 - | `String s -> 55 - (match key_of_string s with 56 - | Ok k -> Ok k 57 - | Error msg -> Error (`Msg("Invalid key format: " ^ msg))) 58 - | _ -> Error (`Msg "Expected string for key") 56 + let key_map_of_yaml (yaml : Yaml.value) = 57 + match yaml with 58 + | `O top_level -> 59 + List.map key_map_item_of_yaml top_level 60 + |> Base.Result.all 61 + |> Result.map (fun x -> Key_Map.of_seq (List.to_seq x)) 62 + | _ -> 63 + Error (`Msg "Invalid YAML structure") 64 + ;; 59 65 60 - let key_to_yaml k = 61 - `String (key_to_string k) 66 + let key_map_of_yaml_exn yaml = 67 + match key_map_of_yaml yaml with 68 + | Ok key_map -> 69 + key_map 70 + | Error (`Msg msg) -> 71 + failwith ("Invalid YAML structure: " ^ msg) 72 + ;; 62 73 63 - let pp_key fmt k = Format.fprintf fmt "%s" (key_to_string k) 64 - (* Update all the types to use key instead of char *) 65 - type bookmark_keys = { 66 - menu:key; 67 - create : key; 68 - delete : key; 69 - forget : key; 70 - rename : key; 71 - set : key; 72 - track : key; 73 - untrack : key; 74 - }[@@deriving yaml, record_updater ~derive: yaml] 74 + open Yaml.Util 75 75 76 - type git_keys = { 77 - menu:key; 78 - push : key; 79 - fetch : key; 80 - fetch_all : key; 81 - }[@@deriving yaml, record_updater ~derive: yaml] 76 + let rec key_map_item_to_yaml = function 77 + | key, Sub_menu { title; subcommands } -> 78 + let sub_yaml = key_map_to_yaml subcommands in 79 + let key = Key.key_to_string key in 80 + key, obj [ "title", string title; "sub", sub_yaml ] 81 + | key, Command { command } -> 82 + let key = Key.key_to_string key in 83 + key, string command 82 84 83 - type squash_keys = { 84 - menu:key; 85 - into_parent : key; 86 - into_rev : key; 87 - unsquash : key; 88 - interactive_parent : key; 89 - interactive_rev : key; 90 - }[@@deriving yaml, record_updater ~derive: yaml] 85 + and key_map_to_yaml (key_map : key_map) : Yaml.value = 86 + obj (key_map |> Key_Map.to_seq |> Seq.map key_map_item_to_yaml |> List.of_seq) 87 + ;; 91 88 92 - type rebase_keys = { 93 - menu:key; 94 - single : key; 95 - with_descendants : key; 96 - with_bookmark : key; 97 - }[@@deriving yaml, record_updater ~derive: yaml] 89 + let key_map_update_t_to_yaml (key_map : key_map_update_t) : Yaml.value = 90 + key_map_to_yaml key_map 91 + ;; 98 92 99 - type file_keys = { 100 - show_help : key; 101 - move_to_rev : key; 102 - move_to_child : key; 103 - move_to_parent : key; 104 - discard : key; 105 - }[@@deriving yaml, record_updater ~derive: yaml] 93 + let key_map_update_t_of_yaml (yaml : Yaml.value) = key_map_of_yaml yaml 106 94 107 - type new_child_keys= { 108 - menu:key; 109 - base:key; 110 - no_edit:key; 111 - inline:key; 112 - inline_no_edit:key; 113 - }[@@deriving yaml, record_updater ~derive: yaml] 95 + (* let rec key_map_merge (key_map1 : key_map) (key_map2 : key_map) : key_map = 96 + let merged = Key_Map.create (Key_Map.length key_map1 + Key_Map.length key_map2) in 97 + Key_Map.iter (fun k v -> Key_Map.add merged k v) key_map1; 98 + Key_Map.iter 99 + (fun k v -> 100 + (**Find if the key is already in the merged map*) 101 + match Key_Map.find_opt merged k with 102 + | Some (Sub_menu { title = t1; subcommands = s1 }) -> 103 + (match v with 104 + | Sub_menu { title = t2; subcommands = s2 } -> 105 + let merged_subcommands = key_map_merge s1 s2 in 106 + Key_Map.replace 107 + merged 108 + k 109 + (Sub_menu { title = t2; subcommands = merged_subcommands }) 110 + | Command _ -> 111 + Key_Map.replace merged k v) 112 + | Some (Command _) | None -> Key_Map.replace merged k v) 113 + key_map2; 114 + merged 115 + ;; *) 114 116 115 - type commit_keys = { 116 - menu:key; 117 - base:key; 118 - no_edit:key; 119 - open_editor:key; 120 - }[@@deriving yaml, record_updater ~derive: yaml] 117 + (**Merge two key maps, checking for duplicate keys*) 118 + let key_map_apply_update override og = 119 + Key_Map.merge 120 + (fun k v1 v2 -> 121 + match v1, v2 with 122 + | Some og, Some override -> Some (override) 123 + | Some v, None | None, Some v -> Some v 124 + | None, None -> None) 125 + og 126 + override 127 + ;; 128 + 129 + let cmd key id = 130 + let key = Key.key_of_string_exn key in 131 + key, Command { command = id } 132 + ;; 121 133 122 - type graph_keys = { 123 - show_help : key; 124 - prev : key; 125 - new_child : new_child_keys; [@updater] 126 - duplicate : key; 127 - undo : key; 128 - commit : commit_keys;[@updater] 129 - split : key; 130 - squash : squash_keys;[@updater] 131 - edit : key; 132 - describe : key; 133 - describe_editor : key; 134 - resolve : key; 135 - rebase : rebase_keys;[@updater] 136 - git : git_keys;[@updater] 137 - parallelize : key; 138 - abandon : key; 139 - bookmark : bookmark_keys;[@updater] 140 - filter : key; 141 - }[@@deriving yaml, record_updater ~derive: yaml] 134 + let sub key title sub = 135 + let key = Key.key_of_string_exn key in 136 + key, Sub_menu { title; subcommands = sub |> List.to_seq |> Key_Map.of_seq } 137 + ;; 142 138 143 - type t = { 144 - confirm:key; 145 - decline:key; 146 - left_alt:key; 147 - right_alt:key; 148 - graph : graph_keys; [@updater] 149 - file : file_keys;[@updater] 150 - }[@@deriving yaml, record_updater ~derive: yaml] 139 + let k_map list = list |> List.to_seq |> Key_Map.of_seq 151 140 152 - (* Helper to create a simple key without modifiers *) 153 - let simple_key c = { key = c; modifiers = [] } 141 + type key_config = { 142 + global : key_map [@updater] 143 + ; graph : key_map [@updater] 144 + ; file : key_map [@updater] 145 + } 146 + [@@deriving yaml, record_updater ~derive:yaml] 154 147 155 148 (* Default key bindings matching current implementation *) 156 - let default:t = { 157 - confirm= simple_key 'y'; 158 - decline= simple_key 'n'; 159 - left_alt= simple_key 'h'; 160 - right_alt= simple_key 'l'; 161 - graph = { 162 - show_help = simple_key '?'; 163 - prev = simple_key 'P'; 164 - 165 - new_child={ 166 - menu= simple_key 'n'; 167 - base= simple_key 'n'; 168 - no_edit= key_of_string_exn "N"; 169 - inline= simple_key 'i'; 170 - inline_no_edit= simple_key 'I'; 171 - }; 172 - duplicate = simple_key 'y'; 173 - undo = simple_key 'u'; 174 - commit = { 175 - menu = simple_key 'c'; 176 - base = simple_key 'c'; 177 - no_edit = simple_key 'C'; 178 - open_editor = simple_key 'D'; 179 - }; 180 - split = simple_key 'S'; 181 - squash = { 182 - menu = simple_key 's'; 183 - into_parent = simple_key 's'; 184 - into_rev = simple_key 'S'; 185 - unsquash = simple_key 'u'; 186 - interactive_parent = simple_key 'i'; 187 - interactive_rev = simple_key 'I'; 188 - }; 189 - edit = simple_key 'e'; 190 - describe = simple_key 'd'; 191 - describe_editor = simple_key 'D'; 192 - resolve = simple_key 'R'; 193 - rebase = { 194 - menu = simple_key 'r'; 195 - single = simple_key 'r'; 196 - with_descendants = simple_key 's'; 197 - with_bookmark = simple_key 'b'; 198 - }; 199 - git = { 200 - menu = simple_key 'g'; 201 - push = simple_key 'p'; 202 - fetch = simple_key 'f'; 203 - fetch_all = simple_key 'F'; 204 - }; 205 - parallelize = simple_key 'z'; 206 - abandon = simple_key 'a'; 207 - bookmark = { 208 - menu = simple_key 'b'; 209 - create = simple_key 'c'; 210 - delete = simple_key 'd'; 211 - forget = simple_key 'f'; 212 - rename = simple_key 'r'; 213 - set = simple_key 's'; 214 - track = simple_key 't'; 215 - untrack = simple_key 'u'; 216 - }; 217 - filter = simple_key 'f'; 218 - }; 219 - file = { 220 - show_help = simple_key '?'; 221 - move_to_rev = simple_key 'm'; 222 - move_to_child = simple_key 'N'; 223 - move_to_parent = simple_key 'P'; 224 - discard = simple_key 'd'; 225 - }; 226 - } 149 + let default : key_config = 150 + let open Key in 151 + { 152 + global = 153 + k_map 154 + [ cmd "y" "confirm"; cmd "n" "decline"; cmd "h" "left_alt"; cmd "l" "right_alt" ] 155 + ; graph = 156 + k_map 157 + [ 158 + sub "?" "Help" [ cmd "?" "show_help" ] 159 + ; cmd "?" "show_help" 160 + ; cmd "P" "prev" 161 + ; sub 162 + "n" 163 + "New Child" 164 + [ 165 + cmd "n" "new_base" 166 + ; cmd "N" "new_no_edit" 167 + ; cmd "i" "new_inline" 168 + ; cmd "I" "new_inline_no_edit" 169 + ] 170 + ; cmd "y" "duplicate" 171 + ; cmd "u" "undo" 172 + ; sub "c" "Commit" [ cmd "c" "commit_base"; cmd "C" "commit_no_edit"; cmd "D" "describe_editor" ] 173 + ; cmd "S" "split" 174 + ; sub 175 + "s" 176 + "Squash" 177 + [ 178 + cmd "s" "squash_into_parent" 179 + ; cmd "S" "squash_into_rev" 180 + ; cmd "u" "squash_unsquash" 181 + ; cmd "i" "squash_interactive_parent" 182 + ; cmd "I" "squash_interactive_rev" 183 + ] 184 + ; cmd "e" "edit" 185 + ; cmd "d" "describe" 186 + ; cmd "D" "describe_editor" 187 + ; cmd "R" "resolve" 188 + ; sub 189 + "r" 190 + "Rebase" 191 + [ cmd "r" "rebase_single"; cmd "s" "rebase_with_descendants"; cmd "b" "rebase_with_bookmark" ] 192 + ; sub "g" "Git" [ cmd "p" "git_push"; cmd "f" "git_fetch"; cmd "F" "git_fetch_all" ] 193 + ; cmd "z" "parallelize" 194 + ; cmd "a" "abandon" 195 + ; sub 196 + "b" 197 + "Bookmark" 198 + [ 199 + cmd "c" "bookmark_create" 200 + ; cmd "d" "bookmark_delete" 201 + ; cmd "f" "bookmark_forget" 202 + ; cmd "r" "bookmark_rename" 203 + ; cmd "s" "bookmark_set" 204 + ; cmd "t" "bookmark_track" 205 + ; cmd "u" "bookmark_untrack" 206 + ] 207 + ; cmd "f" "filter" 208 + ] 209 + ; file = 210 + k_map 211 + [ 212 + cmd "?" "show_help" 213 + ; cmd "m" "move_to_rev" 214 + ; cmd "N" "move_to_child" 215 + ; cmd "P" "move_to_parent" 216 + ; cmd "d" "discard" 217 + ] 218 + } 219 + ;; 227 220 228 221 (* Example usage: 229 222 key_of_string "C+S+A+a" = Ok { key = 'a'; modifiers = [`Ctrl; `Shift; `Meta] } ··· 232 225 key_of_string "C+S+" = Error "No key character provided" 233 226 key_of_string "X+a" = Error "Unknown modifier: X" 234 227 *) 228 + 229 + let sample = 230 + {| 231 + c: 232 + title: "Commit" 233 + sub: 234 + a: amend 235 + n: new 236 + s: 237 + sub: 238 + p: into_parent 239 + r: into_rev 240 + u: unsquash 241 + C+r: interactive_parent 242 + C+i: interactive_rev 243 + title: "Squash" 244 + |} 245 + ;; 246 + 247 + let%expect_test "parse yaml" = 248 + let yaml = Yaml.of_string_exn sample in 249 + let key_map = key_map_of_yaml_exn yaml in 250 + print_endline (Yaml.to_string_exn (key_map_to_yaml key_map)); 251 + [%expect 252 + {| 253 + c: 254 + title: Commit 255 + sub: 256 + s: 257 + title: Squash 258 + sub: 259 + p: into_parent 260 + u: unsquash 261 + r: into_rev 262 + C+r: interactive_parent 263 + C+i: interactive_rev 264 + "n": new 265 + a: amend 266 + |}] 267 + ;; 268 + 269 + let%expect_test "merge" = 270 + let yaml = Yaml.of_string_exn sample in 271 + let key_map = key_map_of_yaml_exn yaml in 272 + let overrides = 273 + k_map [ cmd "c" "override"; sub "s" "Squash" [ cmd "c" "override2" ] ] 274 + in 275 + let merged = key_map_apply_update overrides key_map in 276 + print_endline (Yaml.to_string_exn (key_map_to_yaml merged)); 277 + [%expect 278 + {| 279 + c: override 280 + s: 281 + title: Squash 282 + sub: 283 + c: override2 284 + |}] 285 + ;;
jj_tui/lib/key_map/key_map_new.ml

This is a binary file and will not be displayed.