From a16d2401e7fb47ebd6426c0f7627f6ada94faaa7 Mon Sep 17 00:00:00 2001 From: Bossiara13 <236771060+DmitryBykov-ISPO@users.noreply.github.com> Date: Fri, 15 May 2026 12:58:16 +0300 Subject: [PATCH] arch + features: Lua jarvis.speak() / jarvis.llm() API, /commands GUI page, games / mouse / random_choice packs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Architecture — DRY at the Lua API surface: lua/api/tts.rs (new): jarvis.speak(text, opts?). opts.lang = ISO two-letter code (default "ru"); auto-picks first installed voice matching the culture. opts.async = true|false (default true). The 9 packs that previously inlined a 60-character PS-SAPI string for every spoken reply can now reduce to one line. Refactored fun/, dice/, clipboard_read/ as proof — fun/ask.lua went from 75 lines of HTTP+SAPI boilerplate to ~28 lines of intent code. lua/api/llm.rs (new): jarvis.llm(messages, opts?). messages is a list of {role, content} tables, opts.max_tokens / .temperature / .top_p. Returns the assistant string or nil. Gated by sandbox allows_http() (so Standard+ packs only). Internally uses the existing jarvis_core::llm::LlmClient::complete_with, no duplicated HTTP plumbing. llm/client.rs: split complete() into complete_with(messages, max_tokens, temperature, top_p) for the new caller; old complete() is now a thin wrapper at temp=0.7 top_p=1.0 for backward compat. GUI — /commands page rewrite: Replaces the "Раздел в разработке" placeholder. Calls Tauri command get_commands_list (already existed in tauri_commands/commands.rs, was wired but unused). Renders a search-filterable card grid: - cmd id (monospace) - type badge with colour by kind (lua=blue, ahk=red, cli=grey, voice=green, terminate=red, stop_chaining=violet) - sandbox badge - description line (if non-empty) - all phrases for currentLanguage, fallback to en, fallback to first available; each phrase in a small inline pill Toolbar: text input with magnifier icon + reload button (↻). Counter line shows visible/total + "Скажи Джарвис + любую фразу" hint. Counts down as you type the filter. New packs (3, total 33 → 36): games/ — launch_game / list_games. Reads or creates %USERPROFILE%\Documents\jarvis-games.json (the sample preloads dota, cs2, elden ring, witcher 3, factorio + minecraft launcher path + fortnite epic URI). Trigger-strip + fuzzy name/alias match, then launches via steam://rungameid/N (Steam), epic:// (Epic Games), or start "" "path" (anything else). list_games speaks the configured library. mouse/ — left/right/middle/double click + scroll up/down. Single dispatch.lua + _mouse_helper.ps1. PS P/Invokes user32!mouse_event with the right flag pair (LEFTDOWN+LEFTUP / etc.), 30ms gap, doubles do two LEFT cycles with a 50ms gap, wheel uses ±360 ticks. random_choice/ — "выбери из X или Y или Z". Strips the trigger, splits on " или " / " or " / ", " / " либо " / " чи ", math.random picks one, speaks "Я выбираю N". cargo test commands::tests still 3/3. 36 packs verified via jarvis-gui startup log. --- crates/jarvis-core/src/llm/client.rs | 14 +- crates/jarvis-core/src/lua/api.rs | 4 +- crates/jarvis-core/src/lua/api/llm.rs | 64 +++++ crates/jarvis-core/src/lua/api/tts.rs | 63 +++++ crates/jarvis-core/src/lua/engine.rs | 4 +- frontend/src/routes/commands/index.svelte | 258 ++++++++++++++++-- resources/commands/clipboard_read/read.lua | 9 +- resources/commands/dice/roll.lua | 9 +- resources/commands/fun/ask.lua | 51 +--- resources/commands/games/command.toml | 41 +++ resources/commands/games/launch.lua | 110 ++++++++ resources/commands/games/list.lua | 26 ++ resources/commands/mouse/_mouse_helper.ps1 | 33 +++ resources/commands/mouse/command.toml | 63 +++++ resources/commands/mouse/dispatch.lua | 31 +++ resources/commands/random_choice/command.toml | 23 ++ resources/commands/random_choice/pick.lua | 45 +++ 17 files changed, 765 insertions(+), 83 deletions(-) create mode 100644 crates/jarvis-core/src/lua/api/llm.rs create mode 100644 crates/jarvis-core/src/lua/api/tts.rs create mode 100644 resources/commands/games/command.toml create mode 100644 resources/commands/games/launch.lua create mode 100644 resources/commands/games/list.lua create mode 100644 resources/commands/mouse/_mouse_helper.ps1 create mode 100644 resources/commands/mouse/command.toml create mode 100644 resources/commands/mouse/dispatch.lua create mode 100644 resources/commands/random_choice/command.toml create mode 100644 resources/commands/random_choice/pick.lua diff --git a/crates/jarvis-core/src/llm/client.rs b/crates/jarvis-core/src/llm/client.rs index 7c0ce91..1f92134 100644 --- a/crates/jarvis-core/src/llm/client.rs +++ b/crates/jarvis-core/src/llm/client.rs @@ -103,13 +103,23 @@ impl LlmClient { } pub fn complete(&self, messages: &[ChatMessage], max_tokens: u32) -> Result { + self.complete_with(messages, max_tokens, 0.7, 1.0) + } + + pub fn complete_with( + &self, + messages: &[ChatMessage], + max_tokens: u32, + temperature: f32, + top_p: f32, + ) -> Result { let url = format!("{}/chat/completions", self.base_url.trim_end_matches('/')); let body = ChatRequest { model: &self.model, messages, max_tokens, - temperature: 0.7, - top_p: 1.0, + temperature, + top_p, }; let resp = self diff --git a/crates/jarvis-core/src/lua/api.rs b/crates/jarvis-core/src/lua/api.rs index 8bda5a4..f0e9a08 100644 --- a/crates/jarvis-core/src/lua/api.rs +++ b/crates/jarvis-core/src/lua/api.rs @@ -4,4 +4,6 @@ pub mod context; pub mod http; pub mod fs; pub mod state; -pub mod system; \ No newline at end of file +pub mod system; +pub mod tts; +pub mod llm; \ No newline at end of file diff --git a/crates/jarvis-core/src/lua/api/llm.rs b/crates/jarvis-core/src/lua/api/llm.rs new file mode 100644 index 0000000..3f00cc8 --- /dev/null +++ b/crates/jarvis-core/src/lua/api/llm.rs @@ -0,0 +1,64 @@ +use mlua::{Lua, Table, Value}; + +use crate::llm::{ChatMessage, LlmClient}; + +// Lua-callable wrapper around jarvis_core::llm::LlmClient. +// Centralises the Groq plumbing so command scripts can just say: +// local reply = jarvis.llm( +// { {role='system', content='...'}, {role='user', content='...'} }, +// { temperature = 0.0, max_tokens = 64 } +// ) +// Returns the assistant text (string) or nil on any failure (missing token, +// network error, empty response, etc.) — callers should always check `if reply`. +pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> { + let chat_fn = lua.create_function(|lua, (messages, opts): (Table, Option)| { + let mut chat_messages = Vec::new(); + for pair in messages.sequence_values::
() { + let msg = pair?; + let role = msg.get::("role").unwrap_or_else(|_| "user".to_string()); + let content = msg.get::("content").unwrap_or_default(); + if content.is_empty() { + continue; + } + chat_messages.push(match role.as_str() { + "system" => ChatMessage::system(content), + "assistant" => ChatMessage::assistant(content), + _ => ChatMessage::user(content), + }); + } + if chat_messages.is_empty() { + return Ok(Value::Nil); + } + + let mut max_tokens: u32 = 256; + let mut temperature: f32 = 0.7; + let mut top_p: f32 = 1.0; + if let Some(o) = opts { + if let Ok(v) = o.get::("max_tokens") { max_tokens = v; } + if let Ok(v) = o.get::("temperature") { temperature = v; } + if let Ok(v) = o.get::("top_p") { top_p = v; } + } + + let client = match LlmClient::from_env() { + Ok(c) => c, + Err(e) => { + log::warn!("[Lua llm] no client: {}", e); + return Ok(Value::Nil); + } + }; + + match client.complete_with(&chat_messages, max_tokens, temperature, top_p) { + Ok(reply) => { + let s = lua.create_string(reply.trim())?; + Ok(Value::String(s)) + } + Err(e) => { + log::warn!("[Lua llm] request failed: {}", e); + Ok(Value::Nil) + } + } + })?; + + jarvis.set("llm", chat_fn)?; + Ok(()) +} diff --git a/crates/jarvis-core/src/lua/api/tts.rs b/crates/jarvis-core/src/lua/api/tts.rs new file mode 100644 index 0000000..43990f1 --- /dev/null +++ b/crates/jarvis-core/src/lua/api/tts.rs @@ -0,0 +1,63 @@ +use mlua::{Lua, Table}; +use std::process::{Command, Stdio}; + +pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> { + // jarvis.speak(text, opts?) + // opts.lang: ISO 2-letter code (ru, en, de, ...). default "ru" + // opts.async: if true, fire-and-forget; if false, block until done. default true + let speak_fn = lua.create_function(|_, (text, opts): (String, Option
)| { + let mut iso = "ru".to_string(); + let mut detached = true; + + if let Some(t) = opts { + if let Ok(v) = t.get::("lang") { iso = v; } + if let Ok(v) = t.get::("async") { detached = v; } + } + + speak_via_sapi(&text, &iso, detached); + Ok(()) + })?; + jarvis.set("speak", speak_fn)?; + + Ok(()) +} + +#[cfg(target_os = "windows")] +fn speak_via_sapi(text: &str, iso: &str, detached: bool) { + if text.is_empty() { + return; + } + let escaped = text.replace('\'', "''").replace('\r', " ").replace('\n', " "); + let safe_iso = if iso.chars().all(|c| c.is_ascii_alphabetic()) && iso.len() == 2 { + iso.to_lowercase() + } else { + "ru".to_string() + }; + let ps = format!( + "Add-Type -AssemblyName System.Speech; \ + $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; \ + foreach ($v in $s.GetInstalledVoices()) {{ \ + if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq '{}') {{ \ + try {{ $s.SelectVoice($v.VoiceInfo.Name); break }} catch {{}} \ + }} \ + }} \ + $s.Speak('{}')", + safe_iso, escaped + ); + + let mut cmd = Command::new("powershell"); + cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &ps]) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + if detached { + let _ = cmd.spawn(); + } else { + let _ = cmd.status(); + } +} + +#[cfg(not(target_os = "windows"))] +fn speak_via_sapi(text: &str, _iso: &str, _detached: bool) { + log::info!("[Lua tts] would speak: {}", text); +} diff --git a/crates/jarvis-core/src/lua/engine.rs b/crates/jarvis-core/src/lua/engine.rs index 71380e1..7b0268d 100644 --- a/crates/jarvis-core/src/lua/engine.rs +++ b/crates/jarvis-core/src/lua/engine.rs @@ -76,10 +76,12 @@ impl LuaEngine { api::core::register(&self.lua, &jarvis)?; api::audio::register(&self.lua, &jarvis)?; api::context::register(&self.lua, &jarvis, context)?; - + api::tts::register(&self.lua, &jarvis)?; + // sandbox-controlled APIs if self.sandbox.allows_http() { api::http::register(&self.lua, &jarvis)?; + api::llm::register(&self.lua, &jarvis)?; } if self.sandbox.allows_state() { diff --git a/frontend/src/routes/commands/index.svelte b/frontend/src/routes/commands/index.svelte index bc751bf..0a95b00 100644 --- a/frontend/src/routes/commands/index.svelte +++ b/frontend/src/routes/commands/index.svelte @@ -1,35 +1,249 @@ - + - - {t('commands-wip-desc')}
- {t('commands-wip-builder')} - {t('commands-wip-builder-link')}. - {t('commands-wip-or-fork')} - {t('commands-wip-fork-link')}. -
+
+ + + + +
+ +
+ {countLabel} + · + + {$currentLanguage === "ru" + ? "Скажи «Джарвис» + любую фразу из карточки" + : "Say \"Jarvis\" + any phrase from a card"} + +
+ +{#if loading} +
+{:else if errorMessage} +
{errorMessage}
+{:else if visible.length === 0} +
+ {$currentLanguage === "ru" ? "Ничего не найдено" : "Nothing found"} +
+{:else} +
+ {#each visible as cmd (cmd.id)} +
+
+ {cmd.id} + + {cmd.type} + + {#if cmd.sandbox} + {cmd.sandbox} + {/if} +
+ {#if cmd.description} +
{cmd.description}
+ {/if} +
    + {#each phrasesForLang(cmd, $currentLanguage) as p} +
  • «{p}»
  • + {/each} +
+
+ {/each} +
+{/if}
+ + diff --git a/resources/commands/clipboard_read/read.lua b/resources/commands/clipboard_read/read.lua index fb464b6..6a28f6f 100644 --- a/resources/commands/clipboard_read/read.lua +++ b/resources/commands/clipboard_read/read.lua @@ -17,17 +17,10 @@ if #to_speak > preview_chars then to_speak = to_speak:sub(1, preview_chars) .. (lang == "ru" and " ... и так далее" or " ... and so on") end -local sapi_text = to_speak:gsub("'", "''"):gsub("\r?\n", " ") -local ps = string.format( - [[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq 'ru') { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]], - sapi_text -) -local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', ps:gsub('"', '\\"')) -jarvis.system.exec(cmd) - jarvis.system.notify( lang == "ru" and "Буфер" or "Clipboard", text:sub(1, 120) ) +jarvis.speak(to_speak, { lang = lang }) jarvis.audio.play_ok() return { chain = false } diff --git a/resources/commands/dice/roll.lua b/resources/commands/dice/roll.lua index 24de619..5d908a9 100644 --- a/resources/commands/dice/roll.lua +++ b/resources/commands/dice/roll.lua @@ -23,13 +23,6 @@ else end jarvis.system.notify(title, text) - -local sapi = say:gsub("'", "''") -local sps = string.format( - [[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq 'ru') { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]], - sapi -) -jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', sps:gsub('"', '\\"'))) - +jarvis.speak(say, { lang = lang }) jarvis.audio.play_ok() return { chain = false } diff --git a/resources/commands/fun/ask.lua b/resources/commands/fun/ask.lua index aeb5d02..9364270 100644 --- a/resources/commands/fun/ask.lua +++ b/resources/commands/fun/ask.lua @@ -16,55 +16,24 @@ local prompts = { or "Compliment the user briefly, in JARVIS-the-butler tone.", } -local sys = prompts[cmd_id] or prompts.joke - -local token = jarvis.system.env("GROQ_TOKEN") -if not token or token == "" then - jarvis.system.notify("Fun", "GROQ_TOKEN не задан") - jarvis.audio.play_error() - return { chain = false } -end - -local base = jarvis.system.env("GROQ_BASE_URL"); if not base or base == "" then base = "https://api.groq.com/openai/v1" end -local model = jarvis.system.env("GROQ_MODEL"); if not model or model == "" then model = "llama-3.3-70b-versatile" end - math.randomseed(os.time() + jarvis.context.time.second * 7919) -local seed_word = (math.random(1, 9999)) .. "" -local payload = { - model = model, - messages = { - { role = "system", content = sys }, - { role = "user", content = "seed " .. seed_word }, +local reply = jarvis.llm( + { + { role = "system", content = prompts[cmd_id] or prompts.joke }, + { role = "user", content = "seed " .. tostring(math.random(1, 9999)) }, }, - max_tokens = 220, - temperature = 1.0, -} -local res = jarvis.http.post_json(base .. "/chat/completions", payload, { Authorization = "Bearer " .. token }) + { max_tokens = 220, temperature = 1.0 } +) -if not res.ok then - jarvis.system.notify("Fun", "Groq не ответил") +if not reply or reply == "" then + jarvis.system.notify("Fun", lang == "ru" and "Не получилось" or "Failed") jarvis.audio.play_error() return { chain = false } end -local content = (res.body or ""):match('"content"%s*:%s*"(.-[^\\])"') -if not content then - jarvis.audio.play_error() - return { chain = false } -end -content = content:gsub('\\n', ' '):gsub('\\"', '"'):gsub('\\\\', '\\'):gsub("%s+", " ") -content = content:gsub("^%s+", ""):gsub("%s+$", "") - local titles = { joke = "Анекдот", fact = "Факт", quote = "Цитата", compliment = "Комплимент" } -jarvis.system.notify(titles[cmd_id] or "Fun", content:sub(1, 240)) - -local sapi = content:gsub("'", "''"):gsub("\r?\n", " ") -local ps = string.format( - [[Add-Type -AssemblyName System.Speech; $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; foreach ($v in $s.GetInstalledVoices()) { if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq 'ru') { try { $s.SelectVoice($v.VoiceInfo.Name); break } catch {} } } $s.Speak('%s')]], - sapi -) -jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'))) - +jarvis.system.notify(titles[cmd_id] or "Fun", reply:sub(1, 240)) +jarvis.speak(reply, { lang = lang }) jarvis.audio.play_ok() return { chain = false } diff --git a/resources/commands/games/command.toml b/resources/commands/games/command.toml new file mode 100644 index 0000000..b3165ce --- /dev/null +++ b/resources/commands/games/command.toml @@ -0,0 +1,41 @@ +[[commands]] +id = "launch_game" +type = "lua" +script = "launch.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "запусти игру", + "включи игру", + "поиграем", + "хочу поиграть", + "запусти доту", + "включи кс", + "включи фортнайт", + "включи майнкрафт", +] +en = [ + "launch game", + "play game", + "launch dota", + "launch cs", +] +ua = [ + "запусти гру", + "увімкни гру", +] + + +[[commands]] +id = "list_games" +type = "lua" +script = "list.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["какие у меня игры", "список игр", "покажи игры"] +en = ["list games", "show games"] +ua = ["які в мене ігри"] diff --git a/resources/commands/games/launch.lua b/resources/commands/games/launch.lua new file mode 100644 index 0000000..1a03a3b --- /dev/null +++ b/resources/commands/games/launch.lua @@ -0,0 +1,110 @@ +local lang = jarvis.context.language +local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" +local config_path = userprofile .. "\\Documents\\jarvis-games.json" + +if not jarvis.fs.exists(config_path) then + local sample = [[[ + {"name": "dota", "aliases": ["доту", "доту 2", "dota 2"], "steam_appid": 570}, + {"name": "cs2", "aliases": ["кс", "контру", "контр страйк"], "steam_appid": 730}, + {"name": "elden ring", "aliases": ["элден", "элден ринг"], "steam_appid": 1245620}, + {"name": "witcher 3", "aliases": ["ведьмак", "ведьмака 3"], "steam_appid": 292030}, + {"name": "factorio", "aliases": ["факторио"], "steam_appid": 427520}, + {"name": "minecraft", "aliases": ["майнкрафт", "майн"], "path": "C:\\Program Files (x86)\\Minecraft Launcher\\MinecraftLauncher.exe"}, + {"name": "fortnite", "aliases": ["фортнайт"], "epic_uri": "com.epicgames.launcher://apps/Fortnite?action=launch&silent=true"} +] +]] + jarvis.fs.write(config_path, sample) + jarvis.system.notify( + lang == "ru" and "Игры" or "Games", + lang == "ru" and "Создан шаблон ~/Documents/jarvis-games.json — добавь свои игры и повтори" + or "Template at Documents/jarvis-games.json — edit and retry" + ) + jarvis.system.open(config_path) + jarvis.audio.play_not_found() + return { chain = false } +end + +local content = jarvis.fs.read(config_path) +local entries = {} +local cursor = 1 +while true do + local s, e, block = content:find("({[^{}]+})", cursor) + if not s then break end + cursor = e + 1 + local name = block:match('"name"%s*:%s*"([^"]+)"') + if name then + local entry = { name = name, name_lower = name:lower(), aliases = {} } + for alias in block:gmatch('"([^"]+)"') do + entry.aliases[#entry.aliases + 1] = alias:lower() + end + entry.steam_appid = tonumber(block:match('"steam_appid"%s*:%s*(%d+)')) + entry.path = block:match('"path"%s*:%s*"([^"]+)"') + entry.epic_uri = block:match('"epic_uri"%s*:%s*"([^"]+)"') + table.insert(entries, entry) + end +end + +if #entries == 0 then + jarvis.system.notify("Games", lang == "ru" and "Пусто в конфиге" or "Empty config") + jarvis.audio.play_not_found() + return { chain = false } +end + +local phrase = (jarvis.context.phrase or ""):lower() +local triggers = { + "запусти игру", "включи игру", "хочу поиграть", "поиграем", "запусти", "включи", + "launch game", "play game", "launch", + "запусти гру", "увімкни гру", +} +local query = phrase +for _, t in ipairs(triggers) do + local s, e = string.find(query, t, 1, true) + if s == 1 then + query = query:sub(e + 1) + break + end +end +query = query:gsub("^%s+", ""):gsub("%s+$", "") + +local function name_matches(e, q) + if e.name_lower == q then return true end + if string.find(e.name_lower, q, 1, true) then return true end + for _, a in ipairs(e.aliases) do + if a == q or string.find(q, a, 1, true) then return true end + end + return false +end + +local match +for _, e in ipairs(entries) do + if name_matches(e, query) then match = e; break end +end + +if not match then + jarvis.system.notify("Games", (lang == "ru" and "Не нашёл: " or "Not found: ") .. query) + jarvis.audio.play_not_found() + return { chain = false } +end + +local launched = false +if match.steam_appid then + jarvis.system.open("steam://rungameid/" .. tostring(match.steam_appid)) + launched = true +elseif match.epic_uri then + jarvis.system.open(match.epic_uri) + launched = true +elseif match.path and jarvis.fs.exists(match.path) then + jarvis.system.exec(string.format('start "" "%s"', match.path)) + launched = true +end + +if launched then + jarvis.system.notify(lang == "ru" and "Запускаю" or "Launching", match.name) + jarvis.speak((lang == "ru" and "Запускаю " or "Launching ") .. match.name .. ".", { lang = lang }) + jarvis.audio.play_ok() +else + jarvis.system.notify("Games", (lang == "ru" and "Не настроен запуск: " or "No launch info: ") .. match.name) + jarvis.audio.play_error() +end + +return { chain = false } diff --git a/resources/commands/games/list.lua b/resources/commands/games/list.lua new file mode 100644 index 0000000..37056dc --- /dev/null +++ b/resources/commands/games/list.lua @@ -0,0 +1,26 @@ +local lang = jarvis.context.language +local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" +local config_path = userprofile .. "\\Documents\\jarvis-games.json" + +if not jarvis.fs.exists(config_path) then + jarvis.system.notify("Games", lang == "ru" + and "Конфиг не создан — скажи «запусти игру» один раз" or "No config yet") + jarvis.audio.play_not_found() + return { chain = false } +end + +local names = {} +for name in jarvis.fs.read(config_path):gmatch('"name"%s*:%s*"([^"]+)"') do + table.insert(names, name) +end +if #names == 0 then + jarvis.system.notify("Games", "Empty") + jarvis.audio.play_not_found() + return { chain = false } +end + +local body = table.concat(names, ", ") +jarvis.system.notify((lang == "ru" and "Игр: " or "Games: ") .. #names, body) +jarvis.speak((lang == "ru" and "В библиотеке: " or "Library: ") .. body, { lang = lang }) +jarvis.audio.play_ok() +return { chain = false } diff --git a/resources/commands/mouse/_mouse_helper.ps1 b/resources/commands/mouse/_mouse_helper.ps1 new file mode 100644 index 0000000..25374e7 --- /dev/null +++ b/resources/commands/mouse/_mouse_helper.ps1 @@ -0,0 +1,33 @@ +param( + [Parameter(Mandatory=$true)] + [ValidateSet("left","right","middle","double","scroll_up","scroll_down")] + [string]$Action +) + +Add-Type -Name MouseM -Namespace Win32 -MemberDefinition @' +[DllImport("user32.dll")] +public static extern void mouse_event(uint flags, int dx, int dy, int data, System.UIntPtr extra); +'@ + +$LEFTDOWN = 0x0002 +$LEFTUP = 0x0004 +$RIGHTDOWN = 0x0008 +$RIGHTUP = 0x0010 +$MIDDOWN = 0x0020 +$MIDUP = 0x0040 +$WHEEL = 0x0800 + +function Click([uint32]$down, [uint32]$up) { + [Win32.MouseM]::mouse_event($down, 0, 0, 0, [UIntPtr]::Zero) + Start-Sleep -Milliseconds 30 + [Win32.MouseM]::mouse_event($up, 0, 0, 0, [UIntPtr]::Zero) +} + +switch ($Action) { + "left" { Click $LEFTDOWN $LEFTUP } + "right" { Click $RIGHTDOWN $RIGHTUP } + "middle" { Click $MIDDOWN $MIDUP } + "double" { Click $LEFTDOWN $LEFTUP; Start-Sleep -Milliseconds 50; Click $LEFTDOWN $LEFTUP } + "scroll_up" { [Win32.MouseM]::mouse_event($WHEEL, 0, 0, 360, [UIntPtr]::Zero) } + "scroll_down" { [Win32.MouseM]::mouse_event($WHEEL, 0, 0, -360, [UIntPtr]::Zero) } +} diff --git a/resources/commands/mouse/command.toml b/resources/commands/mouse/command.toml new file mode 100644 index 0000000..8ff9f39 --- /dev/null +++ b/resources/commands/mouse/command.toml @@ -0,0 +1,63 @@ +[[commands]] +id = "mouse_left_click" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["клик", "кликни", "нажми мышкой"] +en = ["click", "left click"] +ua = ["клік", "клікни"] + + +[[commands]] +id = "mouse_right_click" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["правый клик", "клик правой", "правой кнопкой"] +en = ["right click"] +ua = ["правий клік"] + + +[[commands]] +id = "mouse_double_click" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["двойной клик", "дабл клик", "двойное нажатие"] +en = ["double click"] +ua = ["подвійний клік"] + + +[[commands]] +id = "mouse_scroll_down" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["промотай вниз", "прокрути вниз", "вниз промотай"] +en = ["scroll down"] +ua = ["прокрути вниз"] + + +[[commands]] +id = "mouse_scroll_up" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["промотай вверх", "прокрути вверх", "наверх промотай"] +en = ["scroll up"] +ua = ["прокрути вгору"] diff --git a/resources/commands/mouse/dispatch.lua b/resources/commands/mouse/dispatch.lua new file mode 100644 index 0000000..1107005 --- /dev/null +++ b/resources/commands/mouse/dispatch.lua @@ -0,0 +1,31 @@ +local cmd_id = jarvis.context.command_id +local helper = jarvis.context.command_path .. "\\_mouse_helper.ps1" + +local actions = { + mouse_left_click = "left", + mouse_right_click = "right", + mouse_middle_click = "middle", + mouse_double_click = "double", + mouse_scroll_up = "scroll_up", + mouse_scroll_down = "scroll_down", +} + +local action = actions[cmd_id] +if not action then + jarvis.audio.play_error() + return { chain = false } +end + +local res = jarvis.system.exec(string.format( + 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action %s', + helper, action +)) + +if res.success then + jarvis.audio.play_ok() +else + jarvis.log("error", "mouse " .. action .. " failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() +end + +return { chain = false } diff --git a/resources/commands/random_choice/command.toml b/resources/commands/random_choice/command.toml new file mode 100644 index 0000000..79adcd7 --- /dev/null +++ b/resources/commands/random_choice/command.toml @@ -0,0 +1,23 @@ +[[commands]] +id = "random_choice" +type = "lua" +script = "pick.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = [ + "выбери из", + "выбери", + "решай за меня", + "выбрать", +] +en = [ + "pick from", + "choose from", + "decide between", +] +ua = [ + "обери з", + "вибери", +] diff --git a/resources/commands/random_choice/pick.lua b/resources/commands/random_choice/pick.lua new file mode 100644 index 0000000..f759079 --- /dev/null +++ b/resources/commands/random_choice/pick.lua @@ -0,0 +1,45 @@ +local lang = jarvis.context.language +local phrase = (jarvis.context.phrase or ""):lower() + +local triggers = { "выбери из", "выбери", "решай за меня", "выбрать", + "pick from", "choose from", "decide between", + "обери з", "вибери" } +local rest = phrase +for _, t in ipairs(triggers) do + local s, e = string.find(rest, t, 1, true) + if s == 1 then + rest = rest:sub(e + 1) + break + end +end +rest = rest:gsub("^%s+", ""):gsub("%s+$", "") + +local separators = { " или ", " or ", ", ", " либо ", " чи " } +local options = nil +for _, sep in ipairs(separators) do + local pattern = sep:gsub("([%-%.%+%[%]%(%)%$%^%%%?%*])", "%%%1") + if rest:find(pattern) then + options = {} + for token in (rest .. sep):gmatch("(.-)" .. pattern) do + token = token:gsub("^%s+", ""):gsub("%s+$", "") + if token ~= "" then table.insert(options, token) end + end + break + end +end + +if not options or #options < 2 then + jarvis.system.notify("Choice", lang == "ru" + and "Скажи: выбери из X или Y или Z" + or "Say: pick from X or Y or Z") + jarvis.audio.play_error() + return { chain = false } +end + +math.randomseed(os.time() + math.floor(jarvis.context.time.second * 7919)) +local pick = options[math.random(1, #options)] + +jarvis.system.notify(lang == "ru" and "Выбираю" or "Choice", pick) +jarvis.speak((lang == "ru" and "Я выбираю " or "I pick ") .. pick, { lang = lang }) +jarvis.audio.play_ok() +return { chain = false }