279 lines
12 KiB
Rust
279 lines
12 KiB
Rust
//! Settings (`prefs.*`), keyboard shortcut presets (`shortcuts.*`), File ▸ Open Recent and
|
|
//! auto-save commands.
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use super::{CommandSpec, always, bad, str_p};
|
|
use crate::shortcuts::{DEFAULT_PRESET, binding_key, normalize};
|
|
use crate::{EngineError, Result, Session, cmd, query};
|
|
|
|
// ---------------------------------------------------------------- prefs
|
|
|
|
fn prefs_get(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let key = str_p(p, "key").unwrap_or("");
|
|
s.prefs.get(key).ok_or_else(|| bad("prefs.get", format!("unknown setting `{key}`")))
|
|
}
|
|
|
|
fn prefs_set(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let mut pairs: Vec<(String, Value)> = vec![];
|
|
if let Some(Value::Object(m)) = p.get("values") {
|
|
pairs.extend(m.iter().map(|(k, v)| (k.clone(), v.clone())));
|
|
}
|
|
if let Some(k) = str_p(p, "key") {
|
|
let v = p.get("value").cloned().ok_or_else(|| bad("prefs.set", "missing `value`"))?;
|
|
pairs.push((k.to_string(), v));
|
|
}
|
|
if pairs.is_empty() {
|
|
return Err(bad("prefs.set", "give `key` and `value` (or `values: {key: value}`)"));
|
|
}
|
|
let mut next = s.prefs.clone();
|
|
for (k, v) in &pairs {
|
|
next.set(k, v.clone()).map_err(|e| bad("prefs.set", e))?;
|
|
}
|
|
s.prefs = next;
|
|
s.prefs_changed();
|
|
s.save_prefs();
|
|
let out: serde_json::Map<String, Value> = pairs.iter().map(|(k, _)| (k.clone(), s.prefs.get(k).unwrap_or(Value::Null))).collect();
|
|
Ok(Value::Object(out))
|
|
}
|
|
|
|
fn prefs_reset(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let page = str_p(p, "page");
|
|
s.prefs.reset(page).map_err(|e| bad("prefs.reset", e))?;
|
|
s.prefs_changed();
|
|
s.save_prefs();
|
|
s.toast(match page {
|
|
Some(pg) => format!("Reset {pg} settings"),
|
|
None => "Reset all settings".into(),
|
|
});
|
|
Ok(json!({"reset": page.unwrap_or("all")}))
|
|
}
|
|
|
|
fn prefs_open(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let page = str_p(p, "page").unwrap_or("general");
|
|
let id = crate::prefs::page_id(page).ok_or_else(|| bad("prefs.open", format!("unknown settings page `{page}`")))?;
|
|
super::frontend(s, "app.settings", &json!({"page": id}))
|
|
}
|
|
|
|
fn prefs_pages(_: &mut Session, _: &Value) -> Result<Value> {
|
|
Ok(crate::prefs::pages_json())
|
|
}
|
|
|
|
// ---------------------------------------------------------------- shortcuts
|
|
|
|
/// The bindable named by `command` (+ `params`), or by a binding key (`app.settings {...}`).
|
|
fn bindable(s: &Session, p: &Value, cmd: &str) -> Result<crate::shortcuts::Bindable> {
|
|
let c = str_p(p, "command").ok_or_else(|| bad(cmd, "missing `command`"))?;
|
|
let params = p.get("params").cloned().unwrap_or(Value::Null);
|
|
let t = s.shortcuts();
|
|
let key = binding_key(c, ¶ms);
|
|
t.find(&key).or_else(|| t.find(c)).cloned().ok_or_else(|| bad(cmd, format!("`{key}` can't have a shortcut (see shortcuts.list)")))
|
|
}
|
|
|
|
fn keys_param(p: &Value, cmd: &str) -> Result<Vec<String>> {
|
|
Ok(match p.get("keys") {
|
|
None | Some(Value::Null) => vec![],
|
|
Some(Value::String(k)) if k.trim().is_empty() => vec![],
|
|
Some(Value::String(k)) => vec![k.clone()],
|
|
Some(Value::Array(a)) => a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect(),
|
|
Some(v) => return Err(bad(cmd, format!("`keys` must be a string or a list, not {v}"))),
|
|
})
|
|
}
|
|
|
|
fn sc_list(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let q = str_p(p, "query").unwrap_or("").to_lowercase();
|
|
let assigned = p.get("assigned").and_then(Value::as_bool).unwrap_or(false);
|
|
let keys_q = str_p(p, "keys").and_then(normalize);
|
|
let t = s.shortcuts();
|
|
let rows: Vec<Value> = t
|
|
.bindables
|
|
.iter()
|
|
.filter_map(|b| {
|
|
let keys = t.keys.get(&b.key).cloned().unwrap_or_default();
|
|
if assigned && keys.is_empty() {
|
|
return None;
|
|
}
|
|
if let Some(k) = &keys_q
|
|
&& !keys.contains(k)
|
|
{
|
|
return None;
|
|
}
|
|
let hay = format!("{} {} {} {}", b.label, b.command, b.path.join(" "), keys.join(" ")).to_lowercase();
|
|
if !q.split_whitespace().all(|w| hay.contains(w)) {
|
|
return None;
|
|
}
|
|
let mut o = serde_json::to_value(b).unwrap_or_default();
|
|
o["keys"] = json!(keys);
|
|
Some(o)
|
|
})
|
|
.collect();
|
|
Ok(json!({"preset": s.keymaps.active, "presets": s.keymaps.names(), "count": rows.len(), "commands": rows}))
|
|
}
|
|
|
|
fn sc_set(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let b = bindable(s, p, "shortcuts.set")?;
|
|
let keys = keys_param(p, "shortcuts.set")?;
|
|
let conflicts: Vec<_> = keys.iter().flat_map(|k| s.shortcuts().conflicts(&b, k)).collect();
|
|
let created = s.keymaps.set(&b, &keys).map_err(|e| bad("shortcuts.set", e))?;
|
|
s.shortcuts_changed();
|
|
let now = s.shortcuts().keys.get(&b.key).cloned().unwrap_or_default();
|
|
if let Some(n) = &created {
|
|
s.toast(format!("The default shortcuts can't be changed: created the preset \"{n}\""));
|
|
}
|
|
if !conflicts.is_empty() {
|
|
let names: Vec<String> = conflicts.iter().map(|c| format!("{} ({})", c.label, c.keys)).collect();
|
|
s.toast(format!("Shortcut conflict: also used by {}", names.join(", ")));
|
|
}
|
|
Ok(json!({"command": b.key, "keys": now, "preset": s.keymaps.active, "created": created, "conflicts": conflicts}))
|
|
}
|
|
|
|
fn sc_reset(s: &mut Session, p: &Value) -> Result<Value> {
|
|
if let Some(c) = str_p(p, "command") {
|
|
let b = bindable(s, p, "shortcuts.reset")?;
|
|
let defaults = b.defaults.clone();
|
|
s.keymaps.set(&b, &defaults).map_err(|e| bad("shortcuts.reset", e))?;
|
|
s.shortcuts_changed();
|
|
return Ok(json!({"command": c, "keys": defaults}));
|
|
}
|
|
s.keymaps.reset_active();
|
|
s.shortcuts_changed();
|
|
Ok(json!({"preset": s.keymaps.active}))
|
|
}
|
|
|
|
fn sc_preset(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let op = str_p(p, "op").unwrap_or("select");
|
|
let name = str_p(p, "name");
|
|
let e = |m: String| bad("shortcuts.preset", m);
|
|
let k = &mut s.keymaps;
|
|
match op {
|
|
"select" => k.select(name.unwrap_or(DEFAULT_PRESET)).map_err(e)?,
|
|
"new" => {
|
|
k.duplicate(DEFAULT_PRESET, Some(name.unwrap_or("Custom"))).map_err(e)?;
|
|
}
|
|
"duplicate" => {
|
|
let from = str_p(p, "from").map(str::to_string).unwrap_or_else(|| k.active.clone());
|
|
k.duplicate(&from, name).map_err(e)?;
|
|
}
|
|
"delete" => {
|
|
let n = name.map(str::to_string).unwrap_or_else(|| k.active.clone());
|
|
k.delete(&n).map_err(e)?;
|
|
}
|
|
"rename" => {
|
|
let to = str_p(p, "newName").ok_or_else(|| bad("shortcuts.preset", "missing `newName`"))?;
|
|
let from = name.map(str::to_string).unwrap_or_else(|| k.active.clone());
|
|
k.rename(&from, to).map_err(e)?;
|
|
}
|
|
_ => return Err(bad("shortcuts.preset", "op must be select, new, duplicate, delete or rename")),
|
|
}
|
|
s.shortcuts_changed();
|
|
Ok(json!({"preset": s.keymaps.active, "presets": s.keymaps.names()}))
|
|
}
|
|
|
|
fn sc_export(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let doc = s.keymaps.export(str_p(p, "preset")).map_err(|e| bad("shortcuts.export", e))?;
|
|
if let Some(path) = str_p(p, "path") {
|
|
let text = serde_json::to_string_pretty(&doc).unwrap_or_default();
|
|
s.services.write_file(path, text.as_bytes()).map_err(|e| EngineError::Other(format!("cannot write {path}: {e}")))?;
|
|
return Ok(json!({"path": path, "preset": doc["name"]}));
|
|
}
|
|
Ok(doc)
|
|
}
|
|
|
|
fn sc_import(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let doc = match (str_p(p, "path"), p.get("preset")) {
|
|
(Some(path), _) => {
|
|
let bytes = s.services.read_file(path).map_err(|e| EngineError::Other(format!("cannot read {path}: {e}")))?;
|
|
serde_json::from_slice(&bytes).map_err(|e| bad("shortcuts.import", format!("{path}: {e}")))?
|
|
}
|
|
(None, Some(v)) if v.is_object() => v.clone(),
|
|
_ => return Err(bad("shortcuts.import", "give `path` or `preset` (an exported document)")),
|
|
};
|
|
let name = s.keymaps.import(&doc, str_p(p, "name")).map_err(|e| bad("shortcuts.import", e))?;
|
|
s.shortcuts_changed();
|
|
Ok(json!({"preset": name}))
|
|
}
|
|
|
|
fn sc_conflicts(s: &mut Session, _: &Value) -> Result<Value> {
|
|
let c: Vec<Value> =
|
|
s.shortcuts().all_conflicts().into_iter().map(|(a, c)| json!({"command": a, "keys": c.keys, "with": c.key, "scope": c.scope})).collect();
|
|
Ok(json!(c))
|
|
}
|
|
|
|
// ---------------------------------------------------------------- recent / auto-save
|
|
|
|
fn open_recent(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let path = match (str_p(p, "path"), p.get("index").and_then(Value::as_u64)) {
|
|
(Some(path), _) => path.to_string(),
|
|
(None, Some(i)) => s.prefs.recent_projects.get(i as usize).cloned().ok_or_else(|| bad("file.openRecent", format!("no recent project #{i}")))?,
|
|
_ => s.prefs.recent_projects.first().cloned().ok_or_else(|| bad("file.openRecent", "no recent projects"))?,
|
|
};
|
|
match super::file::open(s, &json!({"path": path})) {
|
|
Ok(v) => Ok(v),
|
|
Err(e) => {
|
|
// A project that's gone drops out of the list.
|
|
if !std::path::Path::new(&path).exists() {
|
|
s.prefs.recent_projects.retain(|x| *x != path);
|
|
s.save_prefs();
|
|
}
|
|
Err(e)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn clear_recent(s: &mut Session, _: &Value) -> Result<Value> {
|
|
s.prefs.recent_projects.clear();
|
|
s.save_prefs();
|
|
s.prefs_revision += 1;
|
|
Ok(Value::Null)
|
|
}
|
|
|
|
fn has_recent(s: &Session) -> std::result::Result<(), String> {
|
|
if s.prefs.recent_projects.is_empty() { Err("no recent projects".into()) } else { Ok(()) }
|
|
}
|
|
|
|
fn auto_save(s: &mut Session, _: &Value) -> Result<Value> {
|
|
let path = s.autosave_now()?;
|
|
s.toast(format!("Auto-saved to {path}"));
|
|
Ok(json!({"path": path}))
|
|
}
|
|
|
|
fn recovery_info(s: &mut Session, _: &Value) -> Result<Value> {
|
|
let root = s.default_autosave_root();
|
|
let latest = crate::autosave::latest_in(s.file_ops(), &s.prefs, s.path.as_deref(), root.as_deref()).map(|p| p.to_string_lossy().to_string());
|
|
Ok(json!({
|
|
"lastAutoSave": s.autosave.last_path,
|
|
"latestOnDisk": latest,
|
|
"folder": crate::autosave::folder(&s.prefs, s.path.as_deref(), root.as_deref()).map(|p| p.to_string_lossy().to_string()),
|
|
"recentProjects": s.prefs.recent_projects,
|
|
}))
|
|
}
|
|
|
|
pub fn specs() -> Vec<CommandSpec> {
|
|
vec![
|
|
query!("prefs.get", "Get Setting", "{key?: e.g. general.undoLevels (omit for all settings)}", prefs_get),
|
|
cmd!("prefs.set", "Change Setting", [], None, "{key, value, values?: {key: value}}", always, prefs_set),
|
|
cmd!("prefs.reset", "Reset Settings", [], None, "{page?: general|labels|project|…}", always, prefs_reset),
|
|
cmd!(
|
|
"prefs.open",
|
|
"Open Settings",
|
|
[],
|
|
None,
|
|
"{page?: general|startup|project|composition|previews|appearance|grids|labels|type|import|export|audio|disk|memory|video|3d|scripting}",
|
|
always,
|
|
prefs_open
|
|
),
|
|
query!("prefs.pages", "Settings Pages", "{}", prefs_pages),
|
|
query!("shortcuts.list", "List Keyboard Shortcuts", "{query?, keys?, assigned?: bool}", sc_list),
|
|
cmd!("shortcuts.set", "Set Keyboard Shortcut", [], None, "{command, params?, keys: \"Cmd+Shift+K\" | [keys] | null}", always, sc_set),
|
|
cmd!("shortcuts.reset", "Reset Keyboard Shortcuts", [], None, "{command?, params?}", always, sc_reset),
|
|
cmd!("shortcuts.preset", "Keyboard Shortcut Preset", [], None, "{op: select|new|duplicate|delete|rename, name?, from?, newName?}", always, sc_preset),
|
|
cmd!("shortcuts.export", "Export Keyboard Shortcuts", [], None, "{preset?, path?}", always, sc_export),
|
|
cmd!("shortcuts.import", "Import Keyboard Shortcuts", [], None, "{path? | preset?: exported document, name?}", always, sc_import),
|
|
query!("shortcuts.conflicts", "Keyboard Shortcut Conflicts", "{}", sc_conflicts),
|
|
cmd!("file.openRecent", "Open Recent", [], None, "{index? | path?}", has_recent, open_recent),
|
|
cmd!("file.clearRecent", "Clear Recent Projects", ["File", "Open Recent"], None, "{}", has_recent, clear_recent),
|
|
cmd!("file.autoSave", "Auto-Save Now", [], None, "{}", always, auto_save),
|
|
query!("file.recoveryInfo", "Auto-Save Status", "{}", recovery_info),
|
|
]
|
|
}
|