wip: unify default config
Some checks are pending
/ build (push) Waiting to run

This commit is contained in:
🪞👃🪞 2025-08-10 16:13:54 +03:00
parent 4d4c470a81
commit 50728729b7
8 changed files with 232 additions and 335 deletions

View file

@ -1,164 +0,0 @@
(def :keys/cancel
(@escape cancel))
(def :keys/delete
(@delete delete))
(def :keys/history
(@u undo 1) (@r redo 1))
(def :keys/nextprev
(@comma prev) (@period next))
(def :keys/swap
(@lt swap/prev) (@gt swap/next))
(def :keys/list
(@up list/prev) (@down list/next) (@enter list/select) (@escape list/cancel))
(def :keys/clock
(@space clock/toggle 0) (@shift/space clock/toggle 0))
(def :keys/color
(@c color))
(def :keys/launch
(@q launch))
(def :keys/filter
(see :keys/list :keys/input))
(def :keys/input
(see :keys/cancel :keys/delete)
(@left cursor/prev) (@right cursor/next) (@backspace delete/prev) (:char input))
(def :keys/global
(see :keys/cancel :keys/history)
(@f1 dialog :help) (@f8 dialog :options) (@f10 dialog :quit)
(@f6 dialog :save) (@f9 dialog :load))
(def :keys/arranger
(see :keys/color :keys/launch :keys/arranger/direction :keys/arranger/cycle)
(@tab project/edit) (@enter project/edit)
(@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))
(def :keys/arranger/cycle
(@t select :select/track) (@s select :select/scene))
(def :keys/arranger/direction
(@up select :select/scene/prev) (@down select :select/scene/next)
(@left select :select/track/prev) (@right select :select/track/next))
(def :keys/scene (see :keys/color :keys/launch :keys/nextprev :keys/swap :keys/delete))
(def :keys/track (see :keys/color :keys/launch :keys/nextprev :keys/swap :keys/delete)
(@r toggle :rec) (@m toggle :mon) (@p toggle :play) (@P toggle :solo))
(def :keys/clip (see :keys/color :keys/launch :keys/nextprev :keys/swap :keys/delete)
(@l toggle :loop))
(def :keys/sampler (see :keys/sampler/directions :keys/sampler/record :keys/sampler/play))
(def :keys/sampler/play
(@p sampler/play/sample :sample/selected)
(@P sampler/stop/sample :sample/selected))
(def :keys/sampler/record
(@r sampler/record/toggle :sample/selected)
(@shift/R sampler/record/cancel))
(def :keys/sampler/import-export
(@shift/f6 dialog :dialog/export/sample)
(@shift/f9 dialog :dialog/import/sample))
(def :keys/sampler/directions
(@up sampler/select :sample/above)
(@down sampler/select :sample/below)
(@left sampler/select :sample/to/left)
(@right sampler/select :sample/to/right))
(def :keys/sequencer (see :keys/color :keys/launch)
(@shift/I input/add)
(@shift/O output/add))
(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 :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))

View file

@ -1,74 +0,0 @@
(mode :transport
(name Transport)
(info A JACK transport controller.)
(keys :clock :global)
:view/transport)
(mode :arranger
(name Arranger)
(info A grid of launchable clips arranged by track and scene.)
(mode :editor (keys :editor))
(mode :dialog (keys :dialog))
(mode :message (keys :message))
(mode :add-device (keys :add-device))
(mode :browser (keys :browser))
(mode :rename (keys :pool-rename))
(mode :length (keys :pool-length))
(mode :clip (keys :clip))
(mode :track (keys :track))
(mode :scene (keys :scene))
(mode :mix (keys :mix))
(keys :clock :arranger :global)
(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 :mode/editor
(bsp/e :view/scenes/names :view/editor)
:view/scenes)))))))
(mode :groovebox
(name Groovebox)
(info A sequencer with built-in sampler.)
(mode :browser (keys :browser))
(mode :rename (keys :pool-rename))
(mode :length (keys :pool-length))
(keys :clock :editor :sampler :global)
(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)))))))
(mode :sampler
(name Sampler)
(info A sampling soundboard.)
(keys :sampler :global)
(bsp/s (fixed/y 1 :view/transport)
(bsp/n (fixed/y 1 :view/status)
(fill/xy :view/samples/grid))))
(mode :sequencer
(name Sequencer)
(info A MIDI sequencer.)
(mode :browser (keys :browser))
(mode :rename (keys :pool-rename))
(mode :length (keys :pool-length))
(keys :editor :clock :global)
(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)))))

View file

@ -54,10 +54,10 @@ pub struct App {
pub size: Measure<TuiOut>, pub size: Measure<TuiOut>,
/// Performance counter /// Performance counter
pub perf: PerfModel, pub perf: PerfModel,
/// Available view profiles and input bindings /// Available view modes and input bindings
pub config: Config, pub config: Config,
/// Currently selected profile /// Currently selected mode
pub profile: Profile, pub mode: Mode<Arc<str>>,
/// Contains the currently edited musical arrangement /// Contains the currently edited musical arrangement
pub project: Arrangement, pub project: Arrangement,
/// Contains all recently created clips. /// Contains all recently created clips.
@ -72,124 +72,121 @@ pub struct App {
/// Configuration /// Configuration
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct Config { pub struct Config {
/// XDG basedirs
pub dirs: BaseDirectories, pub dirs: BaseDirectories,
/// Available view profiles pub modes: Arc<RwLock<BTreeMap<Arc<str>, Arc<Mode<Arc<str>>>>>>,
pub views: Arc<RwLock<BTreeMap<Arc<str>, Profile>>>,
/// Available input bindings
pub binds: Arc<RwLock<BTreeMap<Arc<str>, EventMap<Option<TuiEvent>, Arc<str>>>>>, pub binds: Arc<RwLock<BTreeMap<Arc<str>, EventMap<Option<TuiEvent>, Arc<str>>>>>,
} }
/// Profile
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct Profile { pub struct Mode<D: Dsl + Ord> {
/// Path of configuration entrypoint
pub path: PathBuf, pub path: PathBuf,
/// Default mode pub name: Vec<D>,
pub mode: Mode, pub info: Vec<D>,
/// Optional modes pub view: Vec<D>,
pub modes: BTreeMap<Arc<str>, Mode>, pub keys: Vec<D>,
} pub modes: BTreeMap<D, Mode<D>>,
#[derive(Default, Debug)]
pub struct Mode {
/// Name of configuration
pub name: Vec<Arc<str>>,
/// Description of configuration
pub info: Vec<Arc<str>>,
/// View definition
pub view: Vec<Arc<str>>,
// Input keymap
pub keys: Vec<Arc<str>>,
} }
impl Config { impl Config {
const VIEWS: &'static str = "views.edn"; const CONFIG: &'static str = "tek.edn";
const BINDS: &'static str = "binds.edn"; const DEFAULTS: &'static str = include_str!("../../tek.edn");
const DEFAULT_VIEWS: &'static str = include_str!("../../config/views.edn");
const DEFAULT_BINDS: &'static str = include_str!("../../config/binds.edn");
pub fn init () -> Usually<Self> { pub fn init () -> Usually<Self> {
let mut cfgs: Self = Default::default(); let mut cfgs: Self = Default::default();
cfgs.dirs = BaseDirectories::with_profile("tek", "v0"); cfgs.dirs = BaseDirectories::with_profile("tek", "v0");
cfgs.load(Self::BINDS, Self::DEFAULT_BINDS, |cfgs, dsl|cfgs.load_bind(dsl))?; cfgs.init_file(Self::CONFIG, Self::DEFAULTS, |cfgs, dsl|cfgs.load_defs(dsl))?;
cfgs.load(Self::VIEWS, Self::DEFAULT_VIEWS, |cfgs, dsl|cfgs.load_view(dsl))?;
Ok(cfgs) Ok(cfgs)
} }
pub fn load ( pub fn init_file (
&mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()> &mut self, path: &str, defaults: &str, mut each: impl FnMut(&mut Self, &str)->Usually<()>
) -> Usually<()> { ) -> Usually<()> {
if self.dirs.find_config_file(path).is_none() { if self.dirs.find_config_file(path).is_none() {
println!("Creating {path:?}");
std::fs::write(self.dirs.place_config_file(path)?, defaults); std::fs::write(self.dirs.place_config_file(path)?, defaults);
} }
Ok(if let Some(path) = self.dirs.find_config_file(path) { Ok(if let Some(path) = self.dirs.find_config_file(path) {
println!("Loading {path:?}");
let src = std::fs::read_to_string(&path)?; let src = std::fs::read_to_string(&path)?;
src.as_str().each(move|item|each(self, item))?; src.as_str().each(move|item|each(self, item))?;
} else { } else {
return Err(format!("{path}: not found").into()) return Err(format!("{path}: not found").into())
}) })
} }
pub fn load_bind <D: Dsl> (&mut self, dsl: D) -> Usually<()> { pub fn load_defs <D: Dsl> (&mut self, dsl: D) -> Usually<()> {
load_modules(dsl, |id, tail|Ok(self.binds.write().unwrap().insert(id.into(), { dsl.each(|item|{
let mut map = EventMap::new(); println!("{item:?}");
tail.each(|item|{ Ok(match item.exp().head() {
map.add(TuiEvent::from_dsl(item.exp()?.head()?)?, Binding { Ok(Some("keys")) if let Some(id) = item.exp().tail().head()? => {
command: item.exp()?.tail()?.unwrap_or_default().into(), self.binds.write().unwrap().insert(id.into(), {
condition: None, let mut map = EventMap::new();
description: None, item.exp().tail().tail()?.each(|item|Ok({
source: None if let Ok(Some(sym)) = item.exp().head().sym() {
}); map.add(TuiEvent::from_dsl(item.exp()?.head()?)?, Binding {
Ok(()) command: item.exp()?.tail()?.unwrap_or_default().into(),
})?; condition: None,
map description: None,
}))) source: None
} });
pub fn load_view <D: Dsl> (&mut self, dsl: D) -> Usually<()> { } else if item.exp().head() == Ok(Some("see")) {
load_modules(&dsl, |id, tail|Ok(self.views.write().unwrap().insert(id.into(), { // TODO
let mut profile = Profile::default(); } else {
tail.each(|item|Ok(if let Ok(Some(exp)) = item.exp() { return Err(format!("load_defs: unexpected: {item:?}").into())
match exp.head()? { }
Some("name") => profile.mode.name.push( }))?;
exp.tail()?.map(|x|x.trim()).unwrap_or("").into() map
), });
Some("info") => profile.mode.info.push( },
exp.tail()?.map(|x|x.trim()).unwrap_or("").into() Ok(Some("mode")) if let Some(id) = item.exp().tail().head()? => {
), self.modes.write().unwrap().insert(id.into(), {
Some("keys") => if let Some(tail) = exp.tail()? { let mut mode = Mode::default();
tail.each(|keys|Ok(profile.mode.keys.push(keys.trim().into())))?; item.exp().tail().tail()?.each(|item|Ok(if let Ok(Some(exp)) = item.exp() {
} else {
return Err(format!("load_view: empty keys: {exp}").into())
},
Some("mode") => if let (Some(name), Some(tail)) = (
exp.tail()?.head()?, exp.tail()?.tail()?,
) {
let mut mode: Mode = Default::default();
tail.each(|item|Ok(if let Ok(Some(exp)) = item.exp() {
match exp.head()? { match exp.head()? {
Some("name") => mode.name.push(
exp.tail()?.map(|x|x.trim()).unwrap_or("").into()
),
Some("info") => mode.info.push(
exp.tail()?.map(|x|x.trim()).unwrap_or("").into()
),
Some("keys") => if let Some(tail) = exp.tail()? { Some("keys") => if let Some(tail) = exp.tail()? {
tail.each(|keys|Ok(mode.keys.push(keys.trim().into())))?; tail.each(|keys|Ok(mode.keys.push(keys.trim().into())))?;
} else { } else {
return Err(format!("load_view: empty keys: {exp}").into()) return Err(format!("load_view: empty keys: {exp}").into())
}, },
_ => { Some("mode") => if let (Some(name), Some(tail)) = (
return Err(format!("load_view: unexpected in mode {name}: {item:?}").into()) exp.tail()?.head()?, exp.tail()?.tail()?,
} ) {
let mut submode: Mode<Arc<str>> = Default::default();
tail.each(|item|Ok(if let Ok(Some(exp)) = item.exp() {
match exp.head()? {
Some("keys") => if let Some(tail) = exp.tail()? {
tail.each(|keys|Ok(mode.keys.push(keys.trim().into())))?;
} else {
return Err(format!("load_view: empty keys: {exp}").into())
},
_ => {
return Err(format!("load_view: unexpected in mode {name}: {item:?}").into())
}
}
} else if let Ok(Some(sym)) = item.sym() {
// TODO
} else {
return Err(format!("load_view: unexpected in mode {name}: {item:?}").into())
}))?;
mode.modes.insert(name.trim().into(), submode);
} else {
return Err(format!("load_view: empty mode: {exp}").into())
},
Some(_) => mode.view.push(exp.into()),
None => return Err(format!("load_view: empty: {exp}").into())
} }
} else if let Ok(Some(sym)) = item.sym() { } else if let Ok(Some(sym)) = item.sym() {
// TODO mode.view.push(sym.into());
} else { } else {
return Err(format!("load_view: unexpected in mode {name}: {item:?}").into()) return Err(format!("load_view: unexpected: {dsl:?}").into())
}))?; }))?;
profile.modes.insert(name.trim().into(), mode); mode.into()
} else { });
return Err(format!("load_view: empty mode: {exp}").into()) },
}, _ => return Err(format!("load_defs: unexpected: {item:?}").into())
Some(_) => profile.mode.view.push(exp.into()), })
None => return Err(format!("load_view: empty: {exp}").into()) })
}
} else if let Ok(Some(sym)) = item.sym() {
profile.mode.view.push(sym.into());
} else {
return Err(format!("load_view: unexpected: {dsl:?}").into())
}))?;
profile
})))
} }
} }
fn load_modules <D: Dsl, U> (dsl: D, cb: impl Fn(&str, &str)->Usually<U>) -> Usually<()> { fn load_modules <D: Dsl, U> (dsl: D, cb: impl Fn(&str, &str)->Usually<U>) -> Usually<()> {

View file

@ -2,15 +2,15 @@ use crate::*;
#[derive(Debug)] #[derive(Debug)]
pub enum AppCommand {} pub enum AppCommand {}
handle!(TuiIn:|self: App, input|{ handle!(TuiIn:|self: App, input|{
panic!("wat: {:?}", self.profile); panic!("wat: {:?}", self.mode);
for keys in self.profile.mode.keys.iter() { for keys in self.mode.keys.iter() {
panic!("{keys} {:?}", self.config.binds.read().unwrap()); panic!("{keys} {:?}", self.config.binds.read().unwrap());
if let Some(binding) = self.config.binds.read().unwrap().get(keys.as_ref()) { if let Some(binding) = self.config.binds.read().unwrap().get(keys.as_ref()) {
panic!("{binding:?}"); panic!("{binding:?}");
} }
} }
Ok(None) Ok(None)
//Ok(if let Some(binding) = self.profile.as_ref() //Ok(if let Some(binding) = self.mode.as_ref()
//.map(|c|c.keys.dispatch(input.event())).flatten() //.map(|c|c.keys.dispatch(input.event())).flatten()
//{ //{
//let binding = binding.clone(); //let binding = binding.clone();

View file

@ -16,12 +16,12 @@ pub const VIEW: DslNs<'static, DslCb> = DslNs(&[
(":view/ports/ins", |state|Box::new(Fill::x(Fixed::y(3, (":view/ports/ins", |state|Box::new(Fill::x(Fixed::y(3,
Bsp::a(Fill::x(Align::w(" L AUDIO INS")), Bsp::a("MIDI INS", Fill::x(Align::e("AUDIO INS R ")))))))), Bsp::a(Fill::x(Align::w(" L AUDIO INS")), Bsp::a("MIDI INS", Fill::x(Align::e("AUDIO INS R ")))))))),
(":view/profiles", |state: &App|Box::new({ (":view/profiles", |state: &App|Box::new({
let views = state.config.views.clone(); let modes = state.config.modes.clone();
Stack::south(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{ Stack::south(move|add: &mut dyn FnMut(&dyn Render<TuiOut>)|{
for (index, (id, profile)) in views.read().unwrap().iter().enumerate() { for (index, (id, profile)) in modes.read().unwrap().iter().enumerate() {
let bg = if index == 0 { Rgb(64,64,64) } else { Rgb(32,32,32) }; let bg = if index == 0 { Rgb(64,64,64) } else { Rgb(32,32,32) };
let name = profile.mode.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>"); let name = profile.name.get(0).map(|x|x.as_ref()).unwrap_or("<no name>");
let info = profile.mode.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>"); let info = profile.info.get(0).map(|x|x.as_ref()).unwrap_or("<no info>");
add(&Fixed::y(3, Tui::bg(bg, Bsp::s( add(&Fixed::y(3, Tui::bg(bg, Bsp::s(
Fill::x(Bsp::a( Fill::x(Bsp::a(
Fill::x(Align::w(Tui::fg(Rgb(224,192,128), name))), Fill::x(Align::w(Tui::fg(Rgb(224,192,128), name))),

View file

@ -52,6 +52,7 @@ pub enum LaunchMode {
impl Cli { impl Cli {
pub fn run (&self) -> Usually<()> { pub fn run (&self) -> Usually<()> {
let name = self.name.as_ref().map_or("tek", |x|x.as_str()); let name = self.name.as_ref().map_or("tek", |x|x.as_str());
let config = Config::init()?;
let empty = &[] as &[&str]; let empty = &[] as &[&str];
let mut midi_ins = vec![]; let mut midi_ins = vec![];
let mut midi_outs = vec![]; let mut midi_outs = vec![];
@ -72,11 +73,10 @@ impl Cli {
for (index, connect) in midi_tos.iter().enumerate() { for (index, connect) in midi_tos.iter().enumerate() {
midi_outs.push(jack.midi_out(&format!("{index}/M"), &[connect.clone()])?); midi_outs.push(jack.midi_out(&format!("{index}/M"), &[connect.clone()])?);
}; };
let configs = Config::init();
let clock = Clock::new(&jack, self.bpm)?; let clock = Clock::new(&jack, self.bpm)?;
let mut app = App { let mut app = App {
jack: jack.clone(), jack: jack.clone(),
config: Config::init()?, config,
color: ItemTheme::random(), color: ItemTheme::random(),
dialog: Dialog::Menu(0), dialog: Dialog::Menu(0),
project: Arrangement { project: Arrangement {

2
deps/tengri vendored

@ -1 +1 @@
Subproject commit e839096cf33f72c000d60b73958e1d6a0ec82be5 Subproject commit 7fd6c91643cbcfece56ebc14500c6a1ab775fc9e

138
tek.edn Normal file
View file

@ -0,0 +1,138 @@
(keys :help (@f1 dialog :help))
(keys :back (@escape back))
(keys :axis/x (@left x/prev) (@right x/next))
(keys :axis/x2 (@shift/left x2/prev) (@shift/right x2/next))
(keys :axis/y (@up y/prev) (@down y/next))
(keys :axis/y2 (@shift/up y2/prev) (@shift/down y2/next))
(keys :axis/z (@comma z/prev) (@period z/next))
(keys :axis/z2 (@lt z2/prev) (@gt z2/next))
(keys :axis/w (@openbracket w/prev) (@closebracket w/next))
(keys :axis/w2 (@openbrace w2/prev) (@closebrace w2/next))
(keys :axis/page (@pgup page/up) (@pgdn page/down))
(keys :confirm (@enter confirm))
(keys :delete (@delete delete))
(keys :backspace (@backspace backspace))
(keys :input (see :delete :axis/x :backspace) (:char input))
(keys :length (see :confirm :axis/y :axis/x))
(keys :list (see :confirm :axis/y))
(keys :browse (see :list :input :focus))
(keys :focus)
(keys :history (@u undo 1) (@r redo 1))
(keys :clock (@space clock/toggle 0) (@shift/space clock/toggle 0))
(keys :color (@c color))
(keys :launch (@q launch))
(keys :saveload (@f6 dialog :save) (@f9 dialog :load))
(keys :global (see :history :saveload) (@f8 dialog :options) (@f10 dialog :quit))
(mode :transport (name Transport) (info A JACK transport controller.)
(keys :clock :global) :view/transport)
(mode :arranger (name Arranger) (info A grid of launchable clips arranged by track and scene.)
(when :mode/editor (keys :editor))
(when :mode/dialog (keys :dialog))
(when :mode/message (keys :message))
(when :mode/add-device (keys :add-device))
(when :mode/browse (keys :browse))
(when :mode/rename (keys :pool/rename))
(when :mode/length (keys :pool/length))
(when :mode/clip (keys :clip))
(when :mode/track (keys :track))
(when :mode/scene (keys :scene))
(when :mode/mix (keys :mix))
(keys :clock :arranger :global)
(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 :mode/editor
(bsp/e :view/scenes/names :view/editor)
:view/scenes)))))))
(keys :arranger (see :color :launch :scenes :tracks)
(@tab project/edit) (@enter project/edit)
(@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))
(keys :tracks
(@t select :select/track) (@left select :select/track/prev) (@right select :select/track/next))
(keys :track (see :color :launch :axis/z :axis/z2 :delete)
(@r toggle :rec) (@m toggle :mon) (@p toggle :play) (@P toggle :solo))
(keys :scenes
(@s select :select/scene) (@up select :select/scene/prev) (@down select :select/scene/next))
(keys :scene
(see :color :launch :axis/z :axis/z2 :delete))
(keys :clip (see :color :launch :axis/z :axis/z2 :delete)
(@l toggle :loop))
(mode :groovebox (name Groovebox) (info A sequencer with built-in sampler.)
(when :mode/browse (keys :browse))
(when :mode/rename (keys :pool-rename))
(when :mode/length (keys :pool-length))
(keys :clock :editor :sampler :global)
(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)))))))
(mode :sampler (name Sampler) (info A sampling soundboard.)
(keys :sampler :global)
(bsp/s (fixed/y 1 :view/transport)
(bsp/n (fixed/y 1 :view/status)
(fill/xy :view/samples/grid))))
(keys :sampler (see :sampler/directions :sampler/record :sampler/play))
(keys :sampler/play (@p sampler/play/sample :sample/selected) (@P sampler/stop/sample :sample/selected))
(keys :sampler/record (@r sampler/record/toggle :sample/selected) (@shift/R sampler/record/back))
(keys :sampler/import-export (@shift/f6 dialog :dialog/export/sample) (@shift/f9 dialog :dialog/import/sample))
(keys :sampler/directions (@up sampler/select :sample/above) (@down sampler/select :sample/below) (@left sampler/select :sample/to/left) (@right sampler/select :sample/to/right))
(mode :sequencer (name Sequencer) (info A MIDI sequencer.)
(when :mode/browse (keys :browse))
(when :mode/rename (keys :pool-rename))
(when :mode/length (keys :pool-length))
(keys :editor :clock :global)
(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)))))
(keys :sequencer (see :color :launch) (@shift/I input/add) (@shift/O output/add))
(keys :pool (see :axis-y :axis-w :axis/z2 :color :delete)
(@n rename/begin) (@t length/begin) (@m import/begin) (@x export/begin)
(@shift/A clip/add :after :new/clip)
(@shift/D clip/add :after :cloned/clip))
(keys :editor (see :editor/view :editor/note))
(keys :editor/view
(@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))
(keys :editor/note
(@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))