Enable binding multiple commands to a key.

This enables useful combinations of commands like `Space -> queue; move
down 1` using ';' as command separator. ';' can be escaped using ';;'.
This commit is contained in:
HMH
2021-09-06 12:17:36 +02:00
committed by Henrik Friedrichsen
parent d17c66f8ad
commit 102acd803e
4 changed files with 335 additions and 264 deletions

View File

@@ -196,6 +196,8 @@ impl fmt::Display for Command {
Command::Logout => "logout".to_string(), Command::Logout => "logout".to_string(),
Command::ShowRecommendations(mode) => format!("similar {}", mode), Command::ShowRecommendations(mode) => format!("similar {}", mode),
}; };
// escape the command separator
let repr = repr.replace(";", ";;");
write!(f, "{}", repr) write!(f, "{}", repr)
} }
} }
@@ -233,206 +235,236 @@ fn handle_aliases(input: &str) -> &str {
} }
} }
pub fn parse(input: &str) -> Option<Command> { pub fn parse(input: &str) -> Option<Vec<Command>> {
let components: Vec<_> = input.trim().split(' ').collect(); let mut command_inputs = vec!["".to_string()];
let mut command_idx = 0;
let command = handle_aliases(components[0]); enum ParseState {
let args = components[1..].to_vec(); Normal,
SeparatorEncountered,
match command { }
"quit" => Some(Command::Quit), let mut parse_state = ParseState::Normal;
"playpause" => Some(Command::TogglePlay), for c in input.chars() {
"stop" => Some(Command::Stop), let is_separator = c == ';';
"previous" => Some(Command::Previous), match parse_state {
"next" => Some(Command::Next), ParseState::Normal if is_separator => parse_state = ParseState::SeparatorEncountered,
"clear" => Some(Command::Clear), ParseState::Normal => command_inputs[command_idx].push(c),
"playnext" => Some(Command::PlayNext), // ";" is escaped using ";;", so if the previous char already was a ';' push a ';'.
"queue" => Some(Command::Queue), ParseState::SeparatorEncountered if is_separator => {
"play" => Some(Command::Play), command_inputs[command_idx].push(c);
"update" => Some(Command::UpdateLibrary), parse_state = ParseState::Normal;
"delete" => Some(Command::Delete), }
"back" => Some(Command::Back), ParseState::SeparatorEncountered => {
"open" => args command_idx += 1;
.get(0) command_inputs.push(c.to_string());
.and_then(|target| match *target { parse_state = ParseState::Normal;
"selected" => Some(TargetMode::Selected), }
"current" => Some(TargetMode::Current),
_ => None,
})
.map(Command::Open),
"jump" => Some(Command::Jump(JumpMode::Query(args.join(" ")))),
"search" => Some(Command::Search(args.join(" "))),
"shift" => {
let amount = args.get(1).and_then(|amount| amount.parse().ok());
args.get(0)
.and_then(|direction| match *direction {
"up" => Some(ShiftMode::Up),
"down" => Some(ShiftMode::Down),
_ => None,
})
.map(|mode| Command::Shift(mode, amount))
} }
"move" => { }
let cmd: Option<Command> = {
args.get(0).and_then(|extreme| match *extreme { let mut commands = vec![];
"top" => Some(Command::Move(MoveMode::Up, MoveAmount::Extreme)), for command_input in command_inputs {
"bottom" => Some(Command::Move(MoveMode::Down, MoveAmount::Extreme)), let components: Vec<_> = command_input.trim().split(' ').collect();
"leftmost" => Some(Command::Move(MoveMode::Left, MoveAmount::Extreme)),
"rightmost" => Some(Command::Move(MoveMode::Right, MoveAmount::Extreme)), let command = handle_aliases(components[0]);
"playing" => Some(Command::Move(MoveMode::Playing, MoveAmount::default())), let args = components[1..].to_vec();
let command = match command {
"quit" => Some(Command::Quit),
"playpause" => Some(Command::TogglePlay),
"stop" => Some(Command::Stop),
"previous" => Some(Command::Previous),
"next" => Some(Command::Next),
"clear" => Some(Command::Clear),
"playnext" => Some(Command::PlayNext),
"queue" => Some(Command::Queue),
"play" => Some(Command::Play),
"update" => Some(Command::UpdateLibrary),
"delete" => Some(Command::Delete),
"back" => Some(Command::Back),
"open" => args
.get(0)
.and_then(|target| match *target {
"selected" => Some(TargetMode::Selected),
"current" => Some(TargetMode::Current),
_ => None, _ => None,
}) })
}; .map(Command::Open),
"jump" => Some(Command::Jump(JumpMode::Query(args.join(" ")))),
cmd.or({ "search" => Some(Command::Search(args.join(" "))),
let amount = args "shift" => {
.get(1) let amount = args.get(1).and_then(|amount| amount.parse().ok());
.and_then(|amount| amount.parse().ok())
.map(MoveAmount::Integer)
.unwrap_or_default();
args.get(0) args.get(0)
.and_then(|direction| match *direction { .and_then(|direction| match *direction {
"up" => Some(MoveMode::Up), "up" => Some(ShiftMode::Up),
"down" => Some(MoveMode::Down), "down" => Some(ShiftMode::Down),
"left" => Some(MoveMode::Left),
"right" => Some(MoveMode::Right),
_ => None, _ => None,
}) })
.map(|mode| Command::Move(mode, amount)) .map(|mode| Command::Shift(mode, amount))
})
}
"goto" => args
.get(0)
.and_then(|mode| match *mode {
"album" => Some(GotoMode::Album),
"artist" => Some(GotoMode::Artist),
_ => None,
})
.map(Command::Goto),
"share" => args
.get(0)
.and_then(|target| match *target {
"selected" => Some(TargetMode::Selected),
"current" => Some(TargetMode::Current),
_ => None,
})
.map(Command::Share),
"shuffle" => {
let shuffle = args.get(0).and_then(|mode| match *mode {
"on" => Some(true),
"off" => Some(false),
_ => None,
});
Some(Command::Shuffle(shuffle))
}
"repeat" => {
let mode = args.get(0).and_then(|mode| match *mode {
"list" | "playlist" | "queue" => Some(RepeatSetting::RepeatPlaylist),
"track" | "once" => Some(RepeatSetting::RepeatTrack),
"none" | "off" => Some(RepeatSetting::None),
_ => None,
});
Some(Command::Repeat(mode))
}
"seek" => args.get(0).and_then(|arg| match arg.chars().next() {
Some(x) if x == '-' || x == '+' => arg
.chars()
.skip(1)
.collect::<String>()
.parse::<i32>()
.ok()
.map(|amount| {
Command::Seek(SeekDirection::Relative(
amount
* match x {
'-' => -1,
_ => 1,
},
))
}),
_ => arg
.chars()
.collect::<String>()
.parse()
.ok()
.map(|amount| Command::Seek(SeekDirection::Absolute(amount))),
}),
"focus" => args
.get(0)
.map(|target| Command::Focus((*target).to_string())),
"save" => args
.get(0)
.map(|target| match *target {
"queue" => Command::SaveQueue,
_ => Command::Save,
})
.or(Some(Command::Save)),
"volup" => Some(Command::VolumeUp(
args.get(0).and_then(|v| v.parse::<u16>().ok()).unwrap_or(1),
)),
"voldown" => Some(Command::VolumeDown(
args.get(0).and_then(|v| v.parse::<u16>().ok()).unwrap_or(1),
)),
"help" => Some(Command::Help),
"reload" => Some(Command::ReloadConfig),
"insert" => {
if args.is_empty() {
Some(Command::Insert(None))
} else {
args.get(0)
.map(|url| Command::Insert(Some((*url).to_string())))
} }
} "move" => {
"newplaylist" => { let cmd: Option<Command> = {
if !args.is_empty() { args.get(0).and_then(|extreme| match *extreme {
Some(Command::NewPlaylist(args.join(" "))) "top" => Some(Command::Move(MoveMode::Up, MoveAmount::Extreme)),
} else { "bottom" => Some(Command::Move(MoveMode::Down, MoveAmount::Extreme)),
None "leftmost" => Some(Command::Move(MoveMode::Left, MoveAmount::Extreme)),
} "rightmost" => Some(Command::Move(MoveMode::Right, MoveAmount::Extreme)),
} "playing" => Some(Command::Move(MoveMode::Playing, MoveAmount::default())),
"sort" => { _ => None,
if !args.is_empty() {
let sort_key = args.get(0).and_then(|key| match *key {
"title" => Some(SortKey::Title),
"duration" => Some(SortKey::Duration),
"album" => Some(SortKey::Album),
"added" => Some(SortKey::Added),
"artist" => Some(SortKey::Artist),
_ => None,
})?;
let sort_direction = args
.get(1)
.map(|direction| match *direction {
"a" => SortDirection::Ascending,
"asc" => SortDirection::Ascending,
"ascending" => SortDirection::Ascending,
"d" => SortDirection::Descending,
"desc" => SortDirection::Descending,
"descending" => SortDirection::Descending,
_ => SortDirection::Ascending,
}) })
.unwrap_or(SortDirection::Ascending); };
Some(Command::Sort(sort_key, sort_direction)) cmd.or({
} else { let amount = args
None .get(1)
.and_then(|amount| amount.parse().ok())
.map(MoveAmount::Integer)
.unwrap_or_default();
args.get(0)
.and_then(|direction| match *direction {
"up" => Some(MoveMode::Up),
"down" => Some(MoveMode::Down),
"left" => Some(MoveMode::Left),
"right" => Some(MoveMode::Right),
_ => None,
})
.map(|mode| Command::Move(mode, amount))
})
} }
} "goto" => args
"logout" => Some(Command::Logout), .get(0)
"similar" => args .and_then(|mode| match *mode {
.get(0) "album" => Some(GotoMode::Album),
.and_then(|target| match *target { "artist" => Some(GotoMode::Artist),
"selected" => Some(TargetMode::Selected), _ => None,
"current" => Some(TargetMode::Current), })
_ => None, .map(Command::Goto),
}) "share" => args
.map(Command::ShowRecommendations), .get(0)
"noop" => Some(Command::Noop), .and_then(|target| match *target {
_ => None, "selected" => Some(TargetMode::Selected),
"current" => Some(TargetMode::Current),
_ => None,
})
.map(Command::Share),
"shuffle" => {
let shuffle = args.get(0).and_then(|mode| match *mode {
"on" => Some(true),
"off" => Some(false),
_ => None,
});
Some(Command::Shuffle(shuffle))
}
"repeat" => {
let mode = args.get(0).and_then(|mode| match *mode {
"list" | "playlist" | "queue" => Some(RepeatSetting::RepeatPlaylist),
"track" | "once" => Some(RepeatSetting::RepeatTrack),
"none" | "off" => Some(RepeatSetting::None),
_ => None,
});
Some(Command::Repeat(mode))
}
"seek" => args.get(0).and_then(|arg| match arg.chars().next() {
Some(x) if x == '-' || x == '+' => arg
.chars()
.skip(1)
.collect::<String>()
.parse::<i32>()
.ok()
.map(|amount| {
Command::Seek(SeekDirection::Relative(
amount
* match x {
'-' => -1,
_ => 1,
},
))
}),
_ => arg
.chars()
.collect::<String>()
.parse()
.ok()
.map(|amount| Command::Seek(SeekDirection::Absolute(amount))),
}),
"focus" => args
.get(0)
.map(|target| Command::Focus((*target).to_string())),
"save" => args
.get(0)
.map(|target| match *target {
"queue" => Command::SaveQueue,
_ => Command::Save,
})
.or(Some(Command::Save)),
"volup" => Some(Command::VolumeUp(
args.get(0).and_then(|v| v.parse::<u16>().ok()).unwrap_or(1),
)),
"voldown" => Some(Command::VolumeDown(
args.get(0).and_then(|v| v.parse::<u16>().ok()).unwrap_or(1),
)),
"help" => Some(Command::Help),
"reload" => Some(Command::ReloadConfig),
"insert" => {
if args.is_empty() {
Some(Command::Insert(None))
} else {
args.get(0)
.map(|url| Command::Insert(Some((*url).to_string())))
}
}
"newplaylist" => {
if !args.is_empty() {
Some(Command::NewPlaylist(args.join(" ")))
} else {
None
}
}
"sort" => {
if !args.is_empty() {
let sort_key = args.get(0).and_then(|key| match *key {
"title" => Some(SortKey::Title),
"duration" => Some(SortKey::Duration),
"album" => Some(SortKey::Album),
"added" => Some(SortKey::Added),
"artist" => Some(SortKey::Artist),
_ => None,
})?;
let sort_direction = args
.get(1)
.map(|direction| match *direction {
"a" => SortDirection::Ascending,
"asc" => SortDirection::Ascending,
"ascending" => SortDirection::Ascending,
"d" => SortDirection::Descending,
"desc" => SortDirection::Descending,
"descending" => SortDirection::Descending,
_ => SortDirection::Ascending,
})
.unwrap_or(SortDirection::Ascending);
Some(Command::Sort(sort_key, sort_direction))
} else {
None
}
}
"logout" => Some(Command::Logout),
"similar" => args
.get(0)
.and_then(|target| match *target {
"selected" => Some(TargetMode::Selected),
"current" => Some(TargetMode::Current),
_ => None,
})
.map(Command::ShowRecommendations),
"noop" => Some(Command::Noop),
_ => None,
};
commands.push(command?);
} }
Some(commands)
} }

View File

@@ -33,7 +33,7 @@ pub enum CommandResult {
pub struct CommandManager { pub struct CommandManager {
aliases: HashMap<String, String>, aliases: HashMap<String, String>,
bindings: RefCell<HashMap<String, Command>>, bindings: RefCell<HashMap<String, Vec<Command>>>,
spotify: Spotify, spotify: Spotify,
queue: Arc<Queue>, queue: Arc<Queue>,
library: Arc<Library>, library: Arc<Library>,
@@ -61,7 +61,7 @@ impl CommandManager {
} }
} }
pub fn get_bindings(config: Arc<Config>) -> HashMap<String, Command> { pub fn get_bindings(config: Arc<Config>) -> HashMap<String, Vec<Command>> {
let config = config.values(); let config = config.values();
let mut kb = if config.default_keybindings.unwrap_or(true) { let mut kb = if config.default_keybindings.unwrap_or(true) {
Self::default_keybindings() Self::default_keybindings()
@@ -70,12 +70,12 @@ impl CommandManager {
}; };
let custom_bindings: Option<HashMap<String, String>> = config.keybindings.clone(); let custom_bindings: Option<HashMap<String, String>> = config.keybindings.clone();
for (key, command) in custom_bindings.unwrap_or_default() { for (key, commands) in custom_bindings.unwrap_or_default() {
if let Some(command) = parse(&command) { if let Some(commands) = parse(&commands) {
info!("Custom keybinding: {} -> {:?}", key, command); info!("Custom keybinding: {} -> {:?}", key, commands);
kb.insert(key, command); kb.insert(key, commands);
} else { } else {
error!("Invalid command for key {}: {}", key, command); error!("Invalid command(s) for key {}: {}", key, commands);
} }
} }
@@ -309,11 +309,13 @@ impl CommandManager {
&self, &self,
cursive: &mut Cursive, cursive: &mut Cursive,
event: E, event: E,
command: Command, commands: Vec<Command>,
) { ) {
cursive.add_global_callback(event, move |s| { cursive.add_global_callback(event, move |s| {
if let Some(data) = s.user_data::<UserData>().cloned() { if let Some(data) = s.user_data::<UserData>().cloned() {
data.cmd.handle(s, command.clone()); for command in commands.clone().into_iter() {
data.cmd.handle(s, command);
}
} }
}); });
} }
@@ -340,133 +342,160 @@ impl CommandManager {
} }
} }
fn default_keybindings() -> HashMap<String, Command> { fn default_keybindings() -> HashMap<String, Vec<Command>> {
let mut kb = HashMap::new(); let mut kb = HashMap::new();
kb.insert("q".into(), Command::Quit); kb.insert("q".into(), vec![Command::Quit]);
kb.insert("Shift+p".into(), Command::TogglePlay); kb.insert("Shift+p".into(), vec![Command::TogglePlay]);
kb.insert("Shift+u".into(), Command::UpdateLibrary); kb.insert("Shift+u".into(), vec![Command::UpdateLibrary]);
kb.insert("Shift+s".into(), Command::Stop); kb.insert("Shift+s".into(), vec![Command::Stop]);
kb.insert("<".into(), Command::Previous); kb.insert("<".into(), vec![Command::Previous]);
kb.insert(">".into(), Command::Next); kb.insert(">".into(), vec![Command::Next]);
kb.insert("c".into(), Command::Clear); kb.insert("c".into(), vec![Command::Clear]);
kb.insert("Space".into(), Command::Queue); kb.insert(
kb.insert(".".into(), Command::PlayNext); "Space".into(),
kb.insert("Enter".into(), Command::Play); vec![
kb.insert("n".into(), Command::Jump(JumpMode::Next)); Command::Queue,
kb.insert("Shift+n".into(), Command::Jump(JumpMode::Previous)); Command::Move(MoveMode::Down, Default::default()),
kb.insert("s".into(), Command::Save); ],
kb.insert("Ctrl+s".into(), Command::SaveQueue); );
kb.insert("d".into(), Command::Delete); kb.insert(
kb.insert("f".into(), Command::Seek(SeekDirection::Relative(1000))); ".".into(),
kb.insert("b".into(), Command::Seek(SeekDirection::Relative(-1000))); vec![
Command::PlayNext,
Command::Move(MoveMode::Down, Default::default()),
],
);
kb.insert("Enter".into(), vec![Command::Play]);
kb.insert("n".into(), vec![Command::Jump(JumpMode::Next)]);
kb.insert("Shift+n".into(), vec![Command::Jump(JumpMode::Previous)]);
kb.insert("s".into(), vec![Command::Save]);
kb.insert("Ctrl+s".into(), vec![Command::SaveQueue]);
kb.insert("d".into(), vec![Command::Delete]);
kb.insert(
"f".into(),
vec![Command::Seek(SeekDirection::Relative(1000))],
);
kb.insert(
"b".into(),
vec![Command::Seek(SeekDirection::Relative(-1000))],
);
kb.insert( kb.insert(
"Shift+f".into(), "Shift+f".into(),
Command::Seek(SeekDirection::Relative(10000)), vec![Command::Seek(SeekDirection::Relative(10000))],
); );
kb.insert( kb.insert(
"Shift+b".into(), "Shift+b".into(),
Command::Seek(SeekDirection::Relative(-10000)), vec![Command::Seek(SeekDirection::Relative(-10000))],
); );
kb.insert("+".into(), Command::VolumeUp(1)); kb.insert("+".into(), vec![Command::VolumeUp(1)]);
kb.insert("]".into(), Command::VolumeUp(5)); kb.insert("]".into(), vec![Command::VolumeUp(5)]);
kb.insert("-".into(), Command::VolumeDown(1)); kb.insert("-".into(), vec![Command::VolumeDown(1)]);
kb.insert("[".into(), Command::VolumeDown(5)); kb.insert("[".into(), vec![Command::VolumeDown(5)]);
kb.insert("r".into(), Command::Repeat(None)); kb.insert("r".into(), vec![Command::Repeat(None)]);
kb.insert("z".into(), Command::Shuffle(None)); kb.insert("z".into(), vec![Command::Shuffle(None)]);
kb.insert("x".into(), Command::Share(TargetMode::Selected)); kb.insert("x".into(), vec![Command::Share(TargetMode::Selected)]);
kb.insert("Shift+x".into(), Command::Share(TargetMode::Current)); kb.insert("Shift+x".into(), vec![Command::Share(TargetMode::Current)]);
kb.insert("F1".into(), Command::Focus("queue".into())); kb.insert("F1".into(), vec![Command::Focus("queue".into())]);
kb.insert("F2".into(), Command::Focus("search".into())); kb.insert("F2".into(), vec![Command::Focus("search".into())]);
kb.insert("F3".into(), Command::Focus("library".into())); kb.insert("F3".into(), vec![Command::Focus("library".into())]);
#[cfg(feature = "cover")] #[cfg(feature = "cover")]
kb.insert("F8".into(), Command::Focus("cover".into())); kb.insert("F8".into(), vec![Command::Focus("cover".into())]);
kb.insert("?".into(), Command::Help); kb.insert("?".into(), vec![Command::Help]);
kb.insert("Backspace".into(), Command::Back); kb.insert("Backspace".into(), vec![Command::Back]);
kb.insert("o".into(), Command::Open(TargetMode::Selected)); kb.insert("o".into(), vec![Command::Open(TargetMode::Selected)]);
kb.insert("Shift+o".into(), Command::Open(TargetMode::Current)); kb.insert("Shift+o".into(), vec![Command::Open(TargetMode::Current)]);
kb.insert("a".into(), Command::Goto(GotoMode::Album)); kb.insert("a".into(), vec![Command::Goto(GotoMode::Album)]);
kb.insert("A".into(), Command::Goto(GotoMode::Artist)); kb.insert("A".into(), vec![Command::Goto(GotoMode::Artist)]);
kb.insert( kb.insert(
"m".into(), "m".into(),
Command::ShowRecommendations(TargetMode::Selected), vec![Command::ShowRecommendations(TargetMode::Selected)],
); );
kb.insert( kb.insert(
"M".into(), "M".into(),
Command::ShowRecommendations(TargetMode::Current), vec![Command::ShowRecommendations(TargetMode::Current)],
); );
kb.insert("Up".into(), Command::Move(MoveMode::Up, Default::default())); kb.insert(
"Up".into(),
vec![Command::Move(MoveMode::Up, Default::default())],
);
kb.insert( kb.insert(
"p".into(), "p".into(),
Command::Move(MoveMode::Playing, Default::default()), vec![Command::Move(MoveMode::Playing, Default::default())],
); );
kb.insert( kb.insert(
"Down".into(), "Down".into(),
Command::Move(MoveMode::Down, Default::default()), vec![Command::Move(MoveMode::Down, Default::default())],
); );
kb.insert( kb.insert(
"Left".into(), "Left".into(),
Command::Move(MoveMode::Left, Default::default()), vec![Command::Move(MoveMode::Left, Default::default())],
); );
kb.insert( kb.insert(
"Right".into(), "Right".into(),
Command::Move(MoveMode::Right, Default::default()), vec![Command::Move(MoveMode::Right, Default::default())],
); );
kb.insert( kb.insert(
"PageUp".into(), "PageUp".into(),
Command::Move(MoveMode::Up, MoveAmount::Integer(5)), vec![Command::Move(MoveMode::Up, MoveAmount::Integer(5))],
); );
kb.insert( kb.insert(
"PageDown".into(), "PageDown".into(),
Command::Move(MoveMode::Down, MoveAmount::Integer(5)), vec![Command::Move(MoveMode::Down, MoveAmount::Integer(5))],
); );
kb.insert( kb.insert(
"Home".into(), "Home".into(),
Command::Move(MoveMode::Up, MoveAmount::Extreme), vec![Command::Move(MoveMode::Up, MoveAmount::Extreme)],
); );
kb.insert( kb.insert(
"End".into(), "End".into(),
Command::Move(MoveMode::Down, MoveAmount::Extreme), vec![Command::Move(MoveMode::Down, MoveAmount::Extreme)],
);
kb.insert(
"k".into(),
vec![Command::Move(MoveMode::Up, Default::default())],
); );
kb.insert("k".into(), Command::Move(MoveMode::Up, Default::default()));
kb.insert( kb.insert(
"j".into(), "j".into(),
Command::Move(MoveMode::Down, Default::default()), vec![Command::Move(MoveMode::Down, Default::default())],
); );
kb.insert( kb.insert(
"h".into(), "h".into(),
Command::Move(MoveMode::Left, Default::default()), vec![Command::Move(MoveMode::Left, Default::default())],
); );
kb.insert( kb.insert(
"l".into(), "l".into(),
Command::Move(MoveMode::Right, Default::default()), vec![Command::Move(MoveMode::Right, Default::default())],
); );
kb.insert( kb.insert(
"Ctrl+p".into(), "Ctrl+p".into(),
Command::Move(MoveMode::Up, Default::default()), vec![Command::Move(MoveMode::Up, Default::default())],
); );
kb.insert( kb.insert(
"Ctrl+n".into(), "Ctrl+n".into(),
Command::Move(MoveMode::Down, Default::default()), vec![Command::Move(MoveMode::Down, Default::default())],
); );
kb.insert( kb.insert(
"Ctrl+a".into(), "Ctrl+a".into(),
Command::Move(MoveMode::Left, Default::default()), vec![Command::Move(MoveMode::Left, Default::default())],
); );
kb.insert( kb.insert(
"Ctrl+e".into(), "Ctrl+e".into(),
Command::Move(MoveMode::Right, Default::default()), vec![Command::Move(MoveMode::Right, Default::default())],
); );
kb.insert("Shift+Up".into(), Command::Shift(ShiftMode::Up, None)); kb.insert("Shift+Up".into(), vec![Command::Shift(ShiftMode::Up, None)]);
kb.insert("Shift+Down".into(), Command::Shift(ShiftMode::Down, None)); kb.insert(
kb.insert("Ctrl+v".into(), Command::Insert(None)); "Shift+Down".into(),
vec![Command::Shift(ShiftMode::Down, None)],
);
kb.insert("Ctrl+v".into(), vec![Command::Insert(None)]);
kb kb
} }

View File

@@ -288,13 +288,15 @@ async fn main() -> Result<(), String> {
} }
} else { } else {
let parsed = command::parse(cmd_without_prefix); let parsed = command::parse(cmd_without_prefix);
if let Some(parsed) = parsed { if let Some(commands) = parsed {
if let Some(data) = s.user_data::<UserData>().cloned() { if let Some(data) = s.user_data::<UserData>().cloned() {
data.cmd.handle(s, parsed) for cmd in commands {
data.cmd.handle(s, cmd);
}
} }
} else { } else {
let mut main = s.find_name::<ui::layout::Layout>("main").unwrap(); let mut main = s.find_name::<ui::layout::Layout>("main").unwrap();
let err_msg = format!("Unknown command: \"{}\"", cmd_without_prefix); let err_msg = format!("Failed to parse command(s): \"{}\"", cmd_without_prefix);
main.set_result(Err(err_msg)); main.set_result(Err(err_msg));
} }
} }

View File

@@ -17,7 +17,7 @@ pub struct HelpView {
} }
impl HelpView { impl HelpView {
pub fn new(bindings: HashMap<String, Command>) -> HelpView { pub fn new(bindings: HashMap<String, Vec<Command>>) -> HelpView {
let mut text = StyledString::styled("Keybindings\n\n", Effect::Bold); let mut text = StyledString::styled("Keybindings\n\n", Effect::Bold);
let note = format!( let note = format!(
@@ -30,8 +30,16 @@ impl HelpView {
keys.sort(); keys.sort();
for key in keys { for key in keys {
let command = &bindings[key]; let commands = &bindings[key];
let binding = format!("{} -> {}\n", key, command); let binding = format!(
"{} -> {}\n",
key,
commands
.iter()
.map(|c| c.to_string())
.collect::<Vec<_>>()
.join("; ")
);
text.append(binding); text.append(binding);
} }