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.

use record updater for keymaps and config hello hello nest commit command

+613 -178
+17 -10
dune-project
··· 15 15 16 16 (documentation https://url/to/documentation) 17 17 18 + (pin 19 + (url "git+https://github.com/faldor20/ppx_record_updater.git") 20 + (package 21 + (name ppx_record_updater))) 22 + 18 23 (package 19 24 (name jj_tui) 20 25 (synopsis "A short synopsis") 21 26 (description "A longer description") 22 - (depends 27 + (depends 23 28 lwd 24 29 ocaml 25 30 dune 26 31 stdio 27 32 nottui 28 - base 29 - angstrom 30 - ppx_expect 33 + base 34 + angstrom 35 + ppx_expect 31 36 ppx_jane 32 - 33 - (picos_std (= 0.5.0)) 34 - (picos_io (= 0.5.0)) 35 - ;;for notty 36 - uutf 37 - ) 37 + (picos_std 38 + (= 0.5.0)) 39 + (picos_io 40 + (= 0.5.0)) 41 + ;;for notty 42 + uutf 43 + yojson 44 + ppx_record_updater) 38 45 (tags 39 46 (topics "to describe" your project))) 40 47
+14 -48
flake.lock
··· 5 5 "nixpkgs-lib": "nixpkgs-lib" 6 6 }, 7 7 "locked": { 8 - "lastModified": 1725234343, 9 - "narHash": "sha256-+ebgonl3NbiKD2UD0x4BszCZQ6sTfL4xioaM49o5B3Y=", 8 + "lastModified": 1738453229, 9 + "narHash": "sha256-7H9XgNiGLKN1G1CgRh0vUL4AheZSYzPm+zmZ7vxbJdo=", 10 10 "owner": "hercules-ci", 11 11 "repo": "flake-parts", 12 - "rev": "567b938d64d4b4112ee253b9274472dc3a346eb6", 12 + "rev": "32ea77a06711b758da0ad9bd6a844c5740a87abd", 13 13 "type": "github" 14 14 }, 15 15 "original": { ··· 17 17 "type": "indirect" 18 18 } 19 19 }, 20 - "flake-utils": { 21 - "inputs": { 22 - "systems": "systems" 23 - }, 24 - "locked": { 25 - "lastModified": 1710146030, 26 - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 27 - "owner": "numtide", 28 - "repo": "flake-utils", 29 - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 30 - "type": "github" 31 - }, 32 - "original": { 33 - "owner": "numtide", 34 - "repo": "flake-utils", 35 - "type": "github" 36 - } 37 - }, 38 20 "nixpkgs": { 39 21 "locked": { 40 - "lastModified": 1725103162, 41 - "narHash": "sha256-Ym04C5+qovuQDYL/rKWSR+WESseQBbNAe5DsXNx5trY=", 42 - "path": "/nix/store/bd4fmzws6n5542khxbifbkr6nrygi232-source", 43 - "rev": "12228ff1752d7b7624a54e9c1af4b222b3c1073b", 22 + "lastModified": 1739214665, 23 + "narHash": "sha256-26L8VAu3/1YRxS8MHgBOyOM8xALdo6N0I04PgorE7UM=", 24 + "path": "/nix/store/zbvf6xwri9kvf42xl3vai3mx8jry6ax8-source", 25 + "rev": "64e75cd44acf21c7933d61d7721e812eac1b5a0a", 44 26 "type": "path" 45 27 }, 46 28 "original": { ··· 50 32 }, 51 33 "nixpkgs-lib": { 52 34 "locked": { 53 - "lastModified": 1725233747, 54 - "narHash": "sha256-Ss8QWLXdr2JCBPcYChJhz4xJm+h/xjl4G0c0XlP6a74=", 35 + "lastModified": 1738452942, 36 + "narHash": "sha256-vJzFZGaCpnmo7I6i416HaBLpC+hvcURh/BQwROcGIp8=", 55 37 "type": "tarball", 56 - "url": "https://github.com/NixOS/nixpkgs/archive/356624c12086a18f2ea2825fed34523d60ccc4e3.tar.gz" 38 + "url": "https://github.com/NixOS/nixpkgs/archive/072a6db25e947df2f31aab9eccd0ab75d5b2da11.tar.gz" 57 39 }, 58 40 "original": { 59 41 "type": "tarball", 60 - "url": "https://github.com/NixOS/nixpkgs/archive/356624c12086a18f2ea2825fed34523d60ccc4e3.tar.gz" 42 + "url": "https://github.com/NixOS/nixpkgs/archive/072a6db25e947df2f31aab9eccd0ab75d5b2da11.tar.gz" 61 43 } 62 44 }, 63 45 "ocaml-overlay": { 64 46 "inputs": { 65 - "flake-utils": "flake-utils", 66 47 "nixpkgs": [ 67 48 "nixpkgs" 68 49 ] 69 50 }, 70 51 "locked": { 71 - "lastModified": 1725532029, 72 - "narHash": "sha256-PJZLMI9lNJ+DeNiN0Yc5AGo5YlRjLeiytMFPbWfC2xY=", 52 + "lastModified": 1739743692, 53 + "narHash": "sha256-p/ctiOYQfJHo3BmAuMx7Y4lV98MvZAFQc/LqHkLFuj8=", 73 54 "owner": "nix-ocaml", 74 55 "repo": "nix-overlays", 75 - "rev": "43773c6701d459391dadfa8f73225d5fd9ad993d", 56 + "rev": "9e2797cded531bf519b9c536b6b77a86a7dc6dc9", 76 57 "type": "github" 77 58 }, 78 59 "original": { ··· 86 67 "flake-parts": "flake-parts", 87 68 "nixpkgs": "nixpkgs", 88 69 "ocaml-overlay": "ocaml-overlay" 89 - } 90 - }, 91 - "systems": { 92 - "locked": { 93 - "lastModified": 1681028828, 94 - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 95 - "owner": "nix-systems", 96 - "repo": "default", 97 - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 98 - "type": "github" 99 - }, 100 - "original": { 101 - "owner": "nix-systems", 102 - "repo": "default", 103 - "type": "github" 104 70 } 105 71 } 106 72 },
+28 -1
flake.nix
··· 44 44 notty-mine = ocamlPackages.notty.overrideAttrs (old: { 45 45 src = ./forks/notty/.; 46 46 }); 47 + 48 + ppx_record_updater_src = pkgs.fetchFromGitHub { 49 + owner = "faldor20"; 50 + repo = "ppx_record_updater"; 51 + rev = "15a6ac0fa1a98e21e2b4b68b2eaee088186d5515"; 52 + sha256 = "sha256-e1BD3+F+jutQFbjISKcCNJzkzxA+x8vuCphxT/CuZcA="; 53 + }; 54 + 55 + ppx_record_updater = ocamlPackages.buildDunePackage { 56 + pname = "ppx_record_updater"; 57 + version = "0.1.0"; 58 + duneVersion = "3"; 59 + src = ppx_record_updater_src; 60 + buildInputs = with ocamlPackages; [ 61 + ppxlib 62 + ppx_deriving 63 + ]; 64 + strictDeps = true; 65 + }; 66 + 47 67 picos_src = pkgs.fetchFromGitHub { 48 68 owner = "ocaml-multicore"; 49 69 repo = "picos"; ··· 232 252 233 253 signal 234 254 255 + ppx_record_updater 235 256 ocamlPackages.logs 236 257 ocamlPackages.logs-ppx 237 258 ··· 251 272 ocamlPackages.uutf 252 273 253 274 ocamlPackages.re 275 + 276 + ocamlPackages.yojson 277 + ocamlPackages.ppx_deriving_yojson 278 + ocamlPackages.yaml 279 + ocamlPackages.ppx_deriving_yaml 280 + 254 281 # ocamlPackages.parsexp 255 282 256 283 # Ocaml package dependencies needed to build go here. ··· 291 318 jj_tui_build_pkgs = jj_tui_build_pkgs; 292 319 }; 293 320 # OCaml packages available on nixpkgs 294 - ocamlPackages = pkgs.ocaml-ng.ocamlPackages_5_1; 321 + ocamlPackages = pkgs.ocaml-ng.ocamlPackages_5_3; 295 322 inherit (pkgs) mkShell lib; 296 323 297 324 in
+2
jj_tui.opam
··· 22 22 "picos_std" {= "0.5.0"} 23 23 "picos_io" {= "0.5.0"} 24 24 "uutf" 25 + "yojson" 26 + "ppx_record_updater" 25 27 "odoc" {with-doc} 26 28 ] 27 29 build: [
+19 -10
jj_tui/bin/dune
··· 1 1 (executable 2 2 (public_name jj_tui) 3 3 (name main) 4 - (libraries signal jj_tui nottui base stdio picos_io picos_mux.multififo picos_std.sync picos_std.finally picos_std.structured spawn ) 4 + (libraries 5 + signal 6 + jj_tui 7 + nottui 8 + base 9 + stdio 10 + picos_io 11 + picos_mux.multififo 12 + picos_std.sync 13 + picos_std.finally 14 + picos_std.structured 15 + spawn 16 + yojson) 17 + (preprocess 18 + (pps logs-ppx ppx_deriving.std))) 5 19 6 - (preprocess 7 - (pps logs-ppx ppx_deriving.std)) 8 - ) 9 - 10 - (env 11 - (static 12 - (flags(:standard -cclib -static -cclib -no-pie)) 13 - ) 14 - ) 20 + (env 21 + (static 22 + (flags 23 + (:standard -cclib -static -cclib -no-pie))))
+19 -10
jj_tui/bin/file_view.ml
··· 10 10 open Jj_tui 11 11 open Picos_std_structured 12 12 13 + open Jj_tui.Key_map 13 14 let active_files = Lwd.var [ "" ] 14 15 15 - let rec command_mapping = 16 + let rec make_command_mapping (key_map: Key_map.file_keys) = 16 17 [ 17 18 { 18 - key = '?' 19 + key = key_map.show_help 19 20 ; description = "Show help" 20 21 ; cmd = 21 22 Fun 22 23 (fun _ -> 23 24 ui_state.show_popup 24 - $= Some (commands_list_ui ~include_arrows:true command_mapping, "Help"); 25 + $= Some (commands_list_ui ~include_arrows:true (get_command_mapping ()), "Help"); 25 26 ui_state.input $= `Mode (fun _ -> `Unhandled)) 26 27 } 27 28 ; { 28 - key = 'm' 29 + key = key_map.move_to_rev 29 30 ; description = "Move file to other commit" 30 31 ; cmd = 31 32 PromptThen ··· 44 45 @ Lwd.peek active_files) ) 45 46 } 46 47 ; { 47 - key = 'N' 48 + key = key_map.move_to_child 48 49 ; description = "Move file to child commit" 49 50 ; cmd = 50 51 Dynamic_r ··· 54 55 @ Lwd.peek active_files)) 55 56 } 56 57 ; { 57 - key = 'P' 58 + key = key_map.move_to_parent 58 59 ; description = "Move file to parent commit" 59 60 ; cmd = 60 61 Dynamic_r ··· 64 65 @ Lwd.peek active_files)) 65 66 } 66 67 ; { 67 - key = 'd' 68 + key = key_map.discard 68 69 ; description = "Restore to previous revision (git discard)" 69 70 ; cmd = 70 71 Dynamic_r ··· 78 79 (Cmd ([ "restore"; "--to"; rev; "--from"; rev ^ "-" ] @ selected))) 79 80 } 80 81 ] 82 + and command_mapping = ref None 83 + and get_command_mapping () = 84 + match !command_mapping with 85 + | Some mapping -> mapping 86 + | None -> 87 + let mapping = make_command_mapping (Lwd.peek ui_state.config).key_map.file in 88 + command_mapping := Some mapping; 89 + mapping 81 90 ;; 82 - 83 91 let hovered_var = ref "./" 84 92 85 93 let file_view ~focus summary_focus = ··· 116 124 | `Enter, [] -> 117 125 Focus.request_reversable summary_focus; 118 126 `Handled 119 - | `ASCII k, [] -> 120 - handleInputs command_mapping k 127 + | k -> 128 + handleInputs (get_command_mapping ()) k 121 129 | _ -> 122 130 `Unhandled) 123 131 in ··· 129 137 in 130 138 ui 131 139 ;; 140 + 132 141 end
+9 -2
jj_tui/bin/global_vars.ml
··· 4 4 open Lwd_infix 5 5 open Jj_tui.Process 6 6 open Jj_tui.Logging 7 - 7 + open Jj_tui 8 8 type cmd_args = string list 9 + open Jj_tui.Key_map 10 + 9 11 10 12 type ui_state_t = { 11 13 view : ··· 16 18 (* | `Prompt of string * [ `Cmd of cmd_args | `Cmd_I of cmd_args ] *) 17 19 ] 18 20 Lwd.var 19 - ; input : [ `Normal | `Mode of char -> Ui.may_handle ] Lwd.var 21 + ; input : [ `Normal | `Mode of (Nottui.Ui.key) -> Ui.may_handle ] Lwd.var 20 22 ; show_popup : (ui Lwd.t * string) option Lwd.var 21 23 ; show_prompt : W.Overlay.text_prompt_data option Lwd.var 22 24 (* ; show_graph_selection_prompt : *) ··· 34 36 ; revset : string option Lwd.var 35 37 ; trigger_update : unit Lwd.var 36 38 ; reset_selection : unit Signal.t 39 + ; config : Config.t Lwd.var 37 40 } 38 41 39 42 let get_unique_id maybe_unique_rev = ··· 64 67 val get_selected_revs_lwd : unit -> string list Lwd.t 65 68 val get_active_revs : unit -> string list 66 69 val get_active_revs_lwd : unit -> string list Lwd.t 70 + val config : Config.t Lwd.var 67 71 end 68 72 69 73 module Vars : Vars = struct ··· 88 92 ; command_log = Lwd.var [] 89 93 ; trigger_update = Lwd.var () 90 94 ; reset_selection = Signal.make ~equal:(fun _ _ -> false) () 95 + ; config = Lwd.var (Config.default_config) 91 96 } 92 97 ;; 93 98 ··· 138 143 then [ hovered |> get_unique_id ] 139 144 else selected |> List.map get_unique_id 140 145 ;; 146 + 147 + let config = ui_state.config 141 148 end
+133 -63
jj_tui/bin/graph_view.ml
··· 21 21 , func ) 22 22 ;; 23 23 24 - let rec command_mapping : 'acommand list = 24 + let custom_commit ?(edit = true) msg = 25 + let rev = Vars.get_hovered_rev () in 26 + jj [ "describe"; "-r"; rev; "-m"; msg ] |> ignore; 27 + (jj @@ [ "new"; "--insert-after"; rev ] @ if edit then [] else [ "--no-edit" ]) 28 + |> ignore 29 + ;; 30 + 31 + let rec make_command_mapping (key_map : Key_map.graph_keys) : 'acommand list = 25 32 [ 26 33 { 27 - key = '?' 34 + key = key_map.show_help 28 35 ; description = "Show help" 29 36 ; cmd = 30 37 Fun 31 38 (fun _ -> 32 39 ui_state.show_popup 33 - $= Some (commands_list_ui ~include_arrows:true command_mapping, "Help"); 40 + $= Some 41 + (commands_list_ui ~include_arrows:true (get_command_mapping ()), "Help"); 34 42 ui_state.input $= `Mode (fun _ -> `Unhandled)) 35 43 } 36 44 ; { 37 - key = 'P' 45 + key = key_map.prev 38 46 ; description = "Move the working copy to the previous child " 39 47 ; cmd = Cmd [ "prev" ] 40 48 } 41 49 ; { 42 - key = 'N' 43 - ; description = "Make a new change and insert it after the selected rev" 50 + key = key_map.new_child.menu 51 + ; description = "Make a new change" 44 52 ; cmd = 45 - Dynamic (fun () -> Cmd ([ "new"; "--insert-after" ] @ Vars.get_active_revs ())) 46 - } 47 - ; { 48 - key = 'n' 49 - ; cmd = Cmd_with_revs (Active [ "new" ]) 50 - ; description = "Make a new empty change as a child of the selected rev" 53 + SubCmd 54 + [ 55 + { 56 + key = key_map.new_child.base 57 + ; cmd = Cmd_with_revs (Active [ "new" ]) 58 + ; description = "Make new child commit" 59 + } 60 + ; { 61 + key = key_map.new_child.no_edit 62 + ; cmd = Cmd_with_revs (Active [ "new"; "--no-edit" ]) 63 + ; description = "Same as 'new', but without editing the new commit" 64 + } 65 + ; { 66 + key = key_map.new_child.inline 67 + ; description = "Make a new change and insert it after the selected rev" 68 + ; cmd = 69 + Dynamic 70 + (fun () -> 71 + Cmd ([ "new"; "--insert-after" ] @ Vars.get_active_revs ())) 72 + } 73 + ; { 74 + key = key_map.new_child.inline_no_edit 75 + ; description = "Same as 'new insert', but without editing the new commit" 76 + ; cmd = 77 + Dynamic 78 + (fun () -> 79 + Cmd 80 + ([ "new"; "--no-edit"; "--insert-after" ] 81 + @ Vars.get_active_revs ())) 82 + } 83 + ] 51 84 } 52 85 ; { 53 - key = 'y' 86 + key = key_map.duplicate 54 87 ; description = "Duplicate the current selected commits " 55 88 ; cmd = Dynamic (fun () -> Cmd ([ "duplicate" ] @ Vars.get_active_revs ())) 56 89 } 57 - ; { key = 'u'; description = "Undo the last operation"; cmd = Cmd [ "undo" ] } 90 + ; { 91 + key = key_map.undo 92 + ; description = "Undo the last operation" 93 + ; cmd = Cmd [ "undo" ] 94 + } 58 95 ; { 59 - key = 'c' 60 - ; description = 61 - "Describe this change and start working on a new rev (same as `describe` then \ 62 - `new`) " 63 - ; cmd = Prompt ("commit msg", [ "commit"; "-m" ]) 96 + key = key_map.commit.menu 97 + ; description = "Commit" 98 + ; cmd = 99 + SubCmd 100 + [ 101 + { 102 + key = key_map.commit.base 103 + ; description = 104 + "Describe this change and start working on a new rev (same as \ 105 + `describe` then `new`)" 106 + ; cmd = 107 + PromptThen ("commit msg", fun msg -> Fun (fun () -> custom_commit msg)) 108 + } 109 + ; { 110 + key = key_map.commit.no_edit 111 + ; description = "Same as commit but without editing the new commit" 112 + ; cmd = 113 + PromptThen 114 + ( "commit msg" 115 + , fun msg -> Fun (fun () -> custom_commit ~edit:false msg) ) 116 + } 117 + ] 64 118 } 65 119 ; { 66 - key = 'S' 120 + key = key_map.split 67 121 ; description = "Split the current commit interacively" 68 122 ; cmd = Dynamic_r (fun rev -> Cmd_I [ "split"; "-r"; rev; "-i" ]) 69 123 } 70 124 ; { 71 - key = 's' 72 - ; description = "Squash/unsquash (has subcommands)" 125 + key = key_map.squash.menu 126 + ; description = "Squash/unsquash" 73 127 ; cmd = 74 128 SubCmd 75 129 [ 76 130 { 77 - key = 's' 131 + key = key_map.squash.into_parent 78 132 ; description = "Squash into parent" 79 133 ; cmd = 80 134 Fun ··· 87 141 jj [ "squash"; "--quiet"; "-r"; rev; "-m"; new_msg ] |> ignore) 88 142 } 89 143 ; { 90 - key = 'S' 144 + key = key_map.squash.into_rev 91 145 ; description = "Squash into any commit" 92 146 ; cmd = 93 147 PromptThen ··· 112 166 ]) ) 113 167 } 114 168 ; { 115 - key = 'u' 169 + key = key_map.squash.unsquash 116 170 ; cmd = Dynamic_r (fun rev -> Cmd_I [ "unsquash"; "-r"; rev; "-i" ]) 117 171 ; description = "Interactivaly unsquash" 118 172 } 119 173 ; { 120 - key = 'i' 174 + key = key_map.squash.interactive_parent 121 175 ; description = "Interactively choose what to squash into parent" 122 176 ; cmd = Dynamic_r (fun rev -> Cmd_I [ "squash"; "-r"; rev; "-i" ]) 123 177 } 124 178 ; { 125 - key = 'I' 179 + key = key_map.squash.interactive_rev 126 180 ; description = "Interactively choose what to squash into a commit" 127 181 ; cmd = 128 182 Dynamic_r ··· 133 187 ] 134 188 } 135 189 ; { 136 - key = 'e' 190 + key = key_map.edit 137 191 ; cmd = Dynamic_r (fun rev -> Cmd [ "edit"; rev ]) 138 192 ; description = "Edit the selected revision" 139 193 } 140 194 ; { 141 - key = 'd' 195 + key = key_map.describe 142 196 ; cmd = 143 197 Dynamic_r (fun rev -> Prompt ("description", [ "describe"; "-r"; rev; "-m" ])) 144 198 ; description = "Describe this revision" 145 199 } 146 200 ; { 147 - key = 'D' 201 + key = key_map.describe_editor 148 202 ; cmd = Dynamic_r (fun rev -> Cmd_I [ "describe"; "-r"; rev ]) 149 203 ; description = "Describe this revision using an editor" 150 204 } 151 205 ; { 152 - key = 'R' 206 + key = key_map.resolve 153 207 ; cmd = Dynamic_r (fun rev -> Cmd_I [ "resolve"; "-r"; rev ]) 154 208 ; description = "Resolve conflicts at this revision" 155 209 } 156 210 ; { 157 - key = 'r' 211 + key = key_map.rebase.menu 158 212 ; description = "Rebase revision " 159 213 ; cmd = 160 214 SubCmd 161 215 [ 162 216 { 163 - key = 'r' 217 + key = key_map.rebase.single 164 218 ; description = "Rebase single revision " 165 219 ; cmd = 166 220 Dynamic_r ··· 168 222 Prompt ("Dest rev for " ^ rev, [ "rebase"; "-r"; rev; "-d" ])) 169 223 } 170 224 ; { 171 - key = 's' 225 + key = key_map.rebase.with_descendants 172 226 ; description = "Rebase revision and its decendents" 173 227 ; cmd = 174 228 Dynamic_r ··· 178 232 , [ "rebase"; "-s"; rev; "-d" ] )) 179 233 } 180 234 ; { 181 - key = 'b' 235 + key = key_map.rebase.with_bookmark 182 236 ; description = "Rebase revision and all other revissions on its bookmark" 183 237 ; cmd = 184 238 Dynamic_r ··· 190 244 ] 191 245 } 192 246 ; { 193 - key = 'g' 247 + key = key_map.git.menu 194 248 ; description = "Git commands" 195 249 ; cmd = 196 250 SubCmd 197 251 [ 198 252 { 199 - key = 'p' 253 + key = key_map.git.push 200 254 ; description = "git push" 201 255 ; cmd = 202 256 Fun 203 257 (fun _ -> 258 + let revs = Vars.get_active_revs () in 204 259 let subcmds = 205 260 [ 206 261 { 207 - key = 'y' 262 + key = key_map.git.push 208 263 ; description = "proceed" 209 - ; cmd = Cmd [ "git"; "push" ] 264 + ; cmd = Cmd ([ "git"; "push"; "--allow-new"; "-r" ] @ revs) 210 265 } 211 266 ; { 212 - key = 'n' 267 + key = key_map.git.fetch 213 268 ; description = "exit" 214 269 ; cmd = 215 270 Fun ··· 220 275 ] 221 276 in 222 277 let log = 223 - jj_no_log ~get_stderr:true [ "git"; "push"; "--dry-run" ] 278 + jj_no_log 279 + ~get_stderr:true 280 + ([ "git"; "push"; "--allow-new"; "--dry-run"; "-r" ] @ revs) 224 281 |> AnsiReverse.colored_string 225 282 |> Ui.atom 226 283 |> Lwd.pure ··· 229 286 ui_state.show_popup $= Some (ui, "Git push will:"); 230 287 ui_state.input $= `Mode (command_input ~is_sub:true subcmds)) 231 288 } 232 - ; { key = 'f'; description = "git fetch"; cmd = Cmd [ "git"; "fetch" ] } 289 + ; { 290 + key = key_map.git.fetch 291 + ; description = "git fetch" 292 + ; cmd = Cmd [ "git"; "fetch" ] 293 + } 233 294 ] 234 295 } 235 296 ; { 236 - key = 'z' 297 + key = key_map.parallelize 237 298 ; description = 238 299 "Parallelize commits. Takes 2 commits and makes them have the\n\ 239 300 same parent and child. Run `jj parallelize` --help for details" ··· 243 304 , fun x -> Cmd ([ "paralellize" ] @ (x |> String.split_on_char ' ')) ) 244 305 } 245 306 ; { 246 - key = 'a' 307 + key = key_map.abandon 247 308 ; description = "Abandon this change(removes just this change and rebases parents)" 248 309 ; cmd = 249 - Dynamic_r 250 - (fun rev -> 251 - Cmd_r [ "abandon" ] |> confirm_prompt ("abandon the revision:" ^ rev)) 310 + Dynamic 311 + (fun () -> 312 + let revs = Vars.get_active_revs () in 313 + Cmd ([ "abandon" ] @ revs) 314 + |> confirm_prompt ("abandon the revisions:\n" ^ (revs |> String.concat "\n"))) 252 315 } 253 316 ; { 254 - key = 'b' 317 + key = key_map.bookmark.menu 255 318 ; description = "Bookmark commands" 256 319 ; cmd = 257 320 SubCmd 258 321 [ 259 322 { 260 - key = 'c' 323 + key = key_map.bookmark.create 261 324 ; description = "Create new bookmark" 262 325 ; cmd = 263 326 PromptThen ··· 268 331 @ [ x |> String.map (fun c -> if c = ' ' then '_' else c) ]) ) 269 332 } 270 333 ; { 271 - key = 'd' 334 + key = key_map.bookmark.delete 272 335 ; description = "Delete bookmark" 273 336 ; cmd = 274 337 bookmark_select_prompt ··· 283 346 bookmark)) 284 347 } 285 348 ; { 286 - key = 'f' 349 + key = key_map.bookmark.forget 287 350 ; description = "Forget bookmark" 288 351 ; cmd = 289 352 bookmark_select_prompt ··· 298 361 bookmark)) 299 362 } 300 363 ; { 301 - key = 'r' 364 + key = key_map.bookmark.rename 302 365 ; description = "Rename bookmark" 303 366 ; cmd = 304 367 bookmark_select_prompt ··· 309 372 Prompt ("New bookmark name", [ "bookmark"; "rename"; curr_name ])) 310 373 } 311 374 ; { 312 - key = 's' 375 + key = key_map.bookmark.set 313 376 ; description = "Set bookmark to this change" 314 377 ; cmd = 315 378 Dynamic_r ··· 324 387 ])) 325 388 } 326 389 ; { 327 - key = 't' 390 + key = key_map.bookmark.track 328 391 ; description = "track given remote bookmark" 329 392 ; cmd = 330 393 bookmark_select_prompt ··· 333 396 (fun bookmark -> Cmd [ "bookmark"; "track"; bookmark ]) 334 397 } 335 398 ; { 336 - key = 'u' 399 + key = key_map.bookmark.untrack 337 400 ; description = "untrack given remote bookmark" 338 401 ; cmd = 339 402 bookmark_select_prompt ··· 344 407 ] 345 408 } 346 409 ; { 347 - key = 'f' 410 + key = key_map.filter 348 411 ; description = "Filter using revset" 349 412 ; cmd = 350 413 PromptThen ··· 357 420 else Vars.ui_state.revset $= Some revset) ) 358 421 } 359 422 ] 423 + 424 + and command_mapping = ref None 425 + 426 + and get_command_mapping () = 427 + match !command_mapping with 428 + | Some mapping -> 429 + mapping 430 + | None -> 431 + let mapping = make_command_mapping (Lwd.peek ui_state.config).key_map.graph in 432 + command_mapping := Some mapping; 433 + mapping 360 434 ;; 361 435 362 436 (*TODO:make a custom widget the renders the commit with and without selection. ··· 417 491 W.Lists.(Selectable data) 418 492 | `Filler x -> 419 493 W.Lists.( 420 - Filler 421 - (" " ^ x 422 - |> Jj_tui.AnsiReverse.colored_string 423 - |> Ui.atom 424 - |> Lwd.pure))) 494 + Filler (" " ^ x |> Jj_tui.AnsiReverse.colored_string |> Ui.atom |> Lwd.pure))) 425 495 in 426 496 items 427 497 in ··· 430 500 | `Enter, [] -> 431 501 Focus.request_reversable summary_focus; 432 502 `Handled 433 - | `ASCII k, [] -> 434 - handleInputs command_mapping k 503 + | k -> 504 + handleInputs (get_command_mapping ()) k 435 505 | _ -> 436 506 `Unhandled 437 507 in
+24 -12
jj_tui/bin/jj_commands.ml
··· 3 3 We can then run a command matching a key or generate a documentation UI element showing all available commands *) 4 4 5 5 open Jj_tui.Logging 6 - 6 + open Jj_tui.Key_map 7 + open Jj_tui.Key_map 7 8 (** Internal to this module. I'm trying this out as a way to avoid .mli files*) 8 9 module Shared = struct 9 10 type cmd_args = string list [@@deriving show] ··· 45 46 46 47 (** A command that should be run when it's key is pressed*) 47 48 and 'a command = { 48 - key : char 49 + key : key 49 50 ; description : string 50 51 ; cmd : 'a command_variant 51 52 } ··· 85 86 () 86 87 ;; 87 88 88 - let render_command_line ~indent_level key desc = 89 + let render_command_line ~indent_level key_name desc = 89 90 let indent = String.init (indent_level * 2) (fun _ -> ' ') in 90 91 I.hcat 91 92 [ 92 93 I.string A.empty indent 93 - ; I.uchars (A.fg A.lightblue) key 94 + ; I.string (A.fg A.lightblue) key_name 94 95 ; I.strf " " 95 96 ; desc |> String.split_on_char '\n' |> List.map (I.string A.empty) |> I.vcat 96 97 ] ··· 117 118 | Prompt_r _ 118 119 | Dynamic_r _ ) 119 120 } -> 120 - [ render_command_line ~indent_level [| key |> Uchar.of_char |] description ] 121 + [ render_command_line ~indent_level (key_to_string key) description ] 121 122 | { key; description; cmd = SubCmd subs } -> 122 - render_command_line ~indent_level [| key |> Uchar.of_char |] description 123 + render_command_line ~indent_level (key_to_string key) description 123 124 :: render_commands ~indent_level:(indent_level + 1) subs 124 125 ;; 125 126 ··· 127 128 let move_command = 128 129 render_command_line 129 130 ~indent_level:0 130 - ("Arrows" |> String.to_seq |> Seq.map Uchar.of_char |> Array.of_seq) 131 + ("Arrows" ) 131 132 "navigation between windows" 132 133 in 133 134 ((commands |> render_commands) @ if include_arrows then [ move_command ] else []) ··· 247 248 and command_input ~is_sub keymap key = 248 249 (* Use exceptions so we can break out of the list*) 249 250 let input = Lwd.peek ui_state.input in 251 + 250 252 try 251 - keymap 252 - |> List.iter (fun cmd -> 253 - if cmd.key == key then handleCommand cmd.description cmd.cmd else ()); 253 + keymap 254 + |> List.iter (fun cmd -> 255 + (*log keys*) 256 + 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 + ); 254 265 `Unhandled 255 266 with 256 267 | Handled -> ··· 260 271 | Jj_process.JJError (cmd, error) -> 261 272 handle_jj_error cmd error; 262 273 `Unhandled 274 + 263 275 264 276 and command_no_input description cmd = 265 277 (* Use exceptions so we can break out of the list*) ··· 287 299 let rec default_list = 288 300 [ 289 301 { 290 - key = '?' 302 + key = { key = '?'; modifiers = [] } 291 303 ; description = "Show help" 292 304 ; cmd = 293 305 Fun ··· 304 316 305 317 (**`Prompt`:Allows running one command and then running another using the input of the first*) 306 318 let confirm_prompt prompt cmd = 307 - SubCmd [ { key = 'y'; description = "Yes I want to " ^ prompt; cmd } ] 319 + SubCmd [ { key = { key = 'y'; modifiers = [] }; description = "Yes I want to " ^ prompt; cmd } ] 308 320 ;; 309 321 310 322 (** Handles raw command mapping without regard for modes or the current intput state. Should be used when setting a new input mode*)
+9 -2
jj_tui/bin/jj_ui.ml
··· 5 5 open Global_funcs 6 6 open Jj_tui.Util 7 7 open Jj_tui 8 + open Logging 8 9 module Pio = Picos_io 9 10 10 11 module Ui = struct ··· 55 56 ; (function 56 57 | `ASCII 'q', _ -> 57 58 Vars.quit $= true; 59 + `Handled 60 + |`Arrow _,[`Meta]-> 61 + (*totatlly disable all forced focus navigation*) 58 62 `Handled 59 63 | _ -> 60 64 `Unhandled) ··· 131 135 |> W.is_focused ~focus:branch_focus (fun ui focused -> 132 136 ui 133 137 |> Ui.keyboard_area (function 134 - | `ASCII k, [] -> 138 + | k -> 135 139 Jj_commands.handleInputs Jj_commands.default_list k 136 140 | _ -> 137 141 `Unhandled) ··· 158 162 |> W.Overlay.selection_list_prompt_filterable 159 163 ~show_prompt_var:ui_state.show_string_selection_prompt 160 164 |> inputs ~custom:(function 161 - | `ASCII k, [] -> 165 + | k -> 162 166 Jj_commands.handleInputs Jj_commands.default_list k 163 167 | `Arrow _, [ `Ctrl ] 164 168 | `Arrow _, [ `Meta ] ··· 183 187 ;; 184 188 185 189 let mainUi () = 190 + (* first lets load the config*) 191 + Vars.config $= Config.load_config(); 192 + [%log info "loaded config"]; 186 193 (*we want to initialize our states and keep them up to date*) 187 194 let$* startup_result = check_startup () in 188 195 match startup_result with
+53
jj_tui/lib/config.ml
··· 1 + open Util 2 + open Logging 3 + 4 + type t = { key_map : Key_map.t[@updater] } [@@deriving yaml, record_updater ~derive: yaml] 5 + 6 + 7 + let default_config:t = 8 + { 9 + key_map= Key_map.default 10 + } 11 + ;; 12 + 13 + let get_config_dir () = 14 + let os = Os.poll_os () in 15 + let config_home = 16 + match os with 17 + | Some "linux" -> 18 + Sys.getenv_opt "XDG_CONFIG_HOME" |> Option.value ~default:"~/.config" 19 + | Some "macos" -> 20 + let home = Unix.getenv "HOME" in 21 + Filename.concat home "Library/Preferences" 22 + | _ -> 23 + Sys.getenv_opt "HOME" 24 + |> Option.map (Filename.concat "config") 25 + |> Option.value ~default:"./config" 26 + in 27 + Filename.concat config_home "jj_tui" 28 + ;; 29 + 30 + 31 + let load_config () = 32 + [%log info "Loading config..."]; 33 + let config_file = Filename.concat (get_config_dir ()) "config.json" in 34 + try 35 + let ic = open_in config_file in 36 + let content = really_input_string ic (in_channel_length ic) in 37 + close_in ic; 38 + let json = Yaml.of_string_exn content in 39 + match t_update_t_of_yaml json with 40 + | Ok (config_update) -> 41 + [%log info "Config loaded!"]; 42 + default_config |> t_apply_update config_update 43 + | Error (`Msg msg) -> 44 + [%log warn "Error parsing config: %s" msg]; 45 + default_config 46 + with 47 + | Sys_error _ -> 48 + [%log info "No config file found at %s, using defaults" config_file]; 49 + default_config 50 + | ex -> 51 + [%log warn "Error loading config: %s" (Printexc.to_string ex)]; 52 + default_config 53 + ;;
+11 -2
jj_tui/lib/dune
··· 13 13 picos_std.finally 14 14 picos_std.structured 15 15 logs 16 - re) 16 + re 17 + yojson 18 + yaml 19 + ppx_deriving_yojson.runtime) 17 20 (preprocess 18 - (pps ppx_expect logs-ppx ppx_deriving.std))) 21 + (pps 22 + ppx_expect 23 + logs-ppx 24 + ppx_deriving.std 25 + ppx_deriving_yojson 26 + ppx_deriving_yaml 27 + ppx_record_updater)))
+224
jj_tui/lib/key_map.ml
··· 1 + type modifier = [ `Meta | `Shift | `Ctrl ] 2 + 3 + type key = { 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_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] 75 + 76 + type git_keys = { 77 + menu:key; 78 + push : key; 79 + fetch : key; 80 + }[@@deriving yaml, record_updater ~derive: yaml] 81 + 82 + type squash_keys = { 83 + menu:key; 84 + into_parent : key; 85 + into_rev : key; 86 + unsquash : key; 87 + interactive_parent : key; 88 + interactive_rev : key; 89 + }[@@deriving yaml, record_updater ~derive: yaml] 90 + 91 + type rebase_keys = { 92 + menu:key; 93 + single : key; 94 + with_descendants : key; 95 + with_bookmark : key; 96 + }[@@deriving yaml, record_updater ~derive: yaml] 97 + 98 + type file_keys = { 99 + show_help : key; 100 + move_to_rev : key; 101 + move_to_child : key; 102 + move_to_parent : key; 103 + discard : key; 104 + }[@@deriving yaml, record_updater ~derive: yaml] 105 + 106 + type new_child_keys= { 107 + menu:key; 108 + base:key; 109 + no_edit:key; 110 + inline:key; 111 + inline_no_edit:key; 112 + }[@@deriving yaml, record_updater ~derive: yaml] 113 + 114 + type commit_keys = { 115 + menu:key; 116 + base:key; 117 + no_edit:key; 118 + open_editor:key; 119 + }[@@deriving yaml, record_updater ~derive: yaml] 120 + 121 + type graph_keys = { 122 + show_help : key; 123 + prev : key; 124 + new_child : new_child_keys; [@updater] 125 + duplicate : key; 126 + undo : key; 127 + commit : commit_keys;[@updater] 128 + split : key; 129 + squash : squash_keys;[@updater] 130 + edit : key; 131 + describe : key; 132 + describe_editor : key; 133 + resolve : key; 134 + rebase : rebase_keys;[@updater] 135 + git : git_keys;[@updater] 136 + parallelize : key; 137 + abandon : key; 138 + bookmark : bookmark_keys;[@updater] 139 + filter : key; 140 + }[@@deriving yaml, record_updater ~derive: yaml] 141 + 142 + type t = { 143 + graph : graph_keys; [@updater] 144 + file : file_keys;[@updater] 145 + }[@@deriving yaml, record_updater ~derive: yaml] 146 + 147 + (* Helper to create a simple key without modifiers *) 148 + let simple_key c = { key = c; modifiers = [] } 149 + 150 + (* Default key bindings matching current implementation *) 151 + let default:t = { 152 + graph = { 153 + show_help = simple_key '?'; 154 + prev = simple_key 'P'; 155 + 156 + new_child={ 157 + menu= simple_key 'n'; 158 + base= simple_key 'n'; 159 + no_edit= key_of_string_exn "N"; 160 + inline= simple_key 'i'; 161 + inline_no_edit= simple_key 'I'; 162 + }; 163 + duplicate = simple_key 'y'; 164 + undo = simple_key 'u'; 165 + commit = { 166 + menu = simple_key 'c'; 167 + base = simple_key 'c'; 168 + no_edit = simple_key 'C'; 169 + open_editor = simple_key 'D'; 170 + }; 171 + split = simple_key 'S'; 172 + squash = { 173 + menu = simple_key 's'; 174 + into_parent = simple_key 's'; 175 + into_rev = simple_key 'S'; 176 + unsquash = simple_key 'u'; 177 + interactive_parent = simple_key 'i'; 178 + interactive_rev = simple_key 'I'; 179 + }; 180 + edit = simple_key 'e'; 181 + describe = simple_key 'd'; 182 + describe_editor = simple_key 'D'; 183 + resolve = simple_key 'R'; 184 + rebase = { 185 + menu = simple_key 'r'; 186 + single = simple_key 'r'; 187 + with_descendants = simple_key 's'; 188 + with_bookmark = simple_key 'b'; 189 + }; 190 + git = { 191 + menu = simple_key 'g'; 192 + push = simple_key 'p'; 193 + fetch = simple_key 'f'; 194 + }; 195 + parallelize = simple_key 'z'; 196 + abandon = simple_key 'a'; 197 + bookmark = { 198 + menu = simple_key 'b'; 199 + create = simple_key 'c'; 200 + delete = simple_key 'd'; 201 + forget = simple_key 'f'; 202 + rename = simple_key 'r'; 203 + set = simple_key 's'; 204 + track = simple_key 't'; 205 + untrack = simple_key 'u'; 206 + }; 207 + filter = simple_key 'f'; 208 + }; 209 + file = { 210 + show_help = simple_key '?'; 211 + move_to_rev = simple_key 'm'; 212 + move_to_child = simple_key 'N'; 213 + move_to_parent = simple_key 'P'; 214 + discard = simple_key 'd'; 215 + }; 216 + } 217 + 218 + (* Example usage: 219 + key_of_string "C+S+A+a" = Ok { key = 'a'; modifiers = [`Ctrl; `Shift; `Meta] } 220 + key_of_string "C+x" = Ok { key = 'x'; modifiers = [`Ctrl] } 221 + key_of_string "a" = Ok { key = 'a'; modifiers = [] } 222 + key_of_string "C+S+" = Error "No key character provided" 223 + key_of_string "X+a" = Error "Unknown modifier: X" 224 + *)
+4 -18
jj_tui/lib/logging.ml
··· 14 14 ms 15 15 ;; 16 16 17 + 18 + 17 19 module Log = struct 18 20 let timestamp_tag = 19 21 Logs.Tag.def "timestamp" ~doc:"Timestamp" (fun fmt tm -> ··· 90 92 match String.lowercase_ascii raw with "darwin" | "osx" -> "macos" | s -> s 91 93 ;; 92 94 93 - let poll_os () = 94 - let raw = 95 - match Sys.os_type with 96 - | "Unix" -> 97 - (try 98 - let uname_in = Unix.open_process_args_in "uname" [| "uname"; "-s" |] in 99 - let str = uname_in |> In_channel.input_all in 100 - Unix.wait()|>ignore; 101 - Some (str |> String.lowercase_ascii |> String.trim) 102 - with 103 - | _ -> 104 - None) 105 - | s -> 106 - Some (s |> String.lowercase_ascii |> String.trim) 107 - in 108 - match raw with None | Some "" -> None | Some s -> Some (normalise_os s) 109 - ;; 95 + 110 96 111 97 (*tries to get the logging dir for macos and linux*) 112 98 let get_log_dir () = 113 99 try 114 - let os = poll_os () in 100 + let os = Os.poll_os () in 115 101 let state_home = 116 102 try 117 103 match os with
+23
jj_tui/lib/os.ml
··· 1 + module Internal = struct 2 + let normalise_os raw = 3 + match String.lowercase_ascii raw with "darwin" | "osx" -> "macos" | s -> s 4 + ;; 5 + end 6 + 7 + let poll_os () = 8 + let raw = 9 + match Sys.os_type with 10 + | "Unix" -> 11 + (try 12 + let uname_in = Unix.open_process_args_in "uname" [| "uname"; "-s" |] in 13 + let str = uname_in |> In_channel.input_all in 14 + Unix.wait()|>ignore; 15 + Some (str |> String.lowercase_ascii |> String.trim) 16 + with 17 + | _ -> 18 + None) 19 + | s -> 20 + Some (s |> String.lowercase_ascii |> String.trim) 21 + in 22 + match raw with None | Some "" -> None | Some s -> Some (Internal.normalise_os s) 23 + ;;
+24
jj_tui/lib/util.ml
··· 73 73 [] (* If list is empty or has only one element, return empty list *) 74 74 ;; 75 75 76 + module StrMap_=Map.Make(String) 77 + module StrMap =struct 78 + open Yojson.Safe 79 + include StrMap_ 80 + type t= string StrMap_.t 81 + let to_yojson map : Yojson.Safe.t = 82 + `Assoc (bindings map |> List.map (fun (k, v) -> k, `String v)) 83 + ;; 84 + let of_yojson (json:Yojson.Safe.t) = 85 + 86 + match json with 87 + |`Assoc(items)-> 88 + items|>List.map (fun (k,v)-> 89 + (v|>[%of_yojson: string ]) 90 + |>Result.map(fun x-> k,x) 91 + ) 92 + |> 93 + Base.Result.all 94 + |>Result.map of_list 95 + 96 + |_-> Error ("Not an object") 97 + 98 + ;; 99 + end