220 lines
7.5 KiB
Rust
220 lines
7.5 KiB
Rust
use galaxyui::keymap::Keystroke;
|
|
#[cfg(not(test))]
|
|
use galaxyui::keymap::Trigger;
|
|
use galaxyui::AppContext;
|
|
use itertools::Itertools;
|
|
use serde::{Deserialize, Serialize};
|
|
#[cfg(not(test))]
|
|
use std::env::var_os;
|
|
use vec1::{vec1, Vec1};
|
|
|
|
use anyhow::Context;
|
|
|
|
/// Environment variable to disable saving keybindings to file (used in integration tests)
|
|
pub const DISABLE_SAVE_ENV_VAR: &str = "WARP_TEST_DISABLE_KEYBINDING_SAVE";
|
|
const REMOVED_KEYBINDING_SERIALIZATION: &str = "none";
|
|
|
|
#[derive(PartialEq, Debug)]
|
|
/// A type to encapsulate the valid states of a keybinding
|
|
/// provided by a user in their keybindings.yaml file
|
|
pub enum UserDefinedKeybinding {
|
|
/// Keybinding we can normalize/parse and will be recognized
|
|
Keystrokes(Vec1<Keystroke>),
|
|
/// User chose to remove the keybinding for an action
|
|
Removed,
|
|
}
|
|
|
|
impl UserDefinedKeybinding {
|
|
pub fn keystroke(value: Keystroke) -> Self {
|
|
UserDefinedKeybinding::Keystrokes(vec1![value])
|
|
}
|
|
}
|
|
|
|
#[cfg(not(test))]
|
|
const KEYBINDINGS_FILE_NAME: &str = "keybindings.yaml";
|
|
|
|
/// Load all stored custom keybindings into the UI framework so that they are used
|
|
#[cfg(not(test))]
|
|
pub fn load_custom_keybindings(app: &mut AppContext) {
|
|
if let Some(keybindings) = read_custom_keybindings() {
|
|
for (name, trigger) in keybindings.0 {
|
|
let keybinding_type = UserDefinedKeybinding::try_from(trigger.clone());
|
|
|
|
match keybinding_type {
|
|
Ok(UserDefinedKeybinding::Removed) => {
|
|
app.set_custom_trigger(name, Trigger::Empty);
|
|
}
|
|
Ok(UserDefinedKeybinding::Keystrokes(keystrokes)) => {
|
|
app.set_custom_trigger(name, Trigger::Keystrokes(keystrokes.to_vec()));
|
|
}
|
|
Err(e) => {
|
|
log::warn!(
|
|
"Tried to load an unparsable keybinding of {trigger:?} for action: {name}. error: {e}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Write a new custom keybinding to disk
|
|
/// using the name of the editable binding and the new keystrokes
|
|
/// if keystrokes is UserDefinedKeybinding::Removed
|
|
/// we write a special value to disk to save that state
|
|
#[cfg(not(test))]
|
|
pub fn write_custom_keybinding(name: String, keybinding: UserDefinedKeybinding) {
|
|
// In tests, we don't want to write the actual keybindings file, since that could clobber the
|
|
// user's current settings, so we no-op
|
|
if var_os(DISABLE_SAVE_ENV_VAR).is_some() {
|
|
return;
|
|
}
|
|
|
|
let mut map = read_custom_keybindings().unwrap_or_default();
|
|
|
|
map.0.insert(name, keybinding.into());
|
|
save_custom_keybindings(map);
|
|
}
|
|
|
|
/// Remove a custom keybinding from disk.
|
|
#[cfg(not(test))]
|
|
pub fn remove_custom_keybinding<N>(name: N)
|
|
where
|
|
N: AsRef<str>,
|
|
{
|
|
// In tests, we don't want to write the actual keybindings file, since that could clobber the
|
|
// users current settings, so we no-op
|
|
if var_os(DISABLE_SAVE_ENV_VAR).is_some() {
|
|
return;
|
|
}
|
|
|
|
let mut map = read_custom_keybindings().unwrap_or_default();
|
|
|
|
map.0.remove(name.as_ref());
|
|
save_custom_keybindings(map);
|
|
}
|
|
|
|
#[cfg(not(test))]
|
|
pub fn keybinding_file_path() -> std::path::PathBuf {
|
|
galaxy_core::paths::config_local_dir().join(KEYBINDINGS_FILE_NAME)
|
|
}
|
|
|
|
/// Save the custom keybindings map to disk.
|
|
#[cfg(not(test))]
|
|
// Allow unused variables when no local filesystem exists as the arg is unused.
|
|
#[cfg_attr(not(feature = "local_fs"), allow(unused_variables))]
|
|
fn save_custom_keybindings(map: CustomKeybindings) {
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(feature = "local_fs")] {
|
|
let file = match crate::util::file::create_file(keybinding_file_path()) {
|
|
Ok(f) => f,
|
|
Err(e) => {
|
|
log::warn!("Unable to open file for storing custom keybindings: {e}");
|
|
return;
|
|
}
|
|
};
|
|
let writer = std::io::BufWriter::new(file);
|
|
|
|
if let Err(e) = serde_yaml::to_writer(writer, &map) {
|
|
log::warn!("Unable to serialize custom keybindings to file: {e}");
|
|
}
|
|
} else {
|
|
log::warn!("TODO(wasm): need to implement keybindings support");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Read the stored custom keybindings from disk into a map of Editable Binding Name -> Trigger
|
|
///
|
|
/// Returns `None` if the file can't be read or the deserialization fails
|
|
#[cfg(not(test))]
|
|
fn read_custom_keybindings() -> Option<CustomKeybindings> {
|
|
let file = std::fs::File::open(keybinding_file_path()).ok()?;
|
|
let reader = std::io::BufReader::new(file);
|
|
|
|
match serde_yaml::from_reader(reader) {
|
|
Ok(map) => Some(map),
|
|
Err(e) => {
|
|
log::warn!("Unable to deserialize stored keybindings: {e}");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
// For tests, we don't want to read or write from the filesystem.
|
|
//
|
|
// Unit tests are run with #[cfg(test)] enabled, so we can define custom no-op implementations
|
|
#[cfg(test)]
|
|
pub fn load_custom_keybindings(_: &mut AppContext) {}
|
|
#[cfg(test)]
|
|
pub fn write_custom_keybinding(_: String, _: UserDefinedKeybinding) {}
|
|
#[cfg(test)]
|
|
pub fn remove_custom_keybinding<N>(_: N)
|
|
where
|
|
N: AsRef<str>,
|
|
{
|
|
}
|
|
|
|
/// Struct that represents the full custom keybindings file for (de-)serialization
|
|
///
|
|
/// The file format is a top-level YAML map of (Editable Binding Name) -> Keybinding
|
|
/// Since many of the editable bindings have a `:` character in their name, the name will need to
|
|
/// be quoted in most cases.
|
|
/// The format of the keybinding is the normalized version that we use internally, with multiple
|
|
/// keystrokes separated by whitespace, if necessary.
|
|
///
|
|
/// For example:
|
|
/// ---
|
|
/// "editor:delete_all_left": cmd-shift-A
|
|
/// "editor:delete_all_right": cmd-shift-D escape
|
|
#[derive(Serialize, Deserialize, Default)]
|
|
#[cfg(not(test))]
|
|
struct CustomKeybindings(std::collections::HashMap<String, PersistedTrigger>);
|
|
|
|
/// The normalized version of a keystroke or series of keystrokes that is written into the
|
|
/// keybindings file. If there are multiple keystrokes, each is separated by a space
|
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
|
struct PersistedTrigger(String);
|
|
|
|
impl From<UserDefinedKeybinding> for PersistedTrigger {
|
|
fn from(keybinding: UserDefinedKeybinding) -> Self {
|
|
match keybinding {
|
|
UserDefinedKeybinding::Keystrokes(keystrokes) => {
|
|
PersistedTrigger(keystrokes.iter().map(Keystroke::normalized).join(" "))
|
|
}
|
|
UserDefinedKeybinding::Removed => {
|
|
PersistedTrigger(REMOVED_KEYBINDING_SERIALIZATION.to_string())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TryFrom<PersistedTrigger> for UserDefinedKeybinding {
|
|
type Error = anyhow::Error;
|
|
fn try_from(trigger: PersistedTrigger) -> anyhow::Result<Self> {
|
|
if trigger.0 == REMOVED_KEYBINDING_SERIALIZATION {
|
|
return Ok(UserDefinedKeybinding::Removed);
|
|
}
|
|
|
|
let mut keystrokes: Vec<Keystroke> = Vec::new();
|
|
|
|
for keystroke in trigger.0.split_whitespace() {
|
|
let parsed_keystroke: Keystroke = Keystroke::parse(keystroke).context(format!(
|
|
"Failed to parse keystroke \"{}\" in trigger \"{}\"",
|
|
keystroke, trigger.0,
|
|
))?;
|
|
keystrokes.push(parsed_keystroke);
|
|
}
|
|
|
|
let parsed_keystrokes: Vec1<Keystroke> = Vec1::try_from(keystrokes).context(format!(
|
|
"No valid keystrokes were found in trigger: {}",
|
|
trigger.0
|
|
))?;
|
|
|
|
Ok(UserDefinedKeybinding::Keystrokes(parsed_keystrokes))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "keyboard_test.rs"]
|
|
mod tests;
|