diff --git a/crates/jarvis-app/src/app.rs b/crates/jarvis-app/src/app.rs index 032e0e6..afa4f5d 100644 --- a/crates/jarvis-app/src/app.rs +++ b/crates/jarvis-app/src/app.rs @@ -464,11 +464,6 @@ fn execute_command(text: &str, rt: &tokio::runtime::Runtime) -> bool { info!("Command executed successfully"); // voices::play_ok(); voices::play_random_from(cmd_config.get_sounds(&i18n::get_language()).as_slice()); - - // IMBA-7: if macro recording is active, capture this phrase. - // Skipped for macro-control commands themselves (filter inside record_step). - jarvis_core::macros::record_step(text); - ipc::send(IpcEvent::CommandExecuted { id: cmd_config.id.clone(), success: true, diff --git a/crates/jarvis-app/src/main.rs b/crates/jarvis-app/src/main.rs index fb82dc7..86d831b 100644 --- a/crates/jarvis-app/src/main.rs +++ b/crates/jarvis-app/src/main.rs @@ -29,7 +29,6 @@ mod tray; static SHOULD_STOP: AtomicBool = AtomicBool::new(false); fn main() -> Result<(), String> { - let boot_start = std::time::Instant::now(); load_dotenv_near_exe(); eprintln!("[jarvis-app] step: init_dirs"); @@ -89,11 +88,6 @@ fn main() -> Result<(), String> { jarvis_core::scheduler::start_background(); } - eprintln!("[jarvis-app] step: macros::init"); - if let Err(e) = jarvis_core::macros::init() { - warn!("Macros init failed: {}", e); - } - eprintln!("[jarvis-app] step: recorder::init"); if recorder::init().is_err() { notify_mic_problem(); @@ -159,18 +153,9 @@ fn main() -> Result<(), String> { info!("Initializing IPC..."); ipc::init(); - // channel for text commands (manually written in the GUI, OR fed by macro replay) + // channel for text commands (manually written in the GUI) let (text_cmd_tx, text_cmd_rx) = mpsc::channel::(); - // Wire macro replay through the same text-command channel: each step of a - // replayed macro becomes a synthetic text command, processed in order. - let macro_tx = text_cmd_tx.clone(); - jarvis_core::macros::set_replay_callback(Box::new(move |phrase: &str| { - if let Err(e) = macro_tx.send(phrase.to_string()) { - warn!("Macro replay: failed to enqueue '{}': {}", phrase, e); - } - })); - ipc::set_action_handler(move |action| { match action { IpcAction::Stop => { @@ -210,11 +195,6 @@ fn main() -> Result<(), String> { let _ = app::start(text_cmd_rx, &app_rt); }); - // IMBA-8: boot timing — log how long startup took so users can spot regressions. - let boot_ms = boot_start.elapsed().as_millis(); - info!("[startup] Jarvis ready in {} ms (before tray loop)", boot_ms); - eprintln!("[jarvis-app] startup: {} ms", boot_ms); - eprintln!("[jarvis-app] step: tray::init_blocking"); tray::init_blocking(settings); diff --git a/crates/jarvis-core/src/lib.rs b/crates/jarvis-core/src/lib.rs index 7ed72d1..b07b796 100644 --- a/crates/jarvis-core/src/lib.rs +++ b/crates/jarvis-core/src/lib.rs @@ -60,8 +60,6 @@ pub mod profiles; pub mod scheduler; -pub mod macros; - #[cfg(feature = "llm")] pub mod llm; diff --git a/crates/jarvis-core/src/lua/api.rs b/crates/jarvis-core/src/lua/api.rs index 9634b16..22b0326 100644 --- a/crates/jarvis-core/src/lua/api.rs +++ b/crates/jarvis-core/src/lua/api.rs @@ -13,5 +13,4 @@ pub mod profile; pub mod vision; pub mod scheduler; pub mod cmd; -pub mod health; -pub mod macros; \ No newline at end of file +pub mod health; \ No newline at end of file diff --git a/crates/jarvis-core/src/lua/api/macros.rs b/crates/jarvis-core/src/lua/api/macros.rs deleted file mode 100644 index 89d0657..0000000 --- a/crates/jarvis-core/src/lua/api/macros.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! Lua bindings for the macro recorder. -//! -//! Usage from Lua: -//! jarvis.macros.start("работа") -- begin recording -//! jarvis.macros.save() -- persist + stop -//! jarvis.macros.cancel() -- abort without saving -//! jarvis.macros.replay("работа") -- play back; returns step count or nil -//! jarvis.macros.list() -- array of {name, steps_count, last_run} -//! jarvis.macros.delete(name) -- removes -//! jarvis.macros.is_recording() -- bool -//! jarvis.macros.recording_name() -- string or nil - -use mlua::{Lua, Table, Value}; - -use crate::macros; - -pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> { - let m = lua.create_table()?; - - m.set("start", lua.create_function(|_, name: String| { - macros::start_recording(&name).map_err(mlua::Error::external) - })?)?; - - m.set("save", lua.create_function(|_, ()| { - macros::save_recording().map_err(mlua::Error::external) - })?)?; - - m.set("cancel", lua.create_function(|_, ()| { - Ok(macros::cancel_recording()) - })?)?; - - m.set("is_recording", lua.create_function(|_, ()| { - Ok(macros::is_recording()) - })?)?; - - m.set("recording_name", lua.create_function(|lua, ()| { - match macros::recording_name() { - Some(n) => Ok(Value::String(lua.create_string(n)?)), - None => Ok(Value::Nil), - } - })?)?; - - m.set("replay", lua.create_function(|_, name: String| { - macros::replay(&name).map_err(mlua::Error::external) - })?)?; - - m.set("delete", lua.create_function(|_, name: String| { - Ok(macros::delete(&name)) - })?)?; - - m.set("list", lua.create_function(|lua, ()| { - let arr = lua.create_table()?; - for (i, mac) in macros::list().iter().enumerate() { - let row = lua.create_table()?; - row.set("name", mac.name.clone())?; - row.set("steps_count", mac.steps.len())?; - row.set("created_at", mac.created_at)?; - let last: Value = match mac.last_run { - Some(ts) => Value::Integer(ts), - None => Value::Nil, - }; - row.set("last_run", last)?; - arr.set(i + 1, row)?; - } - Ok(arr) - })?)?; - - jarvis.set("macros", m)?; - Ok(()) -} diff --git a/crates/jarvis-core/src/lua/engine.rs b/crates/jarvis-core/src/lua/engine.rs index 91cf1cf..a481b3e 100644 --- a/crates/jarvis-core/src/lua/engine.rs +++ b/crates/jarvis-core/src/lua/engine.rs @@ -83,7 +83,6 @@ impl LuaEngine { api::scheduler::register(&self.lua, &jarvis)?; api::cmd::register(&self.lua, &jarvis)?; api::health::register(&self.lua, &jarvis)?; - api::macros::register(&self.lua, &jarvis)?; // sandbox-controlled APIs if self.sandbox.allows_http() { diff --git a/crates/jarvis-core/src/macros.rs b/crates/jarvis-core/src/macros.rs deleted file mode 100644 index 9c216a9..0000000 --- a/crates/jarvis-core/src/macros.rs +++ /dev/null @@ -1,303 +0,0 @@ -//! IMBA-7: Voice-command macros — record a sequence of commands once, -//! replay them later with one phrase. -//! -//! Simpler than AHK-style keyboard hooks: we record what the user SAID, then -//! replay each phrase through the normal dispatch path. Works for anything -//! that's already a known command (volume, media keys, profile switches, ...). -//! -//! Lifecycle: -//! -//! "Запиши макрос работа" → start_recording("работа") -//! "Открой браузер" → recorder buffers "открой браузер" -//! "Запусти спотифай" → recorder buffers -//! "Режим работа" → recorder buffers -//! "Сохрани макрос" → save() → persists to disk -//! -//! "Запусти макрос работа" → replay("работа") → fires each phrase in order, -//! with a small inter-step delay -//! -//! Storage: JSON at `/macros.json`. - -use once_cell::sync::OnceCell; -use parking_lot::RwLock; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use std::path::PathBuf; - -use crate::APP_CONFIG_DIR; - -const FILE_NAME: &str = "macros.json"; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Macro { - pub name: String, - pub steps: Vec, - #[serde(default)] - pub created_at: i64, - #[serde(default)] - pub last_run: Option, -} - -#[derive(Default, Debug, Serialize, Deserialize)] -struct Store { - #[serde(default)] - macros: BTreeMap, -} - -#[derive(Debug)] -struct RecordingState { - name: String, - steps: Vec, -} - -struct State { - path: PathBuf, - store: RwLock, - recording: RwLock>, -} - -static STATE: OnceCell = OnceCell::new(); - -pub fn init() -> Result<(), String> { - if STATE.get().is_some() { return Ok(()); } - let dir = APP_CONFIG_DIR.get() - .ok_or_else(|| "APP_CONFIG_DIR not initialised".to_string())?; - let path = dir.join(FILE_NAME); - let store = load_or_empty(&path); - info!("Macros loaded: {} stored at {}", store.macros.len(), path.display()); - STATE.set(State { path, store: RwLock::new(store), recording: RwLock::new(None) }) - .map_err(|_| "macros already initialised".to_string())?; - Ok(()) -} - -fn load_or_empty(path: &PathBuf) -> Store { - if !path.is_file() { return Store::default(); } - match std::fs::read_to_string(path) { - Ok(s) => serde_json::from_str(&s).unwrap_or_else(|e| { - warn!("Corrupt macros file {}: {}", path.display(), e); - Store::default() - }), - Err(e) => { - warn!("Cannot read {}: {}", path.display(), e); - Store::default() - } - } -} - -fn persist(state: &State) { - let store = state.store.read(); - let tmp = state.path.with_extension("json.tmp"); - let json = match serde_json::to_string_pretty(&*store) { - Ok(s) => s, - Err(e) => { warn!("Macros serialize failed: {}", e); return; } - }; - if let Err(e) = std::fs::write(&tmp, json) { - warn!("Macros write failed: {}", e); return; - } - if let Err(e) = std::fs::rename(&tmp, &state.path) { - warn!("Macros rename failed: {}", e); - } -} - -fn now_secs() -> i64 { - use std::time::{SystemTime, UNIX_EPOCH}; - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_secs() as i64) - .unwrap_or(0) -} - -fn normalize_name(n: &str) -> String { - n.trim().to_lowercase() -} - -/// Begin recording a new macro. Replaces any in-progress recording. -pub fn start_recording(name: &str) -> Result<(), String> { - let state = STATE.get().ok_or_else(|| "macros not init".to_string())?; - let nname = normalize_name(name); - if nname.is_empty() { return Err("empty macro name".into()); } - *state.recording.write() = Some(RecordingState { - name: nname.clone(), - steps: Vec::new(), - }); - info!("Macros: recording started for '{}'", nname); - Ok(()) -} - -/// Buffer a phrase into the active recording (if any). Called by the command -/// dispatch hook after a command runs successfully. -pub fn record_step(phrase: &str) { - let Some(state) = STATE.get() else { return; }; - let mut rec = state.recording.write(); - if let Some(r) = rec.as_mut() { - let p = phrase.trim(); - if p.is_empty() { return; } - // Don't record macro-control commands themselves (would be infinite recursion). - if is_macro_control(p) { return; } - r.steps.push(p.to_string()); - debug!("Macros: recorded step '{}' for '{}'", p, r.name); - } -} - -fn is_macro_control(phrase: &str) -> bool { - let p = phrase.to_lowercase(); - p.contains("запиши макрос") || p.contains("сохрани макрос") - || p.contains("отмени макрос") || p.contains("прекрати макрос") - || p.contains("отмени запись") - || p.starts_with("запусти макрос") || p.starts_with("воспроизведи макрос") - || p.starts_with("record macro") || p.starts_with("save macro") - || p.starts_with("play macro") -} - -/// Returns true if a recording is currently active. -pub fn is_recording() -> bool { - STATE.get().and_then(|s| s.recording.read().as_ref().map(|_| ())).is_some() -} - -/// Name of the macro currently being recorded, if any. -pub fn recording_name() -> Option { - STATE.get().and_then(|s| s.recording.read().as_ref().map(|r| r.name.clone())) -} - -/// Stop recording and save the macro. Returns the number of steps captured. -pub fn save_recording() -> Result { - let state = STATE.get().ok_or_else(|| "macros not init".to_string())?; - let rec = state.recording.write().take() - .ok_or_else(|| "не было активной записи".to_string())?; - - if rec.steps.is_empty() { - return Err("макрос пустой".into()); - } - - let count = rec.steps.len(); - let m = Macro { - name: rec.name.clone(), - steps: rec.steps, - created_at: now_secs(), - last_run: None, - }; - state.store.write().macros.insert(rec.name.clone(), m); - persist(state); - info!("Macros: saved '{}' ({} steps)", rec.name, count); - Ok(count) -} - -/// Cancel the active recording without saving. -pub fn cancel_recording() -> bool { - let Some(state) = STATE.get() else { return false; }; - state.recording.write().take().is_some() -} - -pub fn list() -> Vec { - let Some(state) = STATE.get() else { return Vec::new(); }; - state.store.read().macros.values().cloned().collect() -} - -pub fn get(name: &str) -> Option { - let state = STATE.get()?; - state.store.read().macros.get(&normalize_name(name)).cloned() -} - -pub fn delete(name: &str) -> bool { - let Some(state) = STATE.get() else { return false; }; - let removed = state.store.write().macros.remove(&normalize_name(name)).is_some(); - if removed { persist(state); } - removed -} - -/// Mark a macro as run (updates last_run timestamp). -pub fn mark_run(name: &str) { - let Some(state) = STATE.get() else { return; }; - let nname = normalize_name(name); - { - let mut store = state.store.write(); - if let Some(m) = store.macros.get_mut(&nname) { - m.last_run = Some(now_secs()); - } - } - persist(state); -} - -/// Replay callback — jarvis-app registers a function that fires a text command. -/// Macros module owns the timing of replays. -pub type ReplayCallback = Box; - -static REPLAY_CB: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); - -pub fn set_replay_callback(cb: ReplayCallback) { - let _ = REPLAY_CB.set(cb); -} - -const STEP_DELAY_MS: u64 = 800; - -/// Replay a stored macro. Each step is sent to the registered callback (which -/// the app wires to its text-command channel). Inter-step delay gives the -/// dispatcher time to process before the next step arrives. Returns step count. -pub fn replay(name: &str) -> Result { - let m = get(name).ok_or_else(|| format!("Макрос '{}' не найден.", name))?; - let cb = REPLAY_CB.get().ok_or_else(|| { - "Replay callback not registered (jarvis-app didn't wire it).".to_string() - })?; - - let steps_count = m.steps.len(); - info!("Macros: replay '{}' ({} steps)", m.name, steps_count); - let _ = cb; // sanity-checked existence; thread re-reads REPLAY_CB inside. - - // Spawn so the caller (likely a Lua script) returns immediately. - let name_owned = m.name.clone(); - let steps = m.steps.clone(); - std::thread::Builder::new() - .name(format!("macro-replay-{}", name_owned)) - .spawn(move || { - let cb = match REPLAY_CB.get() { - Some(c) => c, - None => { warn!("Macros: callback vanished mid-replay"); return; } - }; - for (i, step) in steps.iter().enumerate() { - info!("Macros: replay step {}/{}: {}", i + 1, steps.len(), step); - cb(step); - if i + 1 < steps.len() { - std::thread::sleep(std::time::Duration::from_millis(STEP_DELAY_MS)); - } - } - mark_run(&name_owned); - }) - .ok(); - - Ok(steps_count) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn is_macro_control_catches_record_phrase() { - assert!(is_macro_control("Запиши макрос работа")); - assert!(is_macro_control("сохрани макрос")); - assert!(is_macro_control("запусти макрос работа")); - assert!(!is_macro_control("открой браузер")); - assert!(!is_macro_control("установи громкость 50")); - } - - #[test] - fn normalize_name_lowercases_trims() { - assert_eq!(normalize_name(" Работа "), "работа"); - assert_eq!(normalize_name("GameMode"), "gamemode"); - } - - #[test] - fn macro_serde_round_trip() { - let m = Macro { - name: "test".into(), - steps: vec!["a".into(), "b".into()], - created_at: 100, - last_run: Some(200), - }; - let json = serde_json::to_string(&m).unwrap(); - let back: Macro = serde_json::from_str(&json).unwrap(); - assert_eq!(back.name, "test"); - assert_eq!(back.steps.len(), 2); - assert_eq!(back.last_run, Some(200)); - } -} diff --git a/resources/commands/macros/cancel.lua b/resources/commands/macros/cancel.lua deleted file mode 100644 index e10e867..0000000 --- a/resources/commands/macros/cancel.lua +++ /dev/null @@ -1,4 +0,0 @@ -if jarvis.macros.cancel() then - return jarvis.cmd.ok("Запись макроса отменена.") -end -return jarvis.cmd.not_found("Записи не было.") diff --git a/resources/commands/macros/command.toml b/resources/commands/macros/command.toml deleted file mode 100644 index fa0cf62..0000000 --- a/resources/commands/macros/command.toml +++ /dev/null @@ -1,107 +0,0 @@ -# IMBA-7: Voice macros — record and replay sequences of voice commands. - -[[commands]] -id = "macros.start_recording" -type = "lua" -script = "start.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "запиши макрос", - "начни запись макроса", - "записывай макрос", - "новый макрос", -] -en = ["record macro", "start macro recording"] -ua = ["запиши макрос"] - - -[[commands]] -id = "macros.save" -type = "lua" -script = "save.lua" -sandbox = "standard" -timeout = 3000 - -[commands.phrases] -ru = [ - "сохрани макрос", - "стоп макрос", - "стоп запись макроса", - "закончи запись", - "макрос готов", -] -en = ["save macro", "stop macro", "finish macro"] -ua = ["збережи макрос", "стоп макрос"] - - -[[commands]] -id = "macros.cancel" -type = "lua" -script = "cancel.lua" -sandbox = "minimal" -timeout = 2000 - -[commands.phrases] -ru = [ - "отмени макрос", - "отмени запись макроса", - "отмени запись", - "забудь макрос", -] -en = ["cancel macro", "discard macro"] -ua = ["скасуй макрос"] - - -[[commands]] -id = "macros.replay" -type = "lua" -script = "replay.lua" -sandbox = "standard" -timeout = 60000 - -[commands.phrases] -ru = [ - "запусти макрос", - "выполни макрос", - "воспроизведи макрос", - "сыграй макрос", - "проиграй макрос", -] -en = ["play macro", "replay macro", "run macro"] -ua = ["запусти макрос"] - - -[[commands]] -id = "macros.list" -type = "lua" -script = "list.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "какие у меня макросы", - "список макросов", - "покажи макросы", -] -en = ["list macros", "show macros"] -ua = ["список макросів"] - - -[[commands]] -id = "macros.delete" -type = "lua" -script = "delete.lua" -sandbox = "minimal" -timeout = 3000 - -[commands.phrases] -ru = [ - "удали макрос", - "забудь о макросе", -] -en = ["delete macro", "remove macro"] -ua = ["видали макрос"] diff --git a/resources/commands/macros/delete.lua b/resources/commands/macros/delete.lua deleted file mode 100644 index 4215266..0000000 --- a/resources/commands/macros/delete.lua +++ /dev/null @@ -1,18 +0,0 @@ -local phrase = (jarvis.context.phrase or ""):lower() -local name = jarvis.text.strip_trigger(phrase, { - "удали макрос", - "забудь о макросе", - "delete macro", - "remove macro", - "видали макрос", -}) -name = name:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if name == "" then - return jarvis.cmd.error("Какой макрос удалить?") -end - -if jarvis.macros.delete(name) then - return jarvis.cmd.ok("Удалил макрос " .. name .. ".") -end -return jarvis.cmd.not_found("Макрос " .. name .. " не найден.") diff --git a/resources/commands/macros/list.lua b/resources/commands/macros/list.lua deleted file mode 100644 index ef6bf64..0000000 --- a/resources/commands/macros/list.lua +++ /dev/null @@ -1,12 +0,0 @@ -local list = jarvis.macros.list() -if #list == 0 then - return jarvis.cmd.ok("Макросов нет.") -end - -local sample = math.min(#list, 5) -local line = string.format("Макросов: %d. ", #list) -for i = 1, sample do - line = line .. list[i].name .. " (" .. list[i].steps_count .. " шагов)" - if i < sample then line = line .. ", " end -end -return jarvis.cmd.ok(line .. ".") diff --git a/resources/commands/macros/replay.lua b/resources/commands/macros/replay.lua deleted file mode 100644 index 3dce4a7..0000000 --- a/resources/commands/macros/replay.lua +++ /dev/null @@ -1,24 +0,0 @@ --- "Запусти макрос работа" -local phrase = (jarvis.context.phrase or ""):lower() -local name = jarvis.text.strip_trigger(phrase, { - "выполни макрос", - "воспроизведи макрос", - "запусти макрос", - "сыграй макрос", - "проиграй макрос", - "play macro", - "replay macro", - "run macro", -}) -name = name:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if name == "" then - return jarvis.cmd.error("Какой макрос запустить?") -end - -local ok, result = pcall(function() return jarvis.macros.replay(name) end) -if not ok then - return jarvis.cmd.not_found(tostring(result)) -end - -return jarvis.cmd.ok(string.format("Запускаю %s, шагов: %d.", name, result)) diff --git a/resources/commands/macros/save.lua b/resources/commands/macros/save.lua deleted file mode 100644 index 750a506..0000000 --- a/resources/commands/macros/save.lua +++ /dev/null @@ -1,8 +0,0 @@ -local ok, result = pcall(function() return jarvis.macros.save() end) -if not ok then - -- result here is the error msg from save_recording - return jarvis.cmd.not_found(tostring(result)) -end - -local name = jarvis.macros.recording_name() or "макрос" -return jarvis.cmd.ok(string.format("Сохранил %d шагов.", result)) diff --git a/resources/commands/macros/start.lua b/resources/commands/macros/start.lua deleted file mode 100644 index 164acaf..0000000 --- a/resources/commands/macros/start.lua +++ /dev/null @@ -1,23 +0,0 @@ --- "Запиши макрос работа" -local phrase = (jarvis.context.phrase or ""):lower() -local name = jarvis.text.strip_trigger(phrase, { - "начни запись макроса", - "записывай макрос", - "запиши макрос", - "новый макрос", - "record macro", - "start macro recording", -}) -name = name:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") - -if name == "" then - return jarvis.cmd.error("Как назвать макрос?") -end - -local ok, err = pcall(function() jarvis.macros.start(name) end) -if not ok then - jarvis.log("warn", "macros.start: " .. tostring(err)) - return jarvis.cmd.error("Не получилось начать запись.") -end - -return jarvis.cmd.ok(string.format("Записываю макрос %s. Говорите команды. Когда закончите — скажите сохрани макрос.", name))