Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(not(test))]
|
||||
use std::env::var_os;
|
||||
use vec1::{vec1, Vec1};
|
||||
use warpui::keymap::Keystroke;
|
||||
#[cfg(not(test))]
|
||||
use warpui::keymap::Trigger;
|
||||
use warpui::AppContext;
|
||||
|
||||
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 {
|
||||
warp_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;
|
||||
Reference in New Issue
Block a user