From 16ef413962d21d1b3e8399af81561d53d827a96a Mon Sep 17 00:00:00 2001 From: Bossiara13 <236771060+DmitryBykov-ISPO@users.noreply.github.com> Date: Fri, 15 May 2026 11:27:02 +0300 Subject: [PATCH] =?UTF-8?q?feat(commands):=20tier-1=20PC=20tools=20?= =?UTF-8?q?=E2=80=94=20window=20mgmt,=20clipboard,=20notes,=20process=20ki?= =?UTF-8?q?ller,=20web=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five new portable Lua command packs. Bumps the total from 11 to 16 packs. All sandbox=full, all dispatched via jarvis.system.exec or jarvis.fs / jarvis.system APIs, no new Rust code, post_build.py --sync copies them under target/. cargo test -p jarvis-core --lib commands::tests passes 3/3 against all 16. window/ — 8 commands, single dispatch.lua keyed by jarvis.context.command_id: show_desktop / maximize_window / minimize_window / snap_left / snap_right / close_window / restore_all / task_view. _window_helper.ps1 P/Invokes user32!keybd_event for arbitrary combos like "win+d", "alt+f4", "win+shift+m". Uses one shared script per pack — much less duplication than the volume/ approach. clipboard_read/ — read_clipboard. Pulls jarvis.system.clipboard.get(), caps preview at 400 chars, then shells PowerShell System.Speech to actually speak it aloud (auto-picks the first ru-RU voice if present). Also fires a notify with a 120-char snippet. notes/ — add_note + open_notes. add_note strips the trigger phrase from the recognized voice, appends "[YYYY-MM-DD HH:MM] body\n" to %USERPROFILE%\Documents\jarvis-notes.txt via jarvis.fs.append (creates the file on first call). open_notes opens the file in the default editor via jarvis.system.open. Trigger list covers RU/EN/UA variants of "запиши" / "запомни" / "write down" / etc. process_kill/ — kill_process. Trigger-strips "закрой программу" / "убей процесс" / etc., then runs the remainder through an alias map (хром→ chrome, телега→telegram, спотифай→spotify, edge/edge/едж→msedge etc., 20+ entries) before invoking taskkill /F /IM. Notifies success/not-found. websearch/ — search_google / search_youtube / search_wiki / search_yandex. Single dispatch.lua reads jarvis.context.command_id to pick the base URL, URL-encodes the query (Lua %02X gsub, not relying on jarvis.http), opens the resulting link via jarvis.system.open (system default browser). Portability check: every helper resolves paths via $env:USERPROFILE / jarvis.context.command_path / jarvis.system.env — no hardcoded C:\ refs. --- .../commands/clipboard_read/command.toml | 11 ++ resources/commands/clipboard_read/read.lua | 33 ++++++ resources/commands/notes/add.lua | 56 ++++++++++ resources/commands/notes/command.toml | 24 +++++ resources/commands/notes/open.lua | 16 +++ resources/commands/process_kill/command.toml | 25 +++++ resources/commands/process_kill/kill.lua | 68 ++++++++++++ resources/commands/websearch/command.toml | 71 ++++++++++++ resources/commands/websearch/dispatch.lua | 61 +++++++++++ resources/commands/window/_window_helper.ps1 | 42 ++++++++ resources/commands/window/command.toml | 102 ++++++++++++++++++ resources/commands/window/dispatch.lua | 36 +++++++ 12 files changed, 545 insertions(+) create mode 100644 resources/commands/clipboard_read/command.toml create mode 100644 resources/commands/clipboard_read/read.lua create mode 100644 resources/commands/notes/add.lua create mode 100644 resources/commands/notes/command.toml create mode 100644 resources/commands/notes/open.lua create mode 100644 resources/commands/process_kill/command.toml create mode 100644 resources/commands/process_kill/kill.lua create mode 100644 resources/commands/websearch/command.toml create mode 100644 resources/commands/websearch/dispatch.lua create mode 100644 resources/commands/window/_window_helper.ps1 create mode 100644 resources/commands/window/command.toml create mode 100644 resources/commands/window/dispatch.lua diff --git a/resources/commands/clipboard_read/command.toml b/resources/commands/clipboard_read/command.toml new file mode 100644 index 0000000..981b43e --- /dev/null +++ b/resources/commands/clipboard_read/command.toml @@ -0,0 +1,11 @@ +[[commands]] +id = "read_clipboard" +type = "lua" +script = "read.lua" +sandbox = "full" +timeout = 10000 + +[commands.phrases] +ru = ["прочитай буфер", "что в буфере", "прочитай из буфера", "озвучь буфер"] +en = ["read clipboard", "what is in clipboard", "speak clipboard"] +ua = ["прочитай буфер", "що в буфері"] diff --git a/resources/commands/clipboard_read/read.lua b/resources/commands/clipboard_read/read.lua new file mode 100644 index 0000000..fb464b6 --- /dev/null +++ b/resources/commands/clipboard_read/read.lua @@ -0,0 +1,33 @@ +local lang = jarvis.context.language +local raw = jarvis.system.clipboard.get() or "" +local text = raw:gsub("^%s+", ""):gsub("%s+$", "") + +if text == "" then + jarvis.system.notify( + lang == "ru" and "Буфер" or "Clipboard", + lang == "ru" and "Пусто" or "Empty" + ) + jarvis.audio.play_not_found() + return { chain = false } +end + +local preview_chars = 400 +local to_speak = text +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.audio.play_ok() +return { chain = false } diff --git a/resources/commands/notes/add.lua b/resources/commands/notes/add.lua new file mode 100644 index 0000000..26f9327 --- /dev/null +++ b/resources/commands/notes/add.lua @@ -0,0 +1,56 @@ +local lang = jarvis.context.language +local phrase = (jarvis.context.phrase or ""):lower() + +local triggers = { + "запиши заметку", "запиши идею", "сделай заметку", "запомни идею", + "запиши", "запомни", + "write down", "take a note", "remember this", "note down", + "запиши ідею", "зроби нотатку", +} + +local body = phrase +for _, t in ipairs(triggers) do + local s, e = string.find(body, t, 1, true) + if s == 1 then + body = body:sub(e + 1) + break + end +end +body = body:gsub("^[%s,:%.]+", ""):gsub("%s+$", "") + +if body == "" then + jarvis.system.notify( + lang == "ru" and "Заметка" or "Note", + lang == "ru" and "Что записать?" or "What to write?" + ) + jarvis.audio.play_error() + return { chain = false } +end + +local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" +local notes_path = userprofile .. "\\Documents\\jarvis-notes.txt" + +local t = jarvis.context.time +local stamp = string.format("%s-%s-%s %s:%s", + t.year, t.month, t.day, t.hour, t.minute) + +local line = string.format("[%s] %s\n", stamp, body) + +local ok, err = pcall(function() + jarvis.fs.append(notes_path, line) +end) + +if not ok then + jarvis.log("error", "notes append failed: " .. tostring(err)) + jarvis.system.notify("Заметка", lang == "ru" and "Не удалось записать" or "Append failed") + jarvis.audio.play_error() + return { chain = false } +end + +jarvis.log("info", "note appended: " .. body) +jarvis.system.notify( + lang == "ru" and "Заметка записана" or "Note saved", + body:sub(1, 120) +) +jarvis.audio.play_ok() +return { chain = false } diff --git a/resources/commands/notes/command.toml b/resources/commands/notes/command.toml new file mode 100644 index 0000000..63352ec --- /dev/null +++ b/resources/commands/notes/command.toml @@ -0,0 +1,24 @@ +[[commands]] +id = "add_note" +type = "lua" +script = "add.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = ["запиши", "запиши заметку", "запиши идею", "запомни", "запомни идею", "сделай заметку"] +en = ["write down", "take a note", "remember this", "note down"] +ua = ["запиши", "запиши ідею", "зроби нотатку"] + + +[[commands]] +id = "open_notes" +type = "lua" +script = "open.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["покажи заметки", "открой заметки", "что я записал"] +en = ["show notes", "open notes"] +ua = ["покажи нотатки", "відкрий нотатки"] diff --git a/resources/commands/notes/open.lua b/resources/commands/notes/open.lua new file mode 100644 index 0000000..6743ac4 --- /dev/null +++ b/resources/commands/notes/open.lua @@ -0,0 +1,16 @@ +local lang = jarvis.context.language +local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" +local notes_path = userprofile .. "\\Documents\\jarvis-notes.txt" + +if not jarvis.fs.exists(notes_path) then + jarvis.system.notify( + lang == "ru" and "Заметки" or "Notes", + lang == "ru" and "Файл пуст или не создан" or "No notes yet" + ) + jarvis.audio.play_not_found() + return { chain = false } +end + +jarvis.system.open(notes_path) +jarvis.audio.play_ok() +return { chain = false } diff --git a/resources/commands/process_kill/command.toml b/resources/commands/process_kill/command.toml new file mode 100644 index 0000000..e4b114a --- /dev/null +++ b/resources/commands/process_kill/command.toml @@ -0,0 +1,25 @@ +[[commands]] +id = "kill_process" +type = "lua" +script = "kill.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "убей процесс", + "закрой программу", + "завершить процесс", + "выруби процесс", + "kill процесс", +] +en = [ + "kill process", + "close program", + "terminate process", +] +ua = [ + "вбий процес", + "закрий програму", + "заверши процес", +] diff --git a/resources/commands/process_kill/kill.lua b/resources/commands/process_kill/kill.lua new file mode 100644 index 0000000..4d53b5e --- /dev/null +++ b/resources/commands/process_kill/kill.lua @@ -0,0 +1,68 @@ +local lang = jarvis.context.language +local phrase = (jarvis.context.phrase or ""):lower() + +local triggers = { + "завершить процесс", "выруби процесс", "убей процесс", "kill процесс", + "закрой программу", + "kill process", "close program", "terminate process", + "вбий процес", "закрий програму", "заверши процес", +} + +local name = phrase +for _, t in ipairs(triggers) do + local s, e = string.find(name, t, 1, true) + if s == 1 then + name = name:sub(e + 1) + break + end +end +name = name:gsub("^%s+", ""):gsub("%s+$", "") + +local aliases = { + ["хром"] = "chrome", ["хрома"] = "chrome", + ["спотифай"] = "spotify", + ["едж"] = "msedge", ["эдж"] = "msedge", ["edge"] = "msedge", + ["файрфокс"] = "firefox", ["фаерфокс"] = "firefox", + ["вскод"] = "code", ["vs code"] = "code", + ["дискорд"] = "discord", + ["телега"] = "telegram", ["телеграм"] = "telegram", + ["стим"] = "steam", + ["обс"] = "obs64", + ["проводник"] = "explorer", + ["блокнот"] = "notepad", + ["калькулятор"] = "calculatorapp", + ["диспетчер задач"] = "taskmgr", +} +local target = aliases[name] or name + +if target == "" then + jarvis.system.notify( + lang == "ru" and "Kill" or "Kill", + lang == "ru" and "Какой процесс?" or "Which process?" + ) + jarvis.audio.play_error() + return { chain = false } +end + +local exe = target:match("%.exe$") and target or (target .. ".exe") + +local cmd = string.format('taskkill /F /IM "%s"', exe) +local res = jarvis.system.exec(cmd) + +if res.success then + jarvis.log("info", "killed: " .. exe) + jarvis.system.notify( + lang == "ru" and "Завершён" or "Killed", + exe + ) + jarvis.audio.play_ok() +else + jarvis.log("warn", "taskkill failed for " .. exe .. ": " .. tostring(res.stderr)) + jarvis.system.notify( + lang == "ru" and "Не нашёл процесс" or "Process not found", + exe + ) + jarvis.audio.play_not_found() +end + +return { chain = false } diff --git a/resources/commands/websearch/command.toml b/resources/commands/websearch/command.toml new file mode 100644 index 0000000..46c401a --- /dev/null +++ b/resources/commands/websearch/command.toml @@ -0,0 +1,71 @@ +[[commands]] +id = "search_google" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "загугли", + "поиск в гугл", + "поищи в гугл", + "найди в гугл", + "ищи в гугл", +] +en = ["google", "search google", "google for"] +ua = ["загугли", "пошукай в гугл"] + + +[[commands]] +id = "search_youtube" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "найди на ютубе", + "поиск на ютубе", + "ищи на ютубе", + "ютуб поиск", +] +en = ["youtube search", "search youtube", "find on youtube"] +ua = ["знайди на ютубі", "пошук на ютубі"] + + +[[commands]] +id = "search_wiki" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "найди в википедии", + "поиск в википедии", + "википедия", + "вики поиск", +] +en = ["wikipedia search", "search wikipedia", "look up wikipedia"] +ua = ["знайди в вікіпедії", "пошук у вікіпедії"] + + +[[commands]] +id = "search_yandex" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "поищи в яндекс", + "найди в яндекс", + "яндекс поиск", + "пробей в яндекс", +] +en = ["yandex search", "search yandex"] +ua = ["знайди в яндекс"] diff --git a/resources/commands/websearch/dispatch.lua b/resources/commands/websearch/dispatch.lua new file mode 100644 index 0000000..8b4ea97 --- /dev/null +++ b/resources/commands/websearch/dispatch.lua @@ -0,0 +1,61 @@ +local lang = jarvis.context.language +local phrase = (jarvis.context.phrase or ""):lower() +local cmd_id = jarvis.context.command_id + +local services = { + search_google = { base = "https://www.google.com/search?q=", label = "Google" }, + search_youtube = { base = "https://www.youtube.com/results?search_query=", label = "YouTube" }, + search_wiki = { base = "https://ru.wikipedia.org/wiki/Special:Search?search=", label = "Wikipedia" }, + search_yandex = { base = "https://yandex.ru/search/?text=", label = "Yandex" }, +} + +local svc = services[cmd_id] +if not svc then + jarvis.log("error", "websearch: unknown command_id " .. tostring(cmd_id)) + jarvis.audio.play_error() + return { chain = false } +end + +local triggers = { + "загугли", "поиск в гугл", "поищи в гугл", "найди в гугл", "ищи в гугл", + "найди на ютубе", "поиск на ютубе", "ищи на ютубе", "ютуб поиск", + "найди в википедии", "поиск в википедии", "википедия", "вики поиск", + "поищи в яндекс", "найди в яндекс", "яндекс поиск", "пробей в яндекс", + "search google", "google for", "google", + "youtube search", "search youtube", "find on youtube", + "wikipedia search", "search wikipedia", "look up wikipedia", + "yandex search", "search yandex", + "загугли", "пошукай в гугл", + "знайди на ютубі", "пошук на ютубі", + "знайди в вікіпедії", "пошук у вікіпедії", + "знайди в яндекс", +} + +local q = phrase +for _, t in ipairs(triggers) do + local s, e = string.find(q, t, 1, true) + if s == 1 then + q = q:sub(e + 1) + break + end +end +q = q:gsub("^%s+", ""):gsub("%s+$", "") + +if q == "" then + jarvis.system.notify(svc.label, lang == "ru" and "Что искать?" or "What to search?") + jarvis.audio.play_error() + return { chain = false } +end + +local function url_encode(s) + return (s:gsub("([^%w%-_%.~])", function(c) + return string.format("%%%02X", string.byte(c)) + end)) +end + +local url = svc.base .. url_encode(q) +jarvis.log("info", "websearch " .. svc.label .. ": " .. q) +jarvis.system.open(url) +jarvis.system.notify(svc.label, q) +jarvis.audio.play_ok() +return { chain = false } diff --git a/resources/commands/window/_window_helper.ps1 b/resources/commands/window/_window_helper.ps1 new file mode 100644 index 0000000..522d336 --- /dev/null +++ b/resources/commands/window/_window_helper.ps1 @@ -0,0 +1,42 @@ +param( + [Parameter(Mandatory=$true)] + [string]$Combo +) + +Add-Type -Name WinKey -Namespace Win32 -MemberDefinition @' +[DllImport("user32.dll")] +public static extern void keybd_event(byte vk, byte sc, uint flags, System.UIntPtr extra); +'@ + +$VK = @{ + 'win'=0x5B; 'lwin'=0x5B; 'rwin'=0x5C; + 'alt'=0x12; 'ctrl'=0x11; 'shift'=0x10; + 'a'=0x41; 'b'=0x42; 'c'=0x43; 'd'=0x44; 'e'=0x45; 'f'=0x46; + 'g'=0x47; 'h'=0x48; 'i'=0x49; 'j'=0x4A; 'k'=0x4B; 'l'=0x4C; + 'm'=0x4D; 'n'=0x4E; 'o'=0x4F; 'p'=0x50; 'q'=0x51; 'r'=0x52; + 's'=0x53; 't'=0x54; 'u'=0x55; 'v'=0x56; 'w'=0x57; 'x'=0x58; + 'y'=0x59; 'z'=0x5A; + 'up'=0x26; 'down'=0x28; 'left'=0x25; 'right'=0x27; + 'tab'=0x09; 'esc'=0x1B; 'enter'=0x0D; 'space'=0x20; 'home'=0x24; 'end'=0x23; + 'f1'=0x70; 'f2'=0x71; 'f3'=0x72; 'f4'=0x73; 'f5'=0x74; 'f6'=0x75; + 'f7'=0x76; 'f8'=0x77; 'f9'=0x78; 'f10'=0x79; 'f11'=0x7A; 'f12'=0x7B; +} + +$parts = ($Combo.ToLower() -split '\+') | ForEach-Object { $_.Trim() } +$codes = @() +foreach ($k in $parts) { + if (-not $VK.ContainsKey($k)) { + Write-Error "Unknown key: $k (in combo: $Combo)" + exit 2 + } + $codes += [byte][int]$VK[$k] +} + +foreach ($c in $codes) { + [Win32.WinKey]::keybd_event($c, 0, 0, [UIntPtr]::Zero) +} +Start-Sleep -Milliseconds 35 +[array]::Reverse($codes) +foreach ($c in $codes) { + [Win32.WinKey]::keybd_event($c, 0, 2, [UIntPtr]::Zero) +} diff --git a/resources/commands/window/command.toml b/resources/commands/window/command.toml new file mode 100644 index 0000000..341c973 --- /dev/null +++ b/resources/commands/window/command.toml @@ -0,0 +1,102 @@ +[[commands]] +id = "show_desktop" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["покажи рабочий стол", "свернуть всё", "сверни все окна", "покажи десктоп"] +en = ["show desktop", "minimize all"] +ua = ["покажи робочий стіл", "згорни всі вікна"] + + +[[commands]] +id = "maximize_window" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["разверни окно", "развернуть окно", "на весь экран", "раскрой окно"] +en = ["maximize window", "fullscreen window"] +ua = ["розгорни вікно", "на весь екран"] + + +[[commands]] +id = "minimize_window" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["сверни окно", "свернуть окно", "убери окно"] +en = ["minimize window"] +ua = ["згорни вікно"] + + +[[commands]] +id = "snap_left" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["окно влево", "сдвинь влево", "прижми влево"] +en = ["snap left", "window left"] +ua = ["вікно вліво"] + + +[[commands]] +id = "snap_right" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["окно вправо", "сдвинь вправо", "прижми вправо"] +en = ["snap right", "window right"] +ua = ["вікно вправо"] + + +[[commands]] +id = "close_window" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["закрой окно", "закрой вкладку", "закрой текущее", "alt f4"] +en = ["close window", "close tab"] +ua = ["закрий вікно", "закрий вкладку"] + + +[[commands]] +id = "restore_all" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["восстанови окна", "верни окна", "разверни окна"] +en = ["restore windows", "restore all"] +ua = ["віднови вікна"] + + +[[commands]] +id = "task_view" +type = "lua" +script = "dispatch.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["открой все окна", "обзор окон", "таск вью", "покажи все окна"] +en = ["task view", "show all windows"] +ua = ["показати всі вікна", "огляд вікон"] diff --git a/resources/commands/window/dispatch.lua b/resources/commands/window/dispatch.lua new file mode 100644 index 0000000..9a4fdcf --- /dev/null +++ b/resources/commands/window/dispatch.lua @@ -0,0 +1,36 @@ +local helper = jarvis.context.command_path .. "\\_window_helper.ps1" +local cmd_id = jarvis.context.command_id + +local combos = { + show_desktop = "win+d", + maximize_window = "win+up", + minimize_window = "win+down", + snap_left = "win+left", + snap_right = "win+right", + close_window = "alt+f4", + restore_all = "win+shift+m", + task_view = "win+tab", +} + +local combo = combos[cmd_id] +if not combo then + jarvis.log("error", "window/dispatch.lua: unknown command_id: " .. tostring(cmd_id)) + jarvis.audio.play_error() + return { chain = false } +end + +local cmd = string.format( + 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Combo "%s"', + helper, combo +) +local res = jarvis.system.exec(cmd) + +if res.success then + jarvis.log("info", "window cmd " .. cmd_id .. " (" .. combo .. ")") + jarvis.audio.play_ok() +else + jarvis.log("error", "window cmd " .. combo .. " failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() +end + +return { chain = false }