Compare commits
No commits in common. "c7ab751e39532ed31d3a00b0457c31907d6d957c" and "d63399f4c96e954e310fc38edda2f5c20a6151d8" have entirely different histories.
c7ab751e39
...
d63399f4c9
14 changed files with 2 additions and 600 deletions
|
|
@ -464,11 +464,6 @@ fn execute_command(text: &str, rt: &tokio::runtime::Runtime) -> bool {
|
||||||
info!("Command executed successfully");
|
info!("Command executed successfully");
|
||||||
// voices::play_ok();
|
// voices::play_ok();
|
||||||
voices::play_random_from(cmd_config.get_sounds(&i18n::get_language()).as_slice());
|
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 {
|
ipc::send(IpcEvent::CommandExecuted {
|
||||||
id: cmd_config.id.clone(),
|
id: cmd_config.id.clone(),
|
||||||
success: true,
|
success: true,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ mod tray;
|
||||||
static SHOULD_STOP: AtomicBool = AtomicBool::new(false);
|
static SHOULD_STOP: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
fn main() -> Result<(), String> {
|
fn main() -> Result<(), String> {
|
||||||
let boot_start = std::time::Instant::now();
|
|
||||||
load_dotenv_near_exe();
|
load_dotenv_near_exe();
|
||||||
|
|
||||||
eprintln!("[jarvis-app] step: init_dirs");
|
eprintln!("[jarvis-app] step: init_dirs");
|
||||||
|
|
@ -89,11 +88,6 @@ fn main() -> Result<(), String> {
|
||||||
jarvis_core::scheduler::start_background();
|
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");
|
eprintln!("[jarvis-app] step: recorder::init");
|
||||||
if recorder::init().is_err() {
|
if recorder::init().is_err() {
|
||||||
notify_mic_problem();
|
notify_mic_problem();
|
||||||
|
|
@ -159,18 +153,9 @@ fn main() -> Result<(), String> {
|
||||||
info!("Initializing IPC...");
|
info!("Initializing IPC...");
|
||||||
ipc::init();
|
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::<String>();
|
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| {
|
ipc::set_action_handler(move |action| {
|
||||||
match action {
|
match action {
|
||||||
IpcAction::Stop => {
|
IpcAction::Stop => {
|
||||||
|
|
@ -210,11 +195,6 @@ fn main() -> Result<(), String> {
|
||||||
let _ = app::start(text_cmd_rx, &app_rt);
|
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");
|
eprintln!("[jarvis-app] step: tray::init_blocking");
|
||||||
tray::init_blocking(settings);
|
tray::init_blocking(settings);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,8 +60,6 @@ pub mod profiles;
|
||||||
|
|
||||||
pub mod scheduler;
|
pub mod scheduler;
|
||||||
|
|
||||||
pub mod macros;
|
|
||||||
|
|
||||||
#[cfg(feature = "llm")]
|
#[cfg(feature = "llm")]
|
||||||
pub mod llm;
|
pub mod llm;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,3 @@ pub mod vision;
|
||||||
pub mod scheduler;
|
pub mod scheduler;
|
||||||
pub mod cmd;
|
pub mod cmd;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod macros;
|
|
||||||
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
|
|
@ -83,7 +83,6 @@ impl LuaEngine {
|
||||||
api::scheduler::register(&self.lua, &jarvis)?;
|
api::scheduler::register(&self.lua, &jarvis)?;
|
||||||
api::cmd::register(&self.lua, &jarvis)?;
|
api::cmd::register(&self.lua, &jarvis)?;
|
||||||
api::health::register(&self.lua, &jarvis)?;
|
api::health::register(&self.lua, &jarvis)?;
|
||||||
api::macros::register(&self.lua, &jarvis)?;
|
|
||||||
|
|
||||||
// sandbox-controlled APIs
|
// sandbox-controlled APIs
|
||||||
if self.sandbox.allows_http() {
|
if self.sandbox.allows_http() {
|
||||||
|
|
|
||||||
|
|
@ -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 `<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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
if jarvis.macros.cancel() then
|
|
||||||
return jarvis.cmd.ok("Запись макроса отменена.")
|
|
||||||
end
|
|
||||||
return jarvis.cmd.not_found("Записи не было.")
|
|
||||||
|
|
@ -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 = ["видали макрос"]
|
|
||||||
|
|
@ -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 .. " не найден.")
|
|
||||||
|
|
@ -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 .. ".")
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue