From 3c8616deba51c3bd8b31d25a0811172ffa064858 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 30 Jul 2025 19:12:45 +0300 Subject: [PATCH] unify default configs the definitions are unified alright. it's just not supported yet :D the idea being that tek offers to write out the default configs to ~/.config/tek-v0 where the user can customize them. --- {crates/app/examples => .old}/midi_import.rs | 0 Cargo.lock | 7 + Cargo.toml | 1 + config/bindings.edn | 167 +++++++++++++++ config/config_arranger.edn | 31 --- config/config_groovebox.edn | 28 --- config/config_sampler.edn | 13 -- config/config_sequencer.edn | 19 -- config/config_transport.edn | 9 - config/keys_arranger.edn | 17 -- config/keys_arranger_clip.edn | 1 - config/keys_arranger_device.edn | 0 config/keys_arranger_input.rs | 0 config/keys_arranger_output.rs | 0 config/keys_arranger_scene.edn | 0 config/keys_arranger_track.rs | 0 config/keys_browser.edn | 8 - config/keys_clip.edn | 8 - config/keys_clock.edn | 2 - config/keys_device_add.edn | 2 - config/keys_dialog.edn | 0 config/keys_editor.edn | 26 --- config/keys_global.edn | 9 - config/keys_groovebox.edn | 3 - config/keys_length.edn | 6 - config/keys_message.edn | 2 - config/keys_mix.edn | 0 config/keys_pool.edn | 12 -- config/keys_pool_file.edn | 8 - config/keys_rename.edn | 4 - config/keys_sampler.edn | 12 -- config/keys_scene.edn | 7 - config/keys_sequencer.edn | 4 - config/keys_track.edn | 11 - config/templates.edn | 74 +++++++ crates/app/Cargo.toml | 1 + crates/app/src/api.rs | 4 +- crates/app/src/config.rs | 111 ++++++++++ crates/app/src/lib.rs | 1 + crates/app/src/model.rs | 202 +++++-------------- crates/app/src/view.rs | 8 +- crates/cli/tek.rs | 86 ++------ deps/tengri | 2 +- 43 files changed, 441 insertions(+), 465 deletions(-) rename {crates/app/examples => .old}/midi_import.rs (100%) create mode 100644 config/bindings.edn delete mode 100644 config/config_arranger.edn delete mode 100644 config/config_groovebox.edn delete mode 100644 config/config_sampler.edn delete mode 100644 config/config_sequencer.edn delete mode 100644 config/config_transport.edn delete mode 100644 config/keys_arranger.edn delete mode 100644 config/keys_arranger_clip.edn delete mode 100644 config/keys_arranger_device.edn delete mode 100644 config/keys_arranger_input.rs delete mode 100644 config/keys_arranger_output.rs delete mode 100644 config/keys_arranger_scene.edn delete mode 100644 config/keys_arranger_track.rs delete mode 100644 config/keys_browser.edn delete mode 100644 config/keys_clip.edn delete mode 100644 config/keys_clock.edn delete mode 100644 config/keys_device_add.edn delete mode 100644 config/keys_dialog.edn delete mode 100644 config/keys_editor.edn delete mode 100644 config/keys_global.edn delete mode 100644 config/keys_groovebox.edn delete mode 100644 config/keys_length.edn delete mode 100644 config/keys_message.edn delete mode 100644 config/keys_mix.edn delete mode 100644 config/keys_pool.edn delete mode 100644 config/keys_pool_file.edn delete mode 100644 config/keys_rename.edn delete mode 100644 config/keys_sampler.edn delete mode 100644 config/keys_scene.edn delete mode 100644 config/keys_sequencer.edn delete mode 100644 config/keys_track.edn create mode 100644 config/templates.edn create mode 100644 crates/app/src/config.rs diff --git a/crates/app/examples/midi_import.rs b/.old/midi_import.rs similarity index 100% rename from crates/app/examples/midi_import.rs rename to .old/midi_import.rs diff --git a/Cargo.lock b/Cargo.lock index 1b09411b..f0a12eed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2395,6 +2395,7 @@ dependencies = [ "tengri", "tengri_proc", "toml", + "xdg", ] [[package]] @@ -3408,6 +3409,12 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" +[[package]] +name = "xdg" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5" + [[package]] name = "xkbcommon-dl" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index 1c1ef56f..35b9e4ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ toml = { version = "0.9.2" } uuid = { version = "1.10.0", features = [ "v4" ] } wavers = { version = "1.4.3" } winit = { version = "0.30.4", features = [ "x11" ] } +xdg = { version = "3.0.0" } #once_cell = "1.19.0" #no_deadlocks = "1.3.2" #suil-rs = { path = "../suil" } diff --git a/config/bindings.edn b/config/bindings.edn new file mode 100644 index 00000000..28db6897 --- /dev/null +++ b/config/bindings.edn @@ -0,0 +1,167 @@ +(module :global + (@esc dialog/hide) + (@f1 dialog/show :dialog/help) + (@f6 dialog/show :dialog/save) + (@f8 dialog/show :dialog/options) + (@f9 dialog/show :dialog/load) + (@f10 dialog/show :dialog/quit) + (@u history/undo 1) + (@r history/redo 1)) + +(module :clock + (@space clock/toggle 0) + (@shift/space clock/toggle 0)) + +(module :arranger + (@c color) + (@q launch) + (@tab project/edit) + (@enter project/edit) + (@escape project/home) + (@shift/I project/input/add) + (@shift/O project/output/add) + (@shift/S project/scene/add) + (@shift/T project/track/add) + (@shift/D dialog/show :dialog/device) + (@up select :select/scene/prev) + (@down select :select/scene/next) + (@left select :select/track/prev) + (@right select :select/track/next) + (@t select :select/track) + (@s select :select/scene)) + +(module :track + (@delete track/delete :track) + (@q track/launch :track) + (@c track/color :track) + (@comma track/prev) + (@period track/next) + (@lt track/swap/prev) + (@gt track/swap/next) + (@r track/rec) + (@m track/mon) + (@p track/play) + (@P track/solo)) + +(module :scene + (@delete scene/delete :scene) + (@q scene/launch :scene) + (@c scene/color :scene) + (@comma scene/prev) + (@period scene/next) + (@lt scene/swap/prev) + (@gt scene/swap/next)) + +(module :clip + (@g clip/get) + (@p clip/put) + (@delete clip/del) + (@comma clip/prev) + (@period clip/next) + (@lt clip/swap/prev) + (@gt clip/swap/next) + (@l clip/loop/toggle)) + +(module :browser + (@escape browser/cancel) + (@return browser/confirm) + (@up browser/cursor/set :browser/cursor/prev) + (@down browser/cursor/set :browser/cursor/next) + (@right browser/address/set :browser/address/selected) + (@left browser/address/set :browser/address/parent) + (:char browser/filter/append :char) + (@backspace browser/filter/delete :last)) + +(module :device/add + (@up dialog :dialog/device/prev) + (@down dialog :dialog/device/next)) + +(module :editor + (@left editor/time/set :time/pos/prev) + (@shift/left editor/time/set :time/pos/prev/fine) + (@right editor/time/set :time/pos/next) + (@shift/right editor/time/set :time/pos/next/fine) + (@equal editor/zoom/set :time/zoom/prev) + (@minus editor/zoom/set :time/zoom/next) + (@plus editor/zoom/set :time/zoom/next/fine) + (@underscore editor/zoom/set :time/zoom/prev/fine) + (@z editor/lock/set) + (@comma editor/length/set :note/len/prev) + (@period editor/length/set :note/len/next) + (@lt editor/length/set :note/len/prev) + (@gt editor/length/set :note/len/next) + (@up editor/pitch/set :note/pos/next) + (@down editor/pitch/set :note/pos/prev) + (@pgup editor/pitch/set :note/pos/next/octave) + (@pgdn editor/pitch/set :note/pos/prev/octave) + (@a editor/append :true) + (@enter editor/append :false) + (@del editor/delete/note) + (@shift/del editor/delete/note)) + +(module :groovebox + (@r sampler/record/toggle :sample) + (@tab focus/next) + (@shift/tab focus/prev)) + +(module :length + (@up inc) + (@down dec) + (@right next) + (@left prev) + (@return set :length) + (@escape cancel)) + +(module :message + (@esc message/dismiss) + (@enter message/dismiss)) + +(module :pool + (@n rename/begin) + (@t length/begin) + (@m import/begin) + (@x export/begin) + (@c clip/color :clip :random/color) + (@openbracket select :clip/prev) + (@closebracket select :clip/next) + (@lt swap :clip :clip/prev) + (@gt swap :clip :clip/next) + (@delete clip/delete :clip) + (@shift/A clip/add :after :new/clip) + (@shift/D clip/add :after :cloned/clip)) + +(module :pool/file + (@up select :prev) + (@down select :next) + (@right chdir :selected) + (@left chdir :parent) + (@return confirm) + (@escape cancel) + (:char append :char) + (@backspace delete :last)) + +(module :rename + (:char append :char) + (@backspace delete :last) + (@return confirm) + (@escape cancel)) + +(module :sampler + (@up sampler/select :sample/above) + (@down sampler/select :sample/below) + (@left sampler/select :sample/to/left) + (@right sampler/select :sample/to/right) + + (@r sampler/record/toggle :sample/selected) + (@shift/R sampler/record/cancel) + (@p sampler/play/sample :sample/selected) + (@P sampler/stop/sample :sample/selected) + + (@shift/f6 dialog :dialog/export/sample) + (@shift/f9 dialog :dialog/import/sample)) + +(module :sequencer + (@c color) + (@q launch) + (@shift/I input/add) + (@shift/O output/add)) diff --git a/config/config_arranger.edn b/config/config_arranger.edn deleted file mode 100644 index 36577c97..00000000 --- a/config/config_arranger.edn +++ /dev/null @@ -1,31 +0,0 @@ -(name - "Arranger") -(info - "A grid of launchable clips arranged by track and scene.") -(keys - (cond :focus-editor (load "./keys_editor.edn")) - (cond :focus-dialog (load "./keys_dialog.edn")) - (cond :focus-message (load "./keys_message.edn")) - (cond :focus-device-add (load "./keys_device_add.edn")) - (cond :focus-browser (load "./keys_browser.edn")) - (cond :focus-pool-rename (load "./keys_rename.edn")) - (cond :focus-pool-length (load "./keys_length.edn")) - (cond :focus-clip (load "./keys_clip.edn")) - (cond :focus-track (load "./keys_track.edn")) - (cond :focus-scene (load "./keys_scene.edn")) - (cond :focus-mix (load "./keys_mix.edn")) - (load "./keys_clock.edn") - (load "./keys_arranger.edn") - (load "./keys_global.edn")) -(view - (bsp/a :view-dialog - (bsp/w :view-meters-output - (bsp/e :view-meters-input - (bsp/n (fixed/y 2 :view-status-h2) - (bsp/n :view-tracks-inputs - (bsp/s :view-tracks-devices - (bsp/s :view-tracks-outputs - (bsp/s :view-tracks-names - (fill/xy (either :focus-editor - (bsp/e :view-scenes-names :view-editor) - :view-scenes))))))))))) diff --git a/config/config_groovebox.edn b/config/config_groovebox.edn deleted file mode 100644 index 136b216a..00000000 --- a/config/config_groovebox.edn +++ /dev/null @@ -1,28 +0,0 @@ -(name "Groovebox") - -(info "A sequencer with built-in sampler.") - -(keys - (layer-if :focus-browser "./keys_browser.edn") - (layer-if :focus-pool-rename "./keys_rename.edn") - (layer-if :focus-pool-length "./keys_length.edn") - (layer "./keys_clock.edn") - (layer "./keys_editor.edn") - (layer "./keys_sampler.edn") - (layer "./keys_global.edn")) - -(view (bsp/a :view-dialog (bsp/w :view-meters-output (bsp/e :view-meters-input - (bsp/w - (fill/y (align/n - (bsp/s :view-midi-ins-status - (bsp/s :view-midi-outs-status - (bsp/s :view-audio-ins-status - (bsp/s :view-audio-outs-status - :view-pool)))))) - (bsp/n - (fixed/y :h-sample-detail - (bsp/e (fill/y (fixed/x 20 (align/nw :view-sample-status))) - :view-sample-viewer)) - (bsp/e - (fill/y (align/n (bsp/s :view-status-v :view-editor-status))) - (bsp/e :view-samples-keys :view-editor)))))))) diff --git a/config/config_sampler.edn b/config/config_sampler.edn deleted file mode 100644 index 2739e312..00000000 --- a/config/config_sampler.edn +++ /dev/null @@ -1,13 +0,0 @@ -(name "Sampler") - -(info "A sampling soundboard.") - -(keys - (layer "./keys_sampler.edn") - (layer "./keys_global.edn")) - -(view - (bsp/a :view-dialog - (bsp/s (fixed/y 1 :view-transport) - (bsp/n (fixed/y 1 :view-status) - (fill/xy :view-samples-grid))))) diff --git a/config/config_sequencer.edn b/config/config_sequencer.edn deleted file mode 100644 index 868bc0ba..00000000 --- a/config/config_sequencer.edn +++ /dev/null @@ -1,19 +0,0 @@ -(name "Sequencer") - -(info "A MIDI sequencer.") - -(keys - (layer-if :focus-browser "./keys_browser.edn") - (layer-if :mode-pool-rename "./keys_rename.edn") - (layer-if :mode-pool-length "./keys_length.edn") - (layer "./keys_editor.edn") - (layer "./keys_clock.edn") - (layer "./keys_global.edn")) - -(view - (bsp/a :view-dialog - (bsp/s (fixed/y 1 :view-transport) - (bsp/n (fixed/y 1 :view-status) - (fill/xy (bsp/a - (fill/xy (align/e :view-pool)) - :view-editor))))) diff --git a/config/config_transport.edn b/config/config_transport.edn deleted file mode 100644 index 23ca4f6e..00000000 --- a/config/config_transport.edn +++ /dev/null @@ -1,9 +0,0 @@ -(name "Transport") - -(info "A JACK transport controller.") - -(keys - (layer "./keys_clock.edn") - (layer "./keys_global.edn")) - -(view :view-transport) diff --git a/config/keys_arranger.edn b/config/keys_arranger.edn deleted file mode 100644 index e3949d3f..00000000 --- a/config/keys_arranger.edn +++ /dev/null @@ -1,17 +0,0 @@ -(@c color) -(@q launch) -(@tab project edit) -(@enter project edit) -(@escape project home) -(@shift-I project input-add) -(@shift-O project output-add) -(@shift-S project scene-add) -(@shift-T project track-add) -(@shift-D dialog open :dialog-device) - -(@up select :select-scene-prev) -(@down select :select-scene-next) -(@left select :select-track-prev) -(@right select :select-track-next) -(@t select :select-track) -(@s select :select-scene) diff --git a/config/keys_arranger_clip.edn b/config/keys_arranger_clip.edn deleted file mode 100644 index 8b137891..00000000 --- a/config/keys_arranger_clip.edn +++ /dev/null @@ -1 +0,0 @@ - diff --git a/config/keys_arranger_device.edn b/config/keys_arranger_device.edn deleted file mode 100644 index e69de29b..00000000 diff --git a/config/keys_arranger_input.rs b/config/keys_arranger_input.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/config/keys_arranger_output.rs b/config/keys_arranger_output.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/config/keys_arranger_scene.edn b/config/keys_arranger_scene.edn deleted file mode 100644 index e69de29b..00000000 diff --git a/config/keys_arranger_track.rs b/config/keys_arranger_track.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/config/keys_browser.edn b/config/keys_browser.edn deleted file mode 100644 index b950e989..00000000 --- a/config/keys_browser.edn +++ /dev/null @@ -1,8 +0,0 @@ -(@escape browser cancel) -(@return browser confirm) -(@up browser set-cursor :browser-cursor-prev) -(@down browser set-cursor :browser-cursor-next) -(@right browser set-address :browser-address-selected) -(@left browser set-address :browser-address-parent) -(:char browser append-to-search ;char) -(@backspace browser delete-from-search :last) diff --git a/config/keys_clip.edn b/config/keys_clip.edn deleted file mode 100644 index aa39a5e7..00000000 --- a/config/keys_clip.edn +++ /dev/null @@ -1,8 +0,0 @@ -(@g clip get) -(@p clip put) -(@delete clip del) -(@comma clip prev) -(@period clip next) -(@lt clip swap-prev) -(@gt clip swap-next) -(@l clip loop-toggle) diff --git a/config/keys_clock.edn b/config/keys_clock.edn deleted file mode 100644 index 3f707559..00000000 --- a/config/keys_clock.edn +++ /dev/null @@ -1,2 +0,0 @@ -(@space clock toggle-playback 0) -(@shift-space clock toggle-playback 0) diff --git a/config/keys_device_add.edn b/config/keys_device_add.edn deleted file mode 100644 index 9ad4b197..00000000 --- a/config/keys_device_add.edn +++ /dev/null @@ -1,2 +0,0 @@ -(@up toggle-dialog :dialog-device-prev) -(@down toggle-dialog :dialog-device-next) diff --git a/config/keys_dialog.edn b/config/keys_dialog.edn deleted file mode 100644 index e69de29b..00000000 diff --git a/config/keys_editor.edn b/config/keys_editor.edn deleted file mode 100644 index a99ec7e5..00000000 --- a/config/keys_editor.edn +++ /dev/null @@ -1,26 +0,0 @@ -(@left editor set-time-pos :time-pos-prev) -(@shift-left editor set-time-pos :time-pos-prev-fine) -(@right editor set-time-pos :time-pos-next) -(@shift-right editor set-time-pos :time-pos-next-fine) - -(@equal editor set-time-zoom :time-zoom-prev) -(@minus editor set-time-zoom :time-zoom-next) -(@plus editor set-time-zoom :time-zoom-next-fine) -(@underscore editor set-time-zoom :time-zoom-prev-fine) - -(@z editor set-time-lock) - -(@up editor set-note-pos :note-pos-next) -(@down editor set-note-pos :note-pos-prev) -(@pgup editor set-note-pos :note-pos-next-octave) -(@pgdn editor set-note-pos :note-pos-prev-octave) - -(@comma editor set-note-len :note-len-prev) -(@period editor set-note-len :note-len-next) -(@lt editor set-note-len :note-len-prev) -(@gt editor set-note-len :note-len-next) - -(@a editor append-note :true) -(@enter editor append-note :false) -(@del editor delete-note) -(@shift-del editor delete-note) diff --git a/config/keys_global.edn b/config/keys_global.edn deleted file mode 100644 index 8e9f4233..00000000 --- a/config/keys_global.edn +++ /dev/null @@ -1,9 +0,0 @@ -(@esc dialog :dialog-none) -(@f1 dialog :dialog-help) -(@f6 dialog :dialog-save) -(@f8 dialog :dialog-options) -(@f9 dialog :dialog-load) -(@f10 dialog :dialog-quit) - -(@u undo 1) -(@r redo 1) diff --git a/config/keys_groovebox.edn b/config/keys_groovebox.edn deleted file mode 100644 index 4aa17d4a..00000000 --- a/config/keys_groovebox.edn +++ /dev/null @@ -1,3 +0,0 @@ -(@r sampler record/toggle :sample) -(@tab focus-next) -(@shift-tab focus-prev) diff --git a/config/keys_length.edn b/config/keys_length.edn deleted file mode 100644 index faec020c..00000000 --- a/config/keys_length.edn +++ /dev/null @@ -1,6 +0,0 @@ -(@up inc) -(@down dec) -(@right next) -(@left prev) -(@return set :length) -(@escape cancel) diff --git a/config/keys_message.edn b/config/keys_message.edn deleted file mode 100644 index 90ae7960..00000000 --- a/config/keys_message.edn +++ /dev/null @@ -1,2 +0,0 @@ -(@esc message dismiss) -(@enter message dismiss) diff --git a/config/keys_mix.edn b/config/keys_mix.edn deleted file mode 100644 index e69de29b..00000000 diff --git a/config/keys_pool.edn b/config/keys_pool.edn deleted file mode 100644 index b06a4b17..00000000 --- a/config/keys_pool.edn +++ /dev/null @@ -1,12 +0,0 @@ -(@n rename begin) -(@t length begin) -(@m import begin) -(@x export begin) -(@c clip color :clip :random-color) -(@openbracket select :clip-prev) -(@closebracket select :clip-next) -(@lt swap :clip :clip-prev) -(@gt swap :clip :clip-next) -(@delete clip/delete :clip) -(@shift-A clip/add :after :new-clip) -(@shift-D clip/add :after :cloned-clip) diff --git a/config/keys_pool_file.edn b/config/keys_pool_file.edn deleted file mode 100644 index a7b405ad..00000000 --- a/config/keys_pool_file.edn +++ /dev/null @@ -1,8 +0,0 @@ -(@up select :prev) -(@down select :next) -(@right chdir :selected) -(@left chdir :parent) -(@return confirm) -(@escape cancel) -(:char append :char) -(@backspace delete :last) diff --git a/config/keys_rename.edn b/config/keys_rename.edn deleted file mode 100644 index c4c0df84..00000000 --- a/config/keys_rename.edn +++ /dev/null @@ -1,4 +0,0 @@ -(:char append :char) -(@backspace delete :last) -(@return confirm) -(@escape cancel) diff --git a/config/keys_sampler.edn b/config/keys_sampler.edn deleted file mode 100644 index 596671c4..00000000 --- a/config/keys_sampler.edn +++ /dev/null @@ -1,12 +0,0 @@ -(@up sampler select :sample-above) -(@down sampler select :sample-below) -(@left sampler select :sample-to-left) -(@right sampler select :sample-to-right) - -(@r sampler record-toggle :sample-selected) -(@shift-R sampler record-cancel) -(@p sampler play-sample :sample-selected) -(@P sampler stop-sample :sample-selected) - -(@shift-f6 dialog :dialog-export-sample) -(@shift-f9 dialog :dialog-import-sample) diff --git a/config/keys_scene.edn b/config/keys_scene.edn deleted file mode 100644 index f3265cec..00000000 --- a/config/keys_scene.edn +++ /dev/null @@ -1,7 +0,0 @@ -(@delete scene delete :scene) -(@q scene launch :scene) -(@c scene color :scene) -(@comma scene prev) -(@period scene next) -(@lt scene swap-prev) -(@gt scene swap-next) diff --git a/config/keys_sequencer.edn b/config/keys_sequencer.edn deleted file mode 100644 index eb8eba70..00000000 --- a/config/keys_sequencer.edn +++ /dev/null @@ -1,4 +0,0 @@ -(@c color) -(@q launch) -(@shift-I input add) -(@shift-O output add) diff --git a/config/keys_track.edn b/config/keys_track.edn deleted file mode 100644 index 3ffbb9a9..00000000 --- a/config/keys_track.edn +++ /dev/null @@ -1,11 +0,0 @@ -(@delete track delete :track) -(@q track launch :track) -(@c track color :track) -(@comma track prev) -(@period track next) -(@lt track swap-prev) -(@gt track swap-next) -(@r track rec) -(@m track mon) -(@p track play) -(@P track solo) diff --git a/config/templates.edn b/config/templates.edn new file mode 100644 index 00000000..c3efd64c --- /dev/null +++ b/config/templates.edn @@ -0,0 +1,74 @@ +(module :transport + (name "Transport") + (info "A JACK transport controller.") + (bind :keys/clock) + (bind :keys/global) + :view/transport) + +(module :arranger + (name "Arranger") + (info "A grid of launchable clips arranged by track and scene.") + (bind :keys/editor :focused/editor) + (bind :keys/dialog :focused/dialog) + (bind :keys/message :focused/message) + (bind :keys/device_add :focused/device-add) + (bind :keys/browser :focused/browser) + (bind :keys/rename :focused/pool-rename) + (bind :keys/length :focused/pool-length) + (bind :keys/clip :focused/clip) + (bind :keys/track :focused/track) + (bind :keys/scene :focused/scene) + (bind :keys/mix :focused/mix) + (bind :keys/clock) + (bind :keys/arranger) + (bind :keys/global) + :view/dialog + (bsp/w :view/meters/output + (bsp/e :view/meters/input + (stack/n (fixed/y 2 :view/status/h2) :view/tracks/inputs + (stack/s :view/tracks/devices :view/tracks/outputs :view/tracks/names + (fill/xy (either :focused/editor + (bsp/e :view/scenes/names :view/editor) + :view/scenes))))))) + +(module :groovebox + (name "Groovebox") + (info "A sequencer with built-in sampler.") + (bind :keys/browser :focused/browser) + (bind :keys/rename :focused/pool-rename) + (bind :keys/length :focused/pool-length) + (bind :keys/clock) + (bind :keys/editor) + (bind :keys/sampler) + (bind :keys/global) + :view/dialog + (bsp/w :view/meters/output (bsp/e :view/meters/input (bsp/w (fill/y (align/n (stack/s :view/midi-ins/status + :view/midi-outs/status + :view/audio-ins/status + :view/audio-outs/status + :view/pool))) + (bsp/n (fixed/y :h-sample-detail (bsp/e (fill/y (fixed/x 20 (align/nw :view/sample-status))) :view/sample-viewer)) + (bsp/e (fill/y (align/n (bsp/s :view/status/v :view/editor-status))) (bsp/e :view/samples/keys :view/editor))))))) + +(module :sampler + (name "Sampler") + (info "A sampling soundboard.") + (bind :keys/sampler) + (bind :keys/global) + :view/dialog + (bsp/s (fixed/y 1 :view/transport) + (bsp/n (fixed/y 1 :view/status) + (fill/xy :view/samples/grid)))) + +(module :sequencer + (name "Sequencer") + (info "A MIDI sequencer.") + (bind :keys/browser :focused/browser) + (bind :keys/rename :mode/pool-rename) + (bind :keys/length :mode/pool-length) + (bind :keys/editor) + (bind :keys/clock) + (bind :keys/global) + :view/dialog + (bsp/s (fixed/y 1 :view/transport) (bsp/n (fixed/y 1 :view/status) + (fill/xy (bsp/a (fill/xy (align/e :view/pool)) :view/editor))))) diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 9635da64..0746934e 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -15,6 +15,7 @@ clap = { workspace = true, optional = true } palette = { workspace = true } rand = { workspace = true } toml = { workspace = true } +xdg = { workspace = true } [dev-dependencies] proptest = { workspace = true } diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index bb1d067c..0807ac89 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -7,7 +7,9 @@ macro_rules! cmd_todo { ($msg:literal) => {{ println!($msg); None }}; } handle!(TuiIn: |self: App, input|self.handle_tui_key_with_history(input)); impl App { fn handle_tui_key_with_history (&mut self, input: &TuiIn) -> Perhaps { - Ok(if let Some(binding) = self.config.keys.dispatch(input.event()) { + Ok(if let Some(binding) = self.configs.current.as_ref() + .map(|c|c.keys.dispatch(input.event())).flatten() + { let binding = binding.clone(); let undo = binding.command.clone().execute(self)?; // FIXME failed commands are not persisted in undo history diff --git a/crates/app/src/config.rs b/crates/app/src/config.rs new file mode 100644 index 00000000..9c99aba7 --- /dev/null +++ b/crates/app/src/config.rs @@ -0,0 +1,111 @@ +use crate::*; +use xdg::BaseDirectories; + +/// Configurations +#[derive(Default, Debug)] +pub struct Configurations { + pub dirs: BaseDirectories, + pub modules: Arc, Arc>>>, + pub current: Option +} + +/// Configuration +#[derive(Default, Debug)] +pub struct Configuration { + /// Path of configuration entrypoint + pub path: std::path::PathBuf, + /// Name of configuration + pub name: Option>, + /// Description of configuration + pub info: Option>, + /// View definition + pub view: Arc, + // Input keymap + pub keys: EventMap, +} + +macro_rules! dsl_for_each (($dsl:expr => |$head:ident|$body:expr)=>{ + let mut dsl: Arc = $dsl.src().into(); + let mut $head: Option> = dsl.head()?.map(Into::into); + let mut tail: Option> = dsl.tail()?.map(Into::into); + loop { + if let Some($head) = $head { + $body; + } else { + break + } + if let Some(next) = tail { + $head = next.head()?.map(Into::into); + tail = next.tail()?.map(Into::into); + } else { + break + } + } +}); + +impl Configurations { + const DEFAULT_TEMPLATES: &'static str = include_str!("../../../config/templates.edn"); + const DEFAULT_BINDINGS: &'static str = include_str!("../../../config/bindings.edn"); + pub fn init () -> Usually { + let mut dirs = BaseDirectories::with_profile("tek", "v0"); + let mut cfgs = Self { dirs, ..Default::default() }; + cfgs.init_file("templates.edn", Self::DEFAULT_TEMPLATES)?; + cfgs.load_file("templates.edn", |head|{ Ok(()) })?; + cfgs.init_file("bindings.edn", Self::DEFAULT_BINDINGS)?; + cfgs.load_file("bindings.end", |head|{ Ok(()) })?; + Ok(cfgs) + } + fn init_file (&mut self, path: &str, val: &str) -> Usually<()> { + if self.dirs.find_config_file("templates.edn").is_none() { + std::fs::write(self.dirs.place_config_file("templates.edn")?, Self::DEFAULT_TEMPLATES); + } + Ok(()) + } + fn load_file (&mut self, path: &str, mut each: impl FnMut(&Arc)->Usually<()> ) -> Usually<()> { + Ok(if let Some(path) = self.dirs.find_config_file("templates.edn") { + dsl_for_each!(std::fs::read_to_string(path)?.as_str() => |dsl|each(&dsl)); + } else { + return Err(format!("{path}: not found").into()) + }) + } +} + +impl Configuration { + fn load_template (&mut self, dsl: impl Dsl) -> Usually<&mut Self> { + dsl_for_each!(dsl => |dsl|match () { + _ if let Some(exp) = dsl.exp()? => match exp.head()?.key()? { + Some("name") => match exp.tail()?.text()? { + Some(name) => self.name = Some(name.into()), + _ => return Err(format!("missing name definition").into()) + }, + Some("info") => match exp.tail()?.text()? { + Some(info) => self.info = Some(info.into()), + _ => return Err(format!("missing info definition").into()) + }, + Some("bind") => match exp.tail()? { + Some(keys) => self.keys = EventMap::from_dsl(&mut &keys)?, + _ => return Err(format!("missing keys definition").into()) + }, + Some("view") => match exp.tail()? { + Some(tail) => self.view = tail.src().into(), + _ => return Err(format!("missing view definition").into()) + }, + dsl => return Err(format!("unexpected: {dsl:?}").into()) + }, + + _ => return Err(format!("unexpected: {dsl:?}").into()) + }); + Ok(self) + } + fn load_binding (&mut self, dsl: impl Dsl) -> Usually<&mut Self> { + todo!(); + Ok(self) + } +} + +fn unquote (x: &str) -> &str { + let mut chars = x.chars(); + chars.next(); + //chars.next_back(); + chars.as_str() +} diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index cc811d61..ee5f836d 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -36,6 +36,7 @@ pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering::Relaxed}; mod api; pub use self::api::*; mod audio; pub use self::audio::*; +mod config; pub use self::config::*; mod model; pub use self::model::*; mod view; pub use self::view::*; diff --git a/crates/app/src/model.rs b/crates/app/src/model.rs index d757b431..10b73a3f 100644 --- a/crates/app/src/model.rs +++ b/crates/app/src/model.rs @@ -1,66 +1,28 @@ use crate::*; use std::path::PathBuf; use std::error::Error; + #[derive(Default, Debug)] pub struct App { /// Must not be dropped for the duration of the process - pub jack: Jack<'static>, + pub jack: Jack<'static>, /// Display size - pub size: Measure, + pub size: Measure, /// Performance counter - pub perf: PerfModel, - // View and input definition - pub config: Configuration, + pub perf: PerfModel, + /// Available view definitions and input bindings + pub configs: Configurations, /// Contains all recently created clips. - pub pool: Pool, + pub pool: Pool, /// Contains the currently edited musical arrangement pub project: Arrangement, /// Undo history pub history: Vec<(AppCommand, Option)>, // Dialog overlay - pub dialog: Option, + pub dialog: Option, /// Base color. - pub color: ItemTheme, + pub color: ItemTheme, } -has!(Jack<'static>: |self: App|self.jack); -has!(Pool: |self: App|self.pool); -has!(Option: |self: App|self.dialog); -has!(Clock: |self: App|self.project.clock); -has!(Option: |self: App|self.project.editor); -has!(Selection: |self: App|self.project.selection); -has!(Vec: |self: App|self.project.midi_ins); -has!(Vec: |self: App|self.project.midi_outs); -has!(Vec: |self: App|self.project.scenes); -has!(Vec: |self: App|self.project.tracks); -has!(Measure: |self: App|self.size); -maybe_has!(Track: |self: App| - { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); -impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } -maybe_has!(Scene: |self: App| - { MaybeHas::::get(&self.project) }; - { MaybeHas::::get_mut(&mut self.project) }); -impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } -has_clips!(|self: App|self.pool.clips); -impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.inner_size } } -//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); -//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); -//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); -//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); -//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); -//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); -//has_editor!(|self: App|{ - //editor = self.editor; - //editor_w = { - //let size = self.size.w(); - //let editor = self.editor.as_ref().expect("missing editor"); - //let time_len = editor.time_len().get(); - //let time_zoom = editor.time_zoom().get().max(1); - //(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) - //}; - //editor_h = 15; - //is_editing = self.editor.is_some(); -//}); impl App { pub fn update_clock (&self) { @@ -190,20 +152,16 @@ impl App { self.browser().is_some() } fn focus_clip (&self) -> bool { - !self.focus_editor() && matches!(self.selection(), - Selection::TrackClip{..}) + !self.focus_editor() && matches!(self.selection(), Selection::TrackClip{..}) } fn focus_track (&self) -> bool { - !self.focus_editor() && matches!(self.selection(), - Selection::Track(..)) + !self.focus_editor() && matches!(self.selection(), Selection::Track(..)) } fn focus_scene (&self) -> bool { - !self.focus_editor() && matches!(self.selection(), - Selection::Scene(..)) + !self.focus_editor() && matches!(self.selection(), Selection::Scene(..)) } fn focus_mix (&self) -> bool { - !self.focus_editor() && matches!(self.selection(), - Selection::Mix) + !self.focus_editor() && matches!(self.selection(), Selection::Mix) } fn focus_pool_import (&self) -> bool { matches!(self.pool.mode, Some(PoolMode::Import(..))) @@ -318,99 +276,43 @@ impl App { } } -/// Configuration -#[derive(Default, Debug)] -pub struct Configuration { - /// Path of configuration entrypoint - pub path: PathBuf, - /// Name of configuration - pub name: Option>, - /// Description of configuration - pub info: Option>, - /// View definition - pub view: Arc, - // Input keymap - pub keys: EventMap, -} +has!(Jack<'static>: |self: App|self.jack); +has!(Pool: |self: App|self.pool); +has!(Option: |self: App|self.dialog); +has!(Clock: |self: App|self.project.clock); +has!(Option: |self: App|self.project.editor); +has!(Selection: |self: App|self.project.selection); +has!(Vec: |self: App|self.project.midi_ins); +has!(Vec: |self: App|self.project.midi_outs); +has!(Vec: |self: App|self.project.scenes); +has!(Vec: |self: App|self.project.tracks); +has!(Measure: |self: App|self.size); +maybe_has!(Track: |self: App| + { MaybeHas::::get(&self.project) }; + { MaybeHas::::get_mut(&mut self.project) }); +impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } } +maybe_has!(Scene: |self: App| + { MaybeHas::::get(&self.project) }; + { MaybeHas::::get_mut(&mut self.project) }); +impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } } +has_clips!(|self: App|self.pool.clips); +impl HasClipsSize for App { fn clips_size (&self) -> &Measure { &self.project.inner_size } } -impl Configuration { - pub fn from_path (path: &impl AsRef, _watch: bool) -> Usually { - let mut config = Self { path: path.as_ref().into(), ..Default::default() }; - let mut dsl = read_and_leak(path.as_ref())?; - let mut head: Option> = dsl.head()?.map(Into::into); - let mut tail: Option> = dsl.tail()?.map(Into::into); - loop { - if let Some(exp) = head.exp()? { - match exp.head()?.key()? { - Some("name") => match exp.tail()?.text()? { - Some(name) => config.name = Some(name.into()), - _ => return Err(format!("missing name definition").into()) - }, - Some("info") => match exp.tail()?.text()? { - Some(info) => config.info = Some(info.into()), - _ => return Err(format!("missing info definition").into()) - }, - Some("keys") => match exp.tail()? { - Some(keys) => config.keys = EventMap::from_dsl(&mut &keys)?, - _ => return Err(format!("missing keys definition").into()) - }, - Some("view") => match exp.tail()? { - Some(tail) => config.view = tail.src().into(), - _ => return Err(format!("missing view definition").into()) - }, - Some(k) => return Err(format!("(e3) unexpected key {k:?} in {exp:?}").into()), - None => return Err(format!("(e2) unexpected exp {exp:?}").into()), - } - } else { - break - } - if let Some(next) = tail { - head = next.head()?.map(Into::into); - tail = next.tail()?.map(Into::into); - } else { - break - } - } - Ok(config) - } -} - -fn read_and_leak (path: impl AsRef) -> Usually<&'static str> { - Ok(leak(String::from_utf8(std::fs::read(path.as_ref())?)?)) -} - -fn leak (x: impl AsRef) -> &'static str { - Box::leak(x.as_ref().into()) -} - -fn unquote (x: &str) -> &str { - let mut chars = x.chars(); - chars.next(); - //chars.next_back(); - chars.as_str() -} - -macro_rules! default_config { ($path:literal) => { ($path, include_str!($path)) }; } -pub const DEFAULT_CONFIGS: &'static [(&'static str, &'static str)] = &[ - default_config!("../../../config/config_arranger.edn"), - default_config!("../../../config/config_groovebox.edn"), - default_config!("../../../config/config_sampler.edn"), - default_config!("../../../config/config_sequencer.edn"), - default_config!("../../../config/config_transport.edn"), - - default_config!("../../../config/keys_arranger.edn"), - default_config!("../../../config/keys_clip.edn"), - default_config!("../../../config/keys_clock.edn"), - default_config!("../../../config/keys_editor.edn"), - default_config!("../../../config/keys_global.edn"), - default_config!("../../../config/keys_groovebox.edn"), - default_config!("../../../config/keys_length.edn"), - default_config!("../../../config/keys_mix.edn"), - default_config!("../../../config/keys_pool.edn"), - default_config!("../../../config/keys_pool_file.edn"), - default_config!("../../../config/keys_rename.edn"), - default_config!("../../../config/keys_sampler.edn"), - default_config!("../../../config/keys_scene.edn"), - default_config!("../../../config/keys_sequencer.edn"), - default_config!("../../../config/keys_track.edn"), -]; +//take!(ClockCommand |state: App, iter|Take::take(state.clock(), iter)); +//take!(MidiEditCommand |state: App, iter|Ok(state.editor().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(PoolCommand |state: App, iter|Take::take(&state.pool, iter)); +//take!(SamplerCommand |state: App, iter|Ok(state.project.sampler().map(|x|Take::take(x, iter)).transpose()?.flatten())); +//take!(ArrangementCommand |state: App, iter|Take::take(&state.project, iter)); +//take!(DialogCommand |state: App, iter|Take::take(&state.dialog, iter)); +//has_editor!(|self: App|{ + //editor = self.editor; + //editor_w = { + //let size = self.size.w(); + //let editor = self.editor.as_ref().expect("missing editor"); + //let time_len = editor.time_len().get(); + //let time_zoom = editor.time_zoom().get().max(1); + //(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) + //}; + //editor_h = 15; + //is_editing = self.editor.is_some(); +//}); diff --git a/crates/app/src/view.rs b/crates/app/src/view.rs index 75fef228..e31fe771 100644 --- a/crates/app/src/view.rs +++ b/crates/app/src/view.rs @@ -23,14 +23,14 @@ impl> Content for ErrorBoundary { } impl App { - pub fn view (model: &Self) -> impl Content + '_ { - ErrorBoundary::new(Ok(Some(Tui::bg(Black, model.view_menu())))) + pub fn view (&self) -> impl Content + '_ { + ErrorBoundary::new(Ok(Some(Tui::bg(Black, self.view_menu())))) //ErrorBoundary::new(Take::take(model, &mut model.config.view.clone())) //ErrorBoundary::new(Give::give(model, &mut model.config.view.clone())) } } -content!(TuiOut: |self: App| ErrorBoundary::new(Ok(Some(Tui::bg(Black, self.view_nil()))))); +content!(TuiOut: |self: App| ErrorBoundary::new(Ok(Some(Tui::bg(Black, self.view()))))); #[tengri_proc::view(TuiOut)] impl App { @@ -39,7 +39,7 @@ impl App { } pub fn view_menu (&self) -> impl Content + use<'_> { Stack::south(|add: &mut dyn FnMut(&dyn Render)|{ - add(&Tui::bold(true, "tek")); + add(&Tui::bold(true, "tek 0.3.0-rc0")); add(&""); add(&"+ new session"); }) diff --git a/crates/cli/tek.rs b/crates/cli/tek.rs index d2fde313..2195806e 100644 --- a/crates/cli/tek.rs +++ b/crates/cli/tek.rs @@ -13,7 +13,7 @@ pub struct Cli { /// Pre-defined configuration modes. /// /// TODO: Replace these with scripted configurations. - #[command(subcommand)] mode: LaunchMode, + #[command(subcommand)] mode: Option, /// Name of JACK client #[arg(short='n', long)] name: Option, /// Whether to attempt to become transport master @@ -45,29 +45,8 @@ pub struct Cli { /// Application modes #[derive(Debug, Clone, Subcommand)] pub enum LaunchMode { - /// ⏯️ A standalone transport clock. - Clock, - /// 🎼 A MIDI sequencer. - Sequencer, - /// 🎺 A MIDI-controlled audio sampler. - Sampler, - /// 📻 Sequencer and sampler together. - Groovebox, - /// 🎧 Multi-track MIDI sequencer. - Arranger { - /// Number of scenes - #[arg(short = 'y', long, default_value_t = 16)] scenes: usize, - /// Number of tracks - #[arg(short = 'x', long, default_value_t = 12)] tracks: usize, - /// Width of tracks - #[arg(short = 'w', long, default_value_t = 15)] track_width: usize, - }, - /// TODO: A MIDI-controlled audio mixer - Mixer, - /// TODO: A customizable channel strip - Track, - /// TODO: An audio plugin host - Plugin, + /// Create a new session instead of loading the previous one. + New, } impl Cli { @@ -86,9 +65,6 @@ impl Cli { let right_tos = Connect::collect(&self.right_to, empty, empty); let audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; let audio_tos = &[left_tos.as_slice(), right_tos.as_slice()]; - let clip = Arc::new(RwLock::new(MidiClip::new( - "Clip", true, 384usize, None, Some(ItemColor::random().into())), - )); Tui::new()?.run(&Jack::new_run(&name, move|jack|{ for (index, connect) in midi_froms.iter().enumerate() { midi_ins.push(jack.midi_in(&format!("M/{index}"), &[connect.clone()])?); @@ -96,34 +72,12 @@ impl Cli { for (index, connect) in midi_tos.iter().enumerate() { midi_outs.push(jack.midi_out(&format!("{index}/M"), &[connect.clone()])?); }; - let config = Configuration::from_path(&match self.mode { - LaunchMode::Clock => "config/config_transport.edn", - LaunchMode::Sequencer => "config/config_sequencer.edn", - LaunchMode::Groovebox => "config/config_groovebox.edn", - LaunchMode::Arranger { .. } => "config/config_arranger.edn", - LaunchMode::Sampler => "config/config_sampler.edn", - _ => todo!("{:?}", self.mode), - }, false)?; + let configs = Configurations::init(); let clock = Clock::new(&jack, self.bpm)?; - match self.mode { - LaunchMode::Sequencer => tracks.push(Track::new( - &name, None, &jack, Some(&clock), Some(&clip), - midi_froms.as_slice(), midi_tos.as_slice() - )?), - LaunchMode::Groovebox | LaunchMode::Sampler => tracks.push(Track::new_with_sampler( - &name, None, &jack, Some(&clock), Some(&clip), - midi_froms.as_slice(), midi_tos.as_slice(), audio_froms, audio_tos, - )?), - _ => {} - } let mut app = App { - jack: jack.clone(), - config, - color: ItemTheme::random(), - pool: match self.mode { - LaunchMode::Sequencer | LaunchMode::Groovebox => (&clip).into(), - _ => Default::default() - }, + jack: jack.clone(), + configs: Configurations::init()?, + color: ItemTheme::random(), project: Arrangement { name: Default::default(), color: ItemTheme::random(), @@ -134,20 +88,16 @@ impl Cli { selection: Selection::TrackClip { track: 0, scene: 0 }, midi_ins, midi_outs, - editor: match self.mode { - LaunchMode::Sequencer | LaunchMode::Groovebox => Some((&clip).into()), - _ => None - }, ..Default::default() }, ..Default::default() }; - if let LaunchMode::Arranger { scenes, tracks, track_width, .. } = self.mode { - app.project.arranger = Default::default(); - app.project.selection = Selection::TrackClip { track: 1, scene: 1 }; - app.project.scenes_add(scenes)?; - app.project.tracks_add(tracks, Some(track_width), &[], &[])?; - } + //if let LaunchMode::Arranger { scenes, tracks, track_width, .. } = self.mode { + //app.project.arranger = Default::default(); + //app.project.selection = Selection::TrackClip { track: 1, scene: 1 }; + //app.project.scenes_add(scenes)?; + //app.project.tracks_add(tracks, Some(track_width), &[], &[])?; + //} jack.sync_lead(self.sync_lead, |mut state|{ let clock = app.clock(); clock.playhead.update_from_sample(state.position.frame() as f64); @@ -162,10 +112,12 @@ impl Cli { /// CLI header const HEADER: &'static str = r#" - - ╓─╥─╖ ╓──╖ ╥ ╖ - ║ ╟─╌ ╟─╡ - ╨ ╙──╜ ╨ ╜"#; +~ ╓─╥─╖ ╓──╖ ╥ ╖ ~~~~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~~~~~~ +~ ~ ║ ~ ╟─╌ ~╟─< ~ v0.3.0-rc.0 "no, i insist that i am not a dj ~ +~ ~ ╨ ~ ╙──╜ ╨ ╜ ~ 2025, summer, the nose of the cat. J ~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + On first run, Tek will create configuration and state dirs. ~ + On subsequent runs, Tek should resume from where you left off. ~"#; #[cfg(test)] #[test] fn test_cli () { use clap::CommandFactory; diff --git a/deps/tengri b/deps/tengri index 85c30538..9e0b7be9 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit 85c305385bc08ef805afb113f677c510027a7234 +Subproject commit 9e0b7be9a9c80b5df52854ab426bc60b794931ed