Merge pull request #1210 from arnav-hiwarkar/feat/copy-presets-folder-path

This commit is contained in:
Brandon Thomas committed 2026-10-13 04:35:21 -04:00
commit acb77a8c85
6 files changed
+150 -1

No files matched your search

+1
View File
@@ -37,6 +37,7 @@ pub fn specs() -> Vec<CommandSpec> {
fe!("app.keyboardShortcuts", "Keyboard Shortcuts", ["Edit"], Some("Cmd+Alt+'"), "{}", always),
fe!("app.templates", "Templates", [], None, "{kind: renderSettings|outputModule}", always),
fe!("app.find", "Find", ["File"], Some("Cmd+F"), "{query?}", always),
fe!("effects.copyPresetsFolderPath", "Copy Presets Folder Path", [], None, "{}", always),
// Preview.
fe!(
"playback.toggle",
+6
View File
@@ -227,6 +227,7 @@ const JAPANESE: &[(&str, &str)] = &[
("Edit in the Timeline", "タイムラインで編集"),
("EffectCraft didn't quit normally last time.", "EffectCraftは前回正常に終了しませんでした。"),
("Effects & Presets options", "エフェクト&プリセットのオプション"),
("Copy Presets Folder Path", "プリセットフォルダーのパスをコピー"),
("Effects: {}", "エフェクト:{}"),
("Empty folder", "空のフォルダー"),
("Enable Depth of Field", "被写界深度を有効化"),
@@ -770,6 +771,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
("Edit in the Timeline", "在时间轴中编辑"),
("EffectCraft didn't quit normally last time.", "EffectCraft 上次未正常退出。"),
("Effects & Presets options", "效果和预设选项"),
("Copy Presets Folder Path", "复制预设文件夹路径"),
("Effects: {}", "效果:{}"),
("Empty folder", "空文件夹"),
("Enable Depth of Field", "启用景深"),
@@ -1501,6 +1503,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
("Edit in the Timeline", "在時間軸中編輯"),
("EffectCraft didn't quit normally last time.", "EffectCraft 上次未正常結束。"),
("Effects & Presets options", "效果和預設集選項"),
("Copy Presets Folder Path", "複製預設集資料夾路徑"),
("Effects: {}", "效果:{}"),
("Empty folder", "空資料夾"),
("Enable Depth of Field", "啟用景深"),
@@ -2235,6 +2238,7 @@ const UKRAINIAN: &[(&str, &str)] = &[
("Edit in the Timeline", "Редагувати на шкалі часу"),
("EffectCraft didn't quit normally last time.", "Минулого разу EffectCraft завершив роботу некоректно."),
("Effects & Presets options", "Параметри ефектів і наборів"),
("Copy Presets Folder Path", "Копіювати шлях до папки наборів"),
("Effects: {}", "Ефекти: {}"),
("Empty folder", "Порожня тека"),
("Enable Depth of Field", "Увімкнути глибину різкості"),
@@ -2786,6 +2790,7 @@ const PORTUGUESE: &[(&str, &str)] = &[
("Edit in the Timeline", "Editar na linha do tempo"),
("EffectCraft didn't quit normally last time.", "O EffectCraft não foi encerrado normalmente da última vez."),
("Effects & Presets options", "Opções de Efeitos e predefinições"),
("Copy Presets Folder Path", "Copiar caminho da pasta de predefinições"),
("Effects: {}", "Efeitos: {}"),
("Empty folder", "Pasta vazia"),
("Enable Depth of Field", "Ativar profundidade de campo"),
@@ -3235,6 +3240,7 @@ const CZECH: &[(&str, &str)] = &[
("Edit in the Timeline", "Upravit na časové ose"),
("EffectCraft didn't quit normally last time.", "EffectCraft nebyl při posledním použití ukončen běžným způsobem."),
("Effects & Presets options", "Možnosti efektů a přednastavení"),
("Copy Presets Folder Path", "Kopírovat cestu ke složce přednastavení"),
("Effects: {}", "Efekty: {}"),
("Empty folder", "Prázdná složka"),
("Enable Depth of Field", "Povolit hloubku ostrosti"),
+13
View File
@@ -291,6 +291,9 @@ fn only_comp(p: &Value) -> Option<effectcraft_engine::project::ItemId> {
/// Execute a UI or engine command by id.
pub fn invoke(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, params: Value) -> Result<Value, String> {
if id == "effects.copyPresetsFolderPath" {
return frontend(app, ctx, id, params);
}
// Layer ▸ New ▸ Text (menu, shortcut, context menus) puts an insertion point in the centre of
// the comp, as After Effects does, so typing goes into Source Text rather than running
// shortcuts (#866). `engine.execute` keeps the headless default.
@@ -877,6 +880,16 @@ pub fn frontend(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, p: Valu
let now = ctx.input(|i| i.time);
let v = &mut app.ui.viewer;
Ok(match id {
"effects.copyPresetsFolderPath" => {
if cfg!(target_arch = "wasm32") {
return Err("The web app has no local presets folder".into());
}
let path = crate::panels::effects_presets::presets_dir().filter(|path| !path.as_os_str().is_empty()).ok_or("No presets folder is configured")?;
let path = std::path::absolute(path).map_err(|e| format!("Cannot resolve the presets folder path: {e}"))?;
let path = path.to_str().ok_or("The presets folder path is not valid UTF-8")?;
ctx.copy_text(path.to_string());
json!({"path": path})
}
"app.about" => {
app.dialog = Some(crate::Dialog::About);
Value::Null
+31 -1
View File
@@ -300,9 +300,34 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
"-".into(),
" Refresh List".into(),
" Clear Recently Used".into(),
"-".into(),
format!(" {}", tr("Copy Presets Folder Path")),
];
let mut force_scan = false;
if let Some(i) = widgets::popup_menu(ui, mpop, mr.left_bottom() - vec2(220.0, 0.0), &menu, None) {
let separators = menu.iter().filter(|label| *label == "-").count();
let choice = if widgets::popup_is_open(ui, mpop) {
widgets::popup_list(ui, mpop, mr.left_bottom() - vec2(220.0, 0.0), Rect::NOTHING, menu.len().saturating_sub(separators), separators, |ui| {
ui.set_min_width(160.0);
let mut chosen = None;
for (i, label) in menu.iter().enumerate() {
if label == "-" {
ui.separator();
} else {
let resp = ui.selectable_label(false, label);
if i == 12 {
app.auto.add("effects.copyPresetsFolderPath", resp.rect, "Copy Presets Folder Path");
}
if resp.clicked() {
chosen = Some(i);
}
}
}
chosen
})
} else {
None
};
if let Some(i) = choice {
let v = &mut app.ui.effects_view;
match i {
0 => v.depth = "all".into(),
@@ -313,6 +338,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
7 => v.alphabetical = true,
9 => force_scan = true,
10 => app.ui.effects_recent.clear(),
12 => {
if let Err(e) = crate::menus::invoke(app, &ctx, "effects.copyPresetsFolderPath", json!({})) {
app.ui.status = e;
}
}
_ => {}
}
}
+90
View File
@@ -0,0 +1,90 @@
//! Effects & Presets: copy the configured folder path through the contents menu.
use effectcraft_engine::Session;
use effectcraft_ui_egui::{ControlRequest, EffectcraftApp, control, dock::PanelKind};
use egui::{Event, Modifiers, OutputCommand, PointerButton, pos2};
use egui_kittest::Harness;
use serde_json::json;
fn expected_folder() -> String {
let path = std::env::var("EC_PRESETS_DIR").unwrap_or_else(|_| {
let home = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")).unwrap();
std::path::Path::new(&home).join("Documents/EffectCraft/Presets").to_str().unwrap().to_string()
});
if path.is_empty() { path } else { std::path::absolute(path).unwrap().to_str().unwrap().to_string() }
}
fn click(h: &mut Harness<'_, EffectcraftApp>, id: &str) -> Vec<String> {
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("missing {id}"));
let pos = pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0);
let mut copied = Vec::new();
for pressed in [true, false] {
h.input_mut().events.push(Event::PointerMoved(pos));
h.input_mut().events.push(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
h.step();
for command in &h.output().platform_output.commands {
if let OutputCommand::CopyText(text) = command {
copied.push(text.clone());
}
}
}
copied
}
#[test]
fn presets_menu_copies_the_configured_folder_without_editing_the_project() {
let expected = expected_folder();
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| {
let mut app = EffectcraftApp::new(Session::default());
app.ui.start_screen = false;
app.show_panel(PanelKind::EffectsPresets);
app
});
h.run_steps(3);
let before = serde_json::to_value(&h.state().session.project).unwrap();
let revision = h.state().session.revision;
assert!(h.state().auto.find("effects.copyPresetsFolderPath").is_none());
click(&mut h, "effects.menu");
h.run_steps(3);
if let Ok(path) = std::env::var("PRESETS_FOLDER_SNAPSHOT") {
h.render().unwrap().save(path).unwrap();
}
assert_eq!(click(&mut h, "effects.copyPresetsFolderPath"), [expected]);
h.run_steps(2);
assert!(h.state().auto.find("effects.copyPresetsFolderPath").is_none());
assert_eq!(serde_json::to_value(&h.state().session.project).unwrap(), before);
assert_eq!(h.state().session.revision, revision);
}
#[test]
fn presets_folder_control_command_returns_the_path_or_a_configuration_error() {
let mut app = EffectcraftApp::new(Session::default());
let ctx = egui::Context::default();
let expected = expected_folder();
let mut reply = None;
let mut output = ctx.run_ui(Default::default(), |ui| {
let (request, _rx) = ControlRequest::new("ui.menu.invoke", json!({"id": "effects.copyPresetsFolderPath"}));
reply = match control::handle(&mut app, ui.ctx(), &request) {
control::Outcome::Done(value) => Some(value),
_ => panic!("expected an immediate reply"),
};
});
output.textures_delta.clear();
let copied: Vec<_> = output
.platform_output
.commands
.iter()
.filter_map(|command| match command {
OutputCommand::CopyText(text) => Some(text.as_str()),
_ => None,
})
.collect();
if expected.is_empty() {
assert_eq!(reply, Some(json!({"ok": false, "error": "No presets folder is configured"})));
assert!(copied.is_empty());
assert_eq!(app.ui.status, "No presets folder is configured");
} else {
assert_eq!(reply, Some(json!({"ok": true, "result": {"path": expected}})));
assert_eq!(copied, [expected.as_str()]);
}
}
+9
View File
@@ -10,6 +10,15 @@ user can do, an agent can do too. There are three ways in:
| **CLI** (`effectcraft-cli exec/get/set/...`) | one-shot scripting and CI; JSON output with `--json` | no |
| **Control channel** (`effectcraft --control 9877`) | driving and seeing the live UI; see [control-protocol.md](control-protocol.md) | yes |
## Find the presets folder
The native Effects & Presets panel's options menu includes **Copy Presets Folder Path**. It copies
the folder the panel scans: `EC_PRESETS_DIR` when set, otherwise `~/Documents/EffectCraft/Presets`
using `HOME` or `USERPROFILE`. Relative paths are resolved to an absolute path. The folder may
not exist yet; this action does not create it.
Over the live UI control channel, `ui.menu.invoke {"id":"effects.copyPresetsFolderPath"}` copies
the same path and returns `{path}`. The web app reports that it has no local presets folder.
## MCP setup
Build once with `cargo build --release -p effectcraft-cli`, then register the server. For Claude