mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 03:36:41 +01:00
parent
cfd19062fd
commit
559d2fc4a1
7 changed files with 125 additions and 72 deletions
|
|
@ -31,12 +31,18 @@ pub struct App {
|
||||||
/// Contains the currently edited musical arrangement
|
/// Contains the currently edited musical arrangement
|
||||||
pub project: Arrangement,
|
pub project: Arrangement,
|
||||||
}
|
}
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
|
pub struct Axis {
|
||||||
|
min: usize,
|
||||||
|
max: usize,
|
||||||
|
step: usize,
|
||||||
|
}
|
||||||
/// Various possible dialog modes.
|
/// Various possible dialog modes.
|
||||||
#[derive(Debug, Clone, Default, PartialEq)]
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
pub enum Dialog {
|
pub enum Dialog {
|
||||||
#[default] None,
|
#[default] None,
|
||||||
Help(usize),
|
Help(usize),
|
||||||
Menu(usize, usize),
|
Menu(usize, Arc<[Arc<str>]>),
|
||||||
Device(usize),
|
Device(usize),
|
||||||
Message(Arc<str>),
|
Message(Arc<str>),
|
||||||
Browse(BrowseTarget, Arc<Browse>),
|
Browse(BrowseTarget, Arc<Browse>),
|
||||||
|
|
@ -62,6 +68,25 @@ impl HasClipsSize for App { fn clips_size (&self) -> &Measure<TuiOut> { &self.
|
||||||
impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } }
|
impl HasTrackScroll for App { fn track_scroll (&self) -> usize { self.project.track_scroll() } }
|
||||||
impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } }
|
impl HasSceneScroll for App { fn scene_scroll (&self) -> usize { self.project.scene_scroll() } }
|
||||||
impl Dialog {
|
impl Dialog {
|
||||||
|
pub fn welcome () -> Self {
|
||||||
|
Self::Menu(0, [
|
||||||
|
"Continue session".into(),
|
||||||
|
"Load old session".into(),
|
||||||
|
"Begin new session".into(),
|
||||||
|
].into())
|
||||||
|
}
|
||||||
|
pub fn menu_next (&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Menu(index, items) => Self::Menu(wrap_inc(*index, items.len()), items.clone()),
|
||||||
|
_ => Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn menu_prev (&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Menu(index, items) => Self::Menu(wrap_dec(*index, items.len()), items.clone()),
|
||||||
|
_ => Self::None
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn menu_selected (&self) -> Option<usize> {
|
pub fn menu_selected (&self) -> Option<usize> {
|
||||||
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
|
if let Self::Menu(selected, _) = self { Some(*selected) } else { None }
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +191,6 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn wrap_dialog (dialog: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
fn wrap_dialog (dialog: impl Content<TuiOut>) -> impl Content<TuiOut> {
|
||||||
Fixed::xy(70, 23, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b(
|
Fixed::xy(70, 23, Tui::fg_bg(Rgb(255,255,255), Rgb(16,16,16), Bsp::b(
|
||||||
Repeat(" "), Outer(true, Style::default().fg(Tui::g(96))).enclose(dialog))))
|
Repeat(" "), Outer(true, Style::default().fg(Tui::g(96))).enclose(dialog))))
|
||||||
|
|
|
||||||
|
|
@ -30,34 +30,55 @@ handle!(TuiIn:|self: App, input|{
|
||||||
Ok(None)
|
Ok(None)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub enum Axis { X, Y, Z, I }
|
||||||
|
|
||||||
impl<'t> DslNs<'t, AppCommand> for App {
|
impl<'t> DslNs<'t, AppCommand> for App {
|
||||||
dsl_exprs!(|app| -> AppCommand { /* TODO */ });
|
dsl_exprs!(|app| -> AppCommand { /* TODO */ });
|
||||||
dsl_words!(|app| -> AppCommand {
|
dsl_words!(|app| -> AppCommand {
|
||||||
"y/inc" => match app.dialog {
|
"x/inc" => AppCommand::Inc { axis: Axis::X },
|
||||||
Dialog::Menu(index, count) => AppCommand::SetDialog {
|
"x/dec" => AppCommand::Dec { axis: Axis::X },
|
||||||
dialog: Dialog::Menu(if count > 0 {
|
"y/inc" => AppCommand::Inc { axis: Axis::Y },
|
||||||
(index + 1) % count
|
"y/dec" => AppCommand::Dec { axis: Axis::Y },
|
||||||
} else { 0 }, count)
|
"confirm" => AppCommand::Confirm,
|
||||||
},
|
"cancel" => AppCommand::Cancel,
|
||||||
_ => todo!(),
|
|
||||||
},
|
|
||||||
"y/dec" => match app.dialog {
|
|
||||||
Dialog::Menu(index, count) => AppCommand::SetDialog {
|
|
||||||
dialog: Dialog::Menu(if count > 0 {
|
|
||||||
index.overflowing_sub(1).0.min(count.saturating_sub(1))
|
|
||||||
} else { 0 }, count)
|
|
||||||
},
|
|
||||||
_ => todo!(),
|
|
||||||
},
|
|
||||||
"confirm" => todo!(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for AppCommand { fn default () -> Self { Self::Nop } }
|
||||||
|
|
||||||
def_command!(AppCommand: |app: App| {
|
def_command!(AppCommand: |app: App| {
|
||||||
SetDialog { dialog: Dialog } =>
|
Nop => Ok(None),
|
||||||
swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog }),
|
Confirm => todo!(),
|
||||||
|
Cancel => todo!(), // TODO delegate:
|
||||||
|
Inc { axis: Axis } => Ok(match (&app.dialog, axis) {
|
||||||
|
(Dialog::None, _) => todo!(),
|
||||||
|
(Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_next() }
|
||||||
|
.execute(app)?,
|
||||||
|
_ => todo!()
|
||||||
|
}),
|
||||||
|
Dec { axis: Axis } => Ok(match (&app.dialog, axis) {
|
||||||
|
(Dialog::None, _) => None,
|
||||||
|
(Dialog::Menu(_, _), Axis::Y) => AppCommand::SetDialog { dialog: app.dialog.menu_prev() }
|
||||||
|
.execute(app)?,
|
||||||
|
_ => todo!()
|
||||||
|
}),
|
||||||
|
SetDialog { dialog: Dialog } => {
|
||||||
|
swap_value(&mut app.dialog, dialog, |dialog|Self::SetDialog { dialog })
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
pub fn wrap_inc (index: usize, count: usize) -> usize {
|
||||||
|
if count > 0 { (index + 1) % count } else { 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wrap_dec (index: usize, count: usize) -> usize {
|
||||||
|
if count > 0 { index.overflowing_sub(1).0.min(count.saturating_sub(1)) } else { 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dialog {
|
||||||
|
}
|
||||||
|
|
||||||
//AppCommand => {
|
//AppCommand => {
|
||||||
//("x/inc" /
|
//("x/inc" /
|
||||||
//("stop-all") => todo!(),//app.project.stop_all(),
|
//("stop-all") => todo!(),//app.project.stop_all(),
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ impl<'t> DslNs<'t, Dialog> for App {
|
||||||
":dialog/device/prev" => Dialog::Device(0),
|
":dialog/device/prev" => Dialog::Device(0),
|
||||||
":dialog/device/next" => Dialog::Device(0),
|
":dialog/device/next" => Dialog::Device(0),
|
||||||
":dialog/help" => Dialog::Help(0),
|
":dialog/help" => Dialog::Help(0),
|
||||||
":dialog/menu" => Dialog::Menu(0, 0),
|
|
||||||
":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject,
|
":dialog/save" => Dialog::Browse(BrowseTarget::SaveProject,
|
||||||
Browse::new(None).unwrap().into()),
|
Browse::new(None).unwrap().into()),
|
||||||
":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject,
|
":dialog/load" => Dialog::Browse(BrowseTarget::LoadProject,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
pub fn view_nil (_: &App) -> Box<dyn Render<TuiOut>> {
|
|
||||||
Box::new(Fill::xy("·"))
|
|
||||||
}
|
|
||||||
|
|
||||||
content!(TuiOut:|self: App|Fill::xy(Stack::above(|add|{
|
content!(TuiOut:|self: App|Fill::xy(Stack::above(|add|{
|
||||||
for dsl in self.mode.view.iter() { add(&Fill::xy(self.view(dsl.as_ref()))); }
|
for dsl in self.mode.view.iter() { add(&Fill::xy(self.view(dsl.as_ref()))); }
|
||||||
})));
|
})));
|
||||||
|
|
@ -20,6 +16,7 @@ impl App {
|
||||||
ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80)
|
ViewCache::update_clock(&self.project.clock.view_cache, self.clock(), self.size.w() > 80)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_dsl <'t> (
|
fn render_dsl <'t> (
|
||||||
state: &'t impl DslNs<'t, Box<dyn Render<TuiOut>>>,
|
state: &'t impl DslNs<'t, Box<dyn Render<TuiOut>>>,
|
||||||
src: &str
|
src: &str
|
||||||
|
|
@ -86,6 +83,21 @@ impl<'t> DslNs<'t, Box<dyn Render<TuiOut>>> for App {
|
||||||
"max/xy" (x: u16, y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Max::xy(x, y, c)),
|
"max/xy" (x: u16, y: u16, c: Box<dyn Render<TuiOut>>) => Box::new(Max::xy(x, y, c)),
|
||||||
});
|
});
|
||||||
dsl_words!(|app| -> Box<dyn Render<TuiOut>> {
|
dsl_words!(|app| -> Box<dyn Render<TuiOut>> {
|
||||||
|
":dialog/menu" => Box::new(if let Dialog::Menu(selected, items) = &app.dialog {
|
||||||
|
let items = items.clone();
|
||||||
|
let selected = *selected;
|
||||||
|
Some(Fill::xy(Align::c(Tui::bg(Red, Fill::x(Stack::south(move|add|{
|
||||||
|
for (index, item) in items.iter().enumerate() {
|
||||||
|
add(&Tui::fg_bg(
|
||||||
|
if selected == index { Rgb(240,200,180) } else { Rgb(200, 200, 200) },
|
||||||
|
if selected == index { Rgb(80, 80, 50) } else { Rgb(30, 30, 30) },
|
||||||
|
Fixed::y(2, Align::n(Fill::x(item)))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}))))))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}),
|
||||||
":templates" => Box::new({
|
":templates" => Box::new({
|
||||||
let modes = app.config.modes.clone();
|
let modes = app.config.modes.clone();
|
||||||
let height = (modes.read().unwrap().len() * 2) as u16;
|
let height = (modes.read().unwrap().len() * 2) as u16;
|
||||||
|
|
@ -162,6 +174,10 @@ impl<'t> DslNs<'t, Box<dyn Render<TuiOut>>> for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn view_nil (_: &App) -> Box<dyn Render<TuiOut>> {
|
||||||
|
Box::new(Fill::xy("·"))
|
||||||
|
}
|
||||||
|
|
||||||
//Bsp::s("",
|
//Bsp::s("",
|
||||||
//Map::south(1,
|
//Map::south(1,
|
||||||
//move||app.config.binds.layers.iter()
|
//move||app.config.binds.layers.iter()
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ impl Cli {
|
||||||
let app = App {
|
let app = App {
|
||||||
jack: jack.clone(),
|
jack: jack.clone(),
|
||||||
color: ItemTheme::random(),
|
color: ItemTheme::random(),
|
||||||
dialog: Dialog::Menu(0, 0),
|
dialog: Dialog::welcome(),
|
||||||
mode: config.modes.clone().read().unwrap().get(":menu").cloned().unwrap(),
|
mode: config.modes.clone().read().unwrap().get(":menu").cloned().unwrap(),
|
||||||
config,
|
config,
|
||||||
project: Arrangement {
|
project: Arrangement {
|
||||||
|
|
|
||||||
|
|
@ -124,33 +124,28 @@ impl Mode<Arc<str>> {
|
||||||
modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
|
modes.write().unwrap().insert(name.as_ref().into(), Arc::new(mode));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
pub fn load_one (&mut self, item: impl Dsl) -> Usually<()> {
|
pub fn load_one (&mut self, dsl: impl Dsl) -> Usually<()> {
|
||||||
Ok(if let Ok(Some(key)) = item.expr().head() {
|
Ok(if let Ok(Some(expr)) = dsl.expr() && let Ok(Some(key)) = expr.head() {
|
||||||
|
println!("Mode::load_one: {key} {:?}", expr.tail()?);
|
||||||
|
let tail = expr.tail()?.map(|x|x.trim()).unwrap_or("");
|
||||||
match key {
|
match key {
|
||||||
"name" => {
|
"name" => self.name.push(tail.into()),
|
||||||
self.name.push(item.tail()?.map(|x|x.trim()).unwrap_or("").into())
|
"info" => self.info.push(tail.into()),
|
||||||
},
|
"view" => self.view.push(tail.into()),
|
||||||
"info" => {
|
"keys" => tail.each(|expr|{self.keys.push(expr.trim().into()); Ok(())})?,
|
||||||
self.info.push(item.tail()?.map(|x|x.trim()).unwrap_or("").into())
|
"mode" => if let Some(id) = tail.head()? {
|
||||||
},
|
Self::load_into(&self.modes, &id, &tail.tail())?;
|
||||||
"keys" => {
|
|
||||||
item.tail()?.each(|item|{
|
|
||||||
self.keys.push(item.trim().into());
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
},
|
|
||||||
"self" => if let Some(id) = item.tail()?.head()? {
|
|
||||||
Self::load_into(&self.modes, &id, &item.tail().tail())?;
|
|
||||||
} else {
|
} else {
|
||||||
return Err(format!("Mode::load_one: self: incomplete: {item:?}").into());
|
return Err(format!("Mode::load_one: self: incomplete: {expr:?}").into());
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return Err(format!("Mode::load_one: unexpected expr: {key:?} {tail:?}").into())
|
||||||
},
|
},
|
||||||
_ if let Some(src) = item.src()? => self.view.push(src.into()),
|
|
||||||
_ => {},
|
|
||||||
};
|
};
|
||||||
} else if let Ok(Some(word)) = item.word() {
|
} else if let Ok(Some(word)) = dsl.word() {
|
||||||
self.view.push(word.into());
|
self.view.push(word.into());
|
||||||
} else {
|
} else {
|
||||||
return Err(format!("Mode::load_one: unexpected: {item:?}").into());
|
return Err(format!("Mode::load_one: unexpected: {dsl:?}").into());
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
46
tek.edn
46
tek.edn
|
|
@ -1,12 +1,18 @@
|
||||||
(mode :menu (keys :y :confirm) :menu)
|
(keys :axis/x (@left x/dec) (@right x/inc))
|
||||||
(keys :x (@left x/dec) (@right x/inc))
|
(keys :axis/x2 (@shift/left x2/dec) (@shift/right x2/inc))
|
||||||
(keys :y (@up y/dec) (@down y/inc))
|
(keys :axis/y (@up y/dec) (@down y/inc))
|
||||||
|
(keys :axis/y2 (@shift/up y2/dec) (@shift/down y2/inc))
|
||||||
|
(keys :axis/z (@minus z/dec) (@equal z/inc))
|
||||||
|
(keys :axis/z2 (@underscore z2/dec) (@plus z2/inc))
|
||||||
|
(keys :axis/i (@comma i/dec) (@period z/inc))
|
||||||
|
(keys :axis/i2 (@lt i2/dec) (@gt z2/inc))
|
||||||
|
(keys :axis/w (@openbracket w/dec) (@closebracket w/inc))
|
||||||
|
(keys :axis/w2 (@openbrace w2/dec) (@closebrace w2/inc))
|
||||||
|
|
||||||
|
(mode :menu (keys :axis/y :confirm) :menu)
|
||||||
(keys :confirm (@enter confirm))
|
(keys :confirm (@enter confirm))
|
||||||
(view :menu (bg (g 40) (bsp/s :ports/out (bsp/n :ports/in
|
(view :menu (bg (g 40) (bsp/s :ports/out (bsp/n :ports/in
|
||||||
(bg (g 30) (fill/xy (align/c (bg (g 40) (bsp/s
|
(bg (g 30) (fill/xy (align/c :dialog/menu)))))))
|
||||||
(text CONTINUE-SESSION)
|
|
||||||
(bsp/s (text LOAD-OTHER-SESSION)
|
|
||||||
(text BEGIN-NEW-SESSION)))))))))))
|
|
||||||
(view :ports/out (fill/x (fixed/y 3 (bsp/a (fill/x (align/w (text L-AUDIO-OUT)))
|
(view :ports/out (fill/x (fixed/y 3 (bsp/a (fill/x (align/w (text L-AUDIO-OUT)))
|
||||||
(bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT R))))))))
|
(bsp/a (text MIDI-OUT) (fill/x (align/e (text AUDIO-OUT R))))))))
|
||||||
(view :ports/in (fill/x (fixed/y 3 (bsp/a (fill/x (align/w (text L-AUDIO-IN)))
|
(view :ports/in (fill/x (fixed/y 3 (bsp/a (fill/x (align/w (text L-AUDIO-IN)))
|
||||||
|
|
@ -16,18 +22,10 @@
|
||||||
(keys :help (@f1 dialog :help))
|
(keys :help (@f1 dialog :help))
|
||||||
(keys :back (@escape back))
|
(keys :back (@escape back))
|
||||||
(keys :page (@pgup page/up) (@pgdn page/down))
|
(keys :page (@pgup page/up) (@pgdn page/down))
|
||||||
(keys :x2 (@shift/left x2/dec) (@shift/right x2/inc))
|
|
||||||
(keys :y2 (@shift/up y2/dec) (@shift/down y2/inc))
|
|
||||||
(keys :z (@minus z/dec) (@equal z/inc))
|
|
||||||
(keys :z2 (@underscore z2/dec) (@plus z2/inc))
|
|
||||||
(keys :i (@comma i/dec) (@period z/inc))
|
|
||||||
(keys :i2 (@lt i2/dec) (@gt z2/inc))
|
|
||||||
(keys :w (@openbracket w/dec) (@closebracket w/inc))
|
|
||||||
(keys :w2 (@openbrace w2/dec) (@closebrace w2/inc))
|
|
||||||
(keys :delete (@delete delete) (@backspace delete/back))
|
(keys :delete (@delete delete) (@backspace delete/back))
|
||||||
(keys :input (see :x :delete) (:char input))
|
(keys :input (see :axis/x :delete) (:char input))
|
||||||
(keys :list (see :y :confirm))
|
(keys :list (see :axis/y :confirm))
|
||||||
(keys :length (see :x :y :confirm))
|
(keys :length (see :axis/x :axis/y :confirm))
|
||||||
(keys :browse (see :list :input :focus))
|
(keys :browse (see :list :input :focus))
|
||||||
(keys :history (@u undo 1) (@r redo 1))
|
(keys :history (@u undo 1) (@r redo 1))
|
||||||
(keys :clock (@space clock/toggle 0) (@shift/space clock/toggle 0))
|
(keys :clock (@space clock/toggle 0) (@shift/space clock/toggle 0))
|
||||||
|
|
@ -80,15 +78,15 @@
|
||||||
(@up select :select/scene/dec)
|
(@up select :select/scene/dec)
|
||||||
(@down select :select/scene/inc))
|
(@down select :select/scene/inc))
|
||||||
|
|
||||||
(keys :track (see :color :launch :z :z2 :delete)
|
(keys :track (see :color :launch :axis/z :axis/z2 :delete)
|
||||||
(@r toggle :rec)
|
(@r toggle :rec)
|
||||||
(@m toggle :mon)
|
(@m toggle :mon)
|
||||||
(@p toggle :play)
|
(@p toggle :play)
|
||||||
(@P toggle :solo))
|
(@P toggle :solo))
|
||||||
|
|
||||||
(keys :scene (see :color :launch :z :z2 :delete))
|
(keys :scene (see :color :launch :axis/z :axis/z2 :delete))
|
||||||
|
|
||||||
(keys :clip (see :color :launch :z :z2 :delete)
|
(keys :clip (see :color :launch :axis/z :axis/z2 :delete)
|
||||||
(@l toggle :loop))
|
(@l toggle :loop))
|
||||||
|
|
||||||
(mode :groovebox
|
(mode :groovebox
|
||||||
|
|
@ -154,13 +152,13 @@
|
||||||
(keys :sequencer (see :color :launch)
|
(keys :sequencer (see :color :launch)
|
||||||
(@shift/I input/add)
|
(@shift/I input/add)
|
||||||
(@shift/O output/add))
|
(@shift/O output/add))
|
||||||
(keys :pool (see :axis-y :axis-w :z2 :color :delete)
|
(keys :pool (see :axis-y :axis-w :axis/z2 :color :delete)
|
||||||
(@n rename/begin) (@t length/begin) (@m import/begin) (@x export/begin)
|
(@n rename/begin) (@t length/begin) (@m import/begin) (@x export/begin)
|
||||||
(@shift/A clip/add :after :new/clip)
|
(@shift/A clip/add :after :new/clip)
|
||||||
(@shift/D clip/add :after :cloned/clip))
|
(@shift/D clip/add :after :cloned/clip))
|
||||||
(keys :editor (see :editor/view :editor/note))
|
(keys :editor (see :editor/view :editor/note))
|
||||||
(keys :editor/view (see :x :x2 :z :z2)
|
(keys :editor/view (see :axis/x :axis/x2 :axis/z :axis/z2)
|
||||||
(@z toggle :lock))
|
(@z toggle :lock))
|
||||||
(keys :editor/note (see :i :i2 :y :page)
|
(keys :editor/note (see :axis/i :axis/i2 :axis/y :page)
|
||||||
(@a editor/append :true) (@enter editor/append :false)
|
(@a editor/append :true) (@enter editor/append :false)
|
||||||
(@del editor/delete/note) (@shift/del editor/delete/note))
|
(@del editor/delete/note) (@shift/del editor/delete/note))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue