feat(macros): IMBA-7 voice-recorded command sequences (VoiceAttack-style)
Simpler than AHK keyboard hooks: record what the user SAID, replay each phrase
through the normal dispatch path. Works for any command jarvis already knows.
Flow:
"Запиши макрос работа" → start_recording("работа")
"Открой браузер" → buffered into the recording
"Запусти спотифай" → buffered
"Режим работа" → buffered
"Сохрани макрос" → save_recording() → persist
...later...
"Запусти макрос работа" → replay("работа") → fires each phrase in order
Core (crates/jarvis-core/src/macros.rs)
- Macro {name, steps, created_at, last_run} persisted at
<APP_CONFIG_DIR>/macros.json with atomic write-through.
- Recording state in-memory: RwLock<Option<RecordingState>>.
- start_recording / record_step / save_recording / cancel_recording.
- list / get / delete / mark_run.
- replay(name) spawns a thread that fires each step through a callback,
with 800ms inter-step delay. Caller returns immediately.
- is_macro_control filter prevents recording the meta-commands themselves
(no infinite recursion when "запиши макрос" runs during a recording).
- 3 unit tests (55 total).
Wiring (crates/jarvis-app/src/main.rs)
- macros::init() before listener starts.
- macros::set_replay_callback(move |phrase| text_cmd_tx.send(phrase)) — each
replay step gets queued as a synthetic text command, processed by the same
dispatcher the GUI uses. No special-case code path.
Recording hook (crates/jarvis-app/src/app.rs::execute_command)
- On successful command execution → call macros::record_step(text).
- Failed commands are NOT recorded (would fail on replay too).
- is_macro_control filter inside record_step skips meta-commands.
Lua API (crates/jarvis-core/src/lua/api/macros.rs)
- jarvis.macros.{start, save, cancel, replay, list, delete,
is_recording, recording_name}
Voice pack (resources/commands/macros/, 6 commands)
- "запиши макрос NAME" → macros.start_recording
- "сохрани макрос" → macros.save
- "отмени макрос" → macros.cancel
- "запусти макрос NAME" → macros.replay
- "какие у меня макросы" → macros.list
- "удали макрос NAME" → macros.delete
Build: cargo build --release -p jarvis-app green. 55/55 tests pass.
This commit is contained in:
parent
d63399f4c9
commit
dbb4a6b888
14 changed files with 594 additions and 2 deletions
|
|
@ -464,6 +464,11 @@ 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,
|
||||
|
|
|
|||
|
|
@ -88,6 +88,11 @@ 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();
|
||||
|
|
@ -153,9 +158,18 @@ fn main() -> Result<(), String> {
|
|||
info!("Initializing IPC...");
|
||||
ipc::init();
|
||||
|
||||
// channel for text commands (manually written in the GUI)
|
||||
// channel for text commands (manually written in the GUI, OR fed by macro replay)
|
||||
let (text_cmd_tx, text_cmd_rx) = mpsc::channel::<String>();
|
||||
|
||||
// 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 => {
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ pub mod profiles;
|
|||
|
||||
pub mod scheduler;
|
||||
|
||||
pub mod macros;
|
||||
|
||||
#[cfg(feature = "llm")]
|
||||
pub mod llm;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,4 +13,5 @@ pub mod profile;
|
|||
pub mod vision;
|
||||
pub mod scheduler;
|
||||
pub mod cmd;
|
||||
pub mod health;
|
||||
pub mod health;
|
||||
pub mod macros;
|
||||
70
crates/jarvis-core/src/lua/api/macros.rs
Normal file
70
crates/jarvis-core/src/lua/api/macros.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
//! 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(())
|
||||
}
|
||||
|
|
@ -83,6 +83,7 @@ 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() {
|
||||
|
|
|
|||
303
crates/jarvis-core/src/macros.rs
Normal file
303
crates/jarvis-core/src/macros.rs
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
//! 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 `<APP_CONFIG_DIR>/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<String>,
|
||||
#[serde(default)]
|
||||
pub created_at: i64,
|
||||
#[serde(default)]
|
||||
pub last_run: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize)]
|
||||
struct Store {
|
||||
#[serde(default)]
|
||||
macros: BTreeMap<String, Macro>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RecordingState {
|
||||
name: String,
|
||||
steps: Vec<String>,
|
||||
}
|
||||
|
||||
struct State {
|
||||
path: PathBuf,
|
||||
store: RwLock<Store>,
|
||||
recording: RwLock<Option<RecordingState>>,
|
||||
}
|
||||
|
||||
static STATE: OnceCell<State> = 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<String> {
|
||||
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<usize, String> {
|
||||
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<Macro> {
|
||||
let Some(state) = STATE.get() else { return Vec::new(); };
|
||||
state.store.read().macros.values().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn get(name: &str) -> Option<Macro> {
|
||||
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<dyn Fn(&str) + Send + Sync>;
|
||||
|
||||
static REPLAY_CB: once_cell::sync::OnceCell<ReplayCallback> = 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<usize, String> {
|
||||
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));
|
||||
}
|
||||
}
|
||||
4
resources/commands/macros/cancel.lua
Normal file
4
resources/commands/macros/cancel.lua
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
if jarvis.macros.cancel() then
|
||||
return jarvis.cmd.ok("Запись макроса отменена.")
|
||||
end
|
||||
return jarvis.cmd.not_found("Записи не было.")
|
||||
107
resources/commands/macros/command.toml
Normal file
107
resources/commands/macros/command.toml
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# 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 = ["видали макрос"]
|
||||
18
resources/commands/macros/delete.lua
Normal file
18
resources/commands/macros/delete.lua
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
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 .. " не найден.")
|
||||
12
resources/commands/macros/list.lua
Normal file
12
resources/commands/macros/list.lua
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
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 .. ".")
|
||||
24
resources/commands/macros/replay.lua
Normal file
24
resources/commands/macros/replay.lua
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
-- "Запусти макрос работа"
|
||||
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))
|
||||
8
resources/commands/macros/save.lua
Normal file
8
resources/commands/macros/save.lua
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
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))
|
||||
23
resources/commands/macros/start.lua
Normal file
23
resources/commands/macros/start.lua
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
-- "Запиши макрос работа"
|
||||
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))
|
||||
Loading…
Add table
Add a link
Reference in a new issue