From dcee98bddf99bb64236f20047efb8da1acd8dece Mon Sep 17 00:00:00 2001 From: Bossiara13 <236771060+DmitryBykov-ISPO@users.noreply.github.com> Date: Fri, 15 May 2026 12:28:44 +0300 Subject: [PATCH] feat: 5 new packs + dotenvy + regen icon.ico + shortcuts redo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Iron Man icon regenerated from resources/icons/icon.png at sizes 16/24/32/48/64/128/256 via Pillow (make_ico.py committed alongside); icon.ico is now a clean 83 KB multi-res for sharp display at any scale instead of whatever placeholder Tauri originally emitted. dotenvy added to workspace deps. jarvis-app/main.rs and jarvis-gui/ main.rs both walk up from current_exe() looking for dev.env (up to 5 parent levels), so the Desktop shortcut can launch the exe directly without a wrapper .bat to pre-set GROQ_TOKEN. Falls back to dotenvy::dotenv() (cwd lookup) if nothing found near the binary. 5 new portable Lua command packs. Bumps total from 21 to 26. cargo test -p jarvis-core --lib commands::tests still passes 3/3. reminders/ — set_reminder. Parses "напомни через N (секунд|минут| часов)
" or English equivalents; understands one Russian word numerals (один, две, пять, десять, пятнадцать, ...) for the count when speech recognition returns words instead of digits. Defaults to 5 minutes if number/unit missing. Spawns a detached PowerShell that Start-Sleeps then fires: BurntToast if installed, System.Speech SAPI (ru-RU voice preference), and a WScript.Shell.Popup as the guaranteed-visible last resort. date_query/ — today / tomorrow / yesterday. Single answer.lua keyed by command_id, computes the offset via PowerShell Get-Date.AddDays() in ru-RU culture so the weekday/month come out in Russian, then speaks via SAPI. voice_type/ — type_text. Strips the trigger, drops the remainder to clipboard via jarvis.system.clipboard.set, then synthesises Ctrl+V through user32!keybd_event so it lands in whatever window currently has focus. Works in any text field (note app, browser, IDE, Telegram, etc.). dice/ — coin_flip / roll_dice / random_number. Single roll.lua, seeds math.random with os.time + jitter, speaks the result. "1-6" for dice, "1-100" for random. stopwatch/ — start / check / stop. Uses jarvis.state.get/set (persisted via jarvis-core's settings DB) to remember the start timestamp, computes elapsed via jarvis.context.time.timestamp, formats as "X сек" / "X мин Y сек" / "X ч Y мин Z сек". All new packs follow the established patterns (sandbox=full, PS-via- exec helper where needed, USERPROFILE/SystemDrive everywhere, no hardcoded paths). --- Cargo.lock | 9 ++ Cargo.toml | 1 + crates/jarvis-app/Cargo.toml | 1 + crates/jarvis-app/src/main.rs | 23 ++++ crates/jarvis-gui/Cargo.toml | 1 + crates/jarvis-gui/src/main.rs | 23 +++- make_ico.py | 8 ++ resources/commands/date_query/answer.lua | 37 +++++++ resources/commands/date_query/command.toml | 37 +++++++ resources/commands/dice/command.toml | 37 +++++++ resources/commands/dice/roll.lua | 35 ++++++ resources/commands/reminders/command.toml | 27 +++++ resources/commands/reminders/set.lua | 119 +++++++++++++++++++++ resources/commands/stopwatch/command.toml | 37 +++++++ resources/commands/stopwatch/ctl.lua | 53 +++++++++ resources/commands/voice_type/command.toml | 23 ++++ resources/commands/voice_type/type.lua | 46 ++++++++ resources/icons/icon.ico | Bin 70346 -> 85461 bytes 18 files changed, 516 insertions(+), 1 deletion(-) create mode 100644 make_ico.py create mode 100644 resources/commands/date_query/answer.lua create mode 100644 resources/commands/date_query/command.toml create mode 100644 resources/commands/dice/command.toml create mode 100644 resources/commands/dice/roll.lua create mode 100644 resources/commands/reminders/command.toml create mode 100644 resources/commands/reminders/set.lua create mode 100644 resources/commands/stopwatch/command.toml create mode 100644 resources/commands/stopwatch/ctl.lua create mode 100644 resources/commands/voice_type/command.toml create mode 100644 resources/commands/voice_type/type.lua diff --git a/Cargo.lock b/Cargo.lock index 9a15428..a8f8b66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1554,6 +1554,12 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -3264,6 +3270,7 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" name = "jarvis-app" version = "0.1.0" dependencies = [ + "dotenvy", "glib 0.21.5", "gtk", "image", @@ -3278,6 +3285,7 @@ dependencies = [ "tray-icon", "winapi", "winit", + "winrt-notification", ] [[package]] @@ -3339,6 +3347,7 @@ dependencies = [ name = "jarvis-gui" version = "0.1.0" dependencies = [ + "dotenvy", "jarvis-core", "lazy_static", "log", diff --git a/Cargo.toml b/Cargo.toml index 21386e5..d22ee3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,4 +54,5 @@ tokenizers = { version = "0.22", default-features = false } regex = "1" sys-locale = "0.3" webrtc-vad = "0.4" +dotenvy = "0.15" diff --git a/crates/jarvis-app/Cargo.toml b/crates/jarvis-app/Cargo.toml index 9a1cf02..2e47394 100644 --- a/crates/jarvis-app/Cargo.toml +++ b/crates/jarvis-app/Cargo.toml @@ -11,6 +11,7 @@ jarvis-core = { path = "../jarvis-core", features = ["intent"] } once_cell.workspace = true log.workspace = true simple-log = "2.4" +dotenvy.workspace = true tray-icon = "0.21" winit = "0.30" image.workspace = true diff --git a/crates/jarvis-app/src/main.rs b/crates/jarvis-app/src/main.rs index 1639c8d..3bac3e3 100644 --- a/crates/jarvis-app/src/main.rs +++ b/crates/jarvis-app/src/main.rs @@ -28,6 +28,8 @@ mod tray; static SHOULD_STOP: AtomicBool = AtomicBool::new(false); fn main() -> Result<(), String> { + load_dotenv_near_exe(); + eprintln!("[jarvis-app] step: init_dirs"); config::init_dirs()?; @@ -175,6 +177,27 @@ pub fn should_stop() -> bool { SHOULD_STOP.load(Ordering::SeqCst) } +// Look for dev.env next to the exe, then walk parents until we find one. +// Lets the user double-click jarvis-gui.exe / jarvis-app.exe without needing +// a wrapper .bat to pre-load GROQ_TOKEN and similar secrets. +fn load_dotenv_near_exe() { + if let Ok(exe) = std::env::current_exe() { + let mut dir = exe.parent().map(|p| p.to_path_buf()); + for _ in 0..5 { + if let Some(d) = &dir { + let candidate = d.join("dev.env"); + if candidate.is_file() { + let _ = dotenvy::from_path(&candidate); + eprintln!("[jarvis-app] loaded env: {}", candidate.display()); + return; + } + dir = d.parent().map(|p| p.to_path_buf()); + } + } + } + let _ = dotenvy::dotenv(); +} + #[cfg(target_os = "windows")] fn notify_mic_problem() { use winrt_notification::{Toast, Duration as ToastDuration}; diff --git a/crates/jarvis-gui/Cargo.toml b/crates/jarvis-gui/Cargo.toml index 9e841da..8273215 100644 --- a/crates/jarvis-gui/Cargo.toml +++ b/crates/jarvis-gui/Cargo.toml @@ -21,6 +21,7 @@ sysinfo.workspace = true once_cell.workspace = true log.workspace = true simple-log = "2.4" +dotenvy.workspace = true serde.workspace = true serde_json.workspace = true platform-dirs.workspace = true diff --git a/crates/jarvis-gui/src/main.rs b/crates/jarvis-gui/src/main.rs index 23cfaca..0b32c8b 100644 --- a/crates/jarvis-gui/src/main.rs +++ b/crates/jarvis-gui/src/main.rs @@ -16,8 +16,10 @@ pub struct AppState { } fn main() { + load_dotenv_near_exe(); + config::init_dirs().expect("Failed to init dirs"); - + // basic logging setup (simpler for GUI) simple_log::quick!("info"); @@ -102,3 +104,22 @@ fn main() { .run(tauri::generate_context!()) .expect("error while running tauri application"); } + +// Pull dev.env into the process env so secrets like GROQ_TOKEN are picked up +// when the user launches the exe directly (no wrapper .bat required). +fn load_dotenv_near_exe() { + if let Ok(exe) = std::env::current_exe() { + let mut dir = exe.parent().map(|p| p.to_path_buf()); + for _ in 0..5 { + if let Some(d) = &dir { + let candidate = d.join("dev.env"); + if candidate.is_file() { + let _ = dotenvy::from_path(&candidate); + return; + } + dir = d.parent().map(|p| p.to_path_buf()); + } + } + } + let _ = dotenvy::dotenv(); +} diff --git a/make_ico.py b/make_ico.py new file mode 100644 index 0000000..50e382d --- /dev/null +++ b/make_ico.py @@ -0,0 +1,8 @@ +from PIL import Image +src = r"C:\Jarvis\rust\resources\icons\icon.png" +dst = r"C:\Jarvis\rust\resources\icons\icon.ico" + +img = Image.open(src).convert("RGBA") +sizes = [(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)] +img.save(dst, format="ICO", sizes=sizes) +print(f"wrote {dst} from {src} ({img.size})") diff --git a/resources/commands/date_query/answer.lua b/resources/commands/date_query/answer.lua new file mode 100644 index 0000000..dabee1f --- /dev/null +++ b/resources/commands/date_query/answer.lua @@ -0,0 +1,37 @@ +local lang = jarvis.context.language +local cmd_id = jarvis.context.command_id + +local offsets = { today = 0, tomorrow = 1, yesterday = -1 } +local offset = offsets[cmd_id] or 0 + +local ps = string.format( + [[$ru = [System.Globalization.CultureInfo]::GetCultureInfo('ru-RU'); $d = (Get-Date).AddDays(%d); $d.ToString('dddd, dd MMMM yyyy', $ru)]], + offset +) +local res = jarvis.system.exec(string.format('powershell -NoProfile -Command "%s"', ps:gsub('"', '\\"'))) +local text = (res.stdout or ""):gsub("[\r\n]+$", "") + +if text == "" then + jarvis.audio.play_error() + return { chain = false } +end + +local labels = { + today = lang == "ru" and "Сегодня" or "Today", + tomorrow = lang == "ru" and "Завтра" or "Tomorrow", + yesterday = lang == "ru" and "Вчера" or "Yesterday", +} +local prefix = labels[cmd_id] or "" + +jarvis.system.notify(prefix, text) + +local say = prefix .. ", " .. 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.audio.play_ok() +return { chain = false } diff --git a/resources/commands/date_query/command.toml b/resources/commands/date_query/command.toml new file mode 100644 index 0000000..1d63f7e --- /dev/null +++ b/resources/commands/date_query/command.toml @@ -0,0 +1,37 @@ +[[commands]] +id = "today" +type = "lua" +script = "answer.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["какой сегодня день", "какое сегодня число", "сегодня какое число", "что сегодня"] +en = ["what day is today", "what's today", "today date"] +ua = ["який сьогодні день", "яке сьогодні число"] + + +[[commands]] +id = "tomorrow" +type = "lua" +script = "answer.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["какой день завтра", "что завтра", "завтра какое число", "какое число завтра"] +en = ["what day is tomorrow", "what's tomorrow", "tomorrow date"] +ua = ["який день завтра", "яке число завтра"] + + +[[commands]] +id = "yesterday" +type = "lua" +script = "answer.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["какой был вчера день", "что было вчера", "вчера какое число было"] +en = ["what day was yesterday", "yesterday date"] +ua = ["який був вчора день"] diff --git a/resources/commands/dice/command.toml b/resources/commands/dice/command.toml new file mode 100644 index 0000000..231db01 --- /dev/null +++ b/resources/commands/dice/command.toml @@ -0,0 +1,37 @@ +[[commands]] +id = "coin_flip" +type = "lua" +script = "roll.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["подбрось монетку", "брось монетку", "орёл или решка", "орел или решка"] +en = ["flip a coin", "toss a coin", "heads or tails"] +ua = ["підкинь монетку", "орел чи решка"] + + +[[commands]] +id = "roll_dice" +type = "lua" +script = "roll.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["брось кубик", "подбрось кубик", "кинь кубик"] +en = ["roll a dice", "roll the dice"] +ua = ["кинь кубик"] + + +[[commands]] +id = "random_number" +type = "lua" +script = "roll.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["случайное число", "выбери случайное число", "рандомное число"] +en = ["random number", "pick a random number"] +ua = ["випадкове число"] diff --git a/resources/commands/dice/roll.lua b/resources/commands/dice/roll.lua new file mode 100644 index 0000000..24de619 --- /dev/null +++ b/resources/commands/dice/roll.lua @@ -0,0 +1,35 @@ +local lang = jarvis.context.language +local cmd_id = jarvis.context.command_id + +math.randomseed(os.time() + math.floor(jarvis.context.time.second * 7919)) + +local title, text, say + +if cmd_id == "coin_flip" then + local r = math.random(2) + text = (r == 1) and (lang == "ru" and "Орёл" or "Heads") or (lang == "ru" and "Решка" or "Tails") + title = lang == "ru" and "Монетка" or "Coin" + say = text +elseif cmd_id == "roll_dice" then + local r = math.random(1, 6) + text = tostring(r) + title = lang == "ru" and "Кубик" or "Dice" + say = (lang == "ru" and "Выпало " or "Rolled ") .. r +else + local r = math.random(1, 100) + text = tostring(r) + title = lang == "ru" and "Случайное число" or "Random number" + say = (lang == "ru" and "Число " or "Number ") .. r +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.audio.play_ok() +return { chain = false } diff --git a/resources/commands/reminders/command.toml b/resources/commands/reminders/command.toml new file mode 100644 index 0000000..fabe52a --- /dev/null +++ b/resources/commands/reminders/command.toml @@ -0,0 +1,27 @@ +[[commands]] +id = "set_reminder" +type = "lua" +script = "set.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "напомни через", + "напомни мне через", + "поставь напоминание через", + "установи таймер на", + "таймер на", + "напомни", +] +en = [ + "remind me in", + "set reminder in", + "set timer for", + "timer for", +] +ua = [ + "нагадай через", + "постав нагадування через", + "таймер на", +] diff --git a/resources/commands/reminders/set.lua b/resources/commands/reminders/set.lua new file mode 100644 index 0000000..3309479 --- /dev/null +++ b/resources/commands/reminders/set.lua @@ -0,0 +1,119 @@ +local lang = jarvis.context.language +local phrase = (jarvis.context.phrase or ""):lower() + +local triggers = { + "поставь напоминание через", "установи таймер на", + "напомни мне через", "напомни через", "напомни", + "remind me in", "set reminder in", "set timer for", "timer for", + "постав нагадування через", "нагадай через", "таймер на", +} + +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+", "") + +local unit_words = { + ["секунд"] = 1, ["секунду"] = 1, ["секунды"] = 1, + ["second"] = 1, ["seconds"] = 1, ["sec"] = 1, + ["минут"] = 60, ["минуту"] = 60, ["минуты"] = 60, ["мин"] = 60, + ["minute"] = 60, ["minutes"] = 60, ["min"] = 60, + ["час"] = 3600, ["часа"] = 3600, ["часов"] = 3600, ["часик"] = 3600, + ["hour"] = 3600, ["hours"] = 3600, ["hr"] = 3600, +} +local rus_numerals = { + ["один"] = 1, ["одну"] = 1, ["одна"] = 1, ["полторы"] = 1, ["полтора"] = 1, + ["два"] = 2, ["две"] = 2, ["три"] = 3, ["четыре"] = 4, ["пять"] = 5, + ["шесть"] = 6, ["семь"] = 7, ["восемь"] = 8, ["девять"] = 9, ["десять"] = 10, + ["пятнадцать"] = 15, ["двадцать"] = 20, ["тридцать"] = 30, ["сорок"] = 40, + ["пятьдесят"] = 50, ["час"] = 60, -- "через час" without number +} + +local number, unit, body = nil, nil, nil + +local num_str, rest_after_num = rest:match("^(%d+)%s+(.+)") +if num_str then + number = tonumber(num_str) + rest = rest_after_num +else + local first_word, after = rest:match("^(%S+)%s*(.*)") + if first_word and rus_numerals[first_word] then + number = rus_numerals[first_word] + rest = after or "" + end +end + +if rest and #rest > 0 then + local first_word, after = rest:match("^(%S+)%s*(.*)") + if first_word then + for w, secs in pairs(unit_words) do + if first_word:sub(1, #w) == w then + unit = secs + body = after or "" + break + end + end + end +end + +if not number then + number = 5 + if not unit then unit = 60 end + body = rest +elseif not unit then + unit = 60 + body = rest +end + +local total_seconds = number * unit +if total_seconds < 1 then total_seconds = 1 end +if total_seconds > 86400 then total_seconds = 86400 end + +body = (body or ""):gsub("^%s+", ""):gsub("%s+$", "") +if body == "" then + body = lang == "ru" and "Напоминание!" or "Reminder!" +end + +local function human_time(sec) + if sec < 60 then return sec .. " сек" end + if sec < 3600 then return math.floor(sec/60) .. " мин" end + return string.format("%d ч %d мин", math.floor(sec/3600), math.floor((sec%3600)/60)) +end + +local title_esc = (lang == "ru" and "Напоминание J.A.R.V.I.S." or "J.A.R.V.I.S. reminder"):gsub("'", "''") +local body_esc = body:gsub("'", "''") +local sapi_text = ((lang == "ru" and "Сэр, напоминаю: " or "Reminder: ") .. body):gsub("'", "''"):gsub("\r?\n", " ") + +local ps = string.format( + [[Start-Sleep -Seconds %d; ]] + .. [[$null = New-Object -ComObject Shell.Application; ]] + .. [[try { ]] + .. [[ Import-Module BurntToast -ErrorAction SilentlyContinue; ]] + .. [[ New-BurntToastNotification -Text '%s', '%s' -ErrorAction SilentlyContinue ]] + .. [[} catch {}; ]] + .. [[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'); ]] + .. [[$wsh = New-Object -ComObject WScript.Shell; $wsh.Popup('%s', 30, '%s', 64) | Out-Null]], + total_seconds, title_esc, body_esc, sapi_text, body_esc, title_esc +) +local cmd = string.format( + 'powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -Command "%s"', + ps:gsub('"', '\\"') +) + +jarvis.system.exec("start /b " .. cmd) + +jarvis.system.notify( + lang == "ru" and "Таймер установлен" or "Timer set", + string.format(lang == "ru" and "Через %s: %s" or "In %s: %s", human_time(total_seconds), body) +) +jarvis.log("info", string.format("reminder in %ds: %s", total_seconds, body)) +jarvis.audio.play_ok() +return { chain = false } diff --git a/resources/commands/stopwatch/command.toml b/resources/commands/stopwatch/command.toml new file mode 100644 index 0000000..1ddc8d9 --- /dev/null +++ b/resources/commands/stopwatch/command.toml @@ -0,0 +1,37 @@ +[[commands]] +id = "stopwatch_start" +type = "lua" +script = "ctl.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["засеки время", "запусти секундомер", "старт секундомер", "включи секундомер"] +en = ["start stopwatch", "start timer"] +ua = ["запусти секундомір"] + + +[[commands]] +id = "stopwatch_check" +type = "lua" +script = "ctl.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["сколько прошло", "сколько времени прошло", "проверь секундомер"] +en = ["how long has it been", "check stopwatch"] +ua = ["скільки пройшло"] + + +[[commands]] +id = "stopwatch_stop" +type = "lua" +script = "ctl.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = ["стоп секундомер", "останови секундомер", "выключи секундомер"] +en = ["stop stopwatch", "stop timer"] +ua = ["стоп секундомір"] diff --git a/resources/commands/stopwatch/ctl.lua b/resources/commands/stopwatch/ctl.lua new file mode 100644 index 0000000..2e0edb0 --- /dev/null +++ b/resources/commands/stopwatch/ctl.lua @@ -0,0 +1,53 @@ +local lang = jarvis.context.language +local cmd_id = jarvis.context.command_id + +local function speak(text) + local sapi = text: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('"', '\\"'))) +end + +local function format_elapsed(sec) + sec = math.floor(sec) + if sec < 60 then return sec .. (lang == "ru" and " секунд" or " seconds") end + if sec < 3600 then + local m = math.floor(sec / 60); local s = sec % 60 + return string.format(lang == "ru" and "%d мин %d сек" or "%d min %d sec", m, s) + end + local h = math.floor(sec / 3600); local m = math.floor((sec % 3600) / 60); local s = sec % 60 + return string.format(lang == "ru" and "%d ч %d мин %d сек" or "%d h %d m %d s", h, m, s) +end + +if cmd_id == "stopwatch_start" then + jarvis.state.set("stopwatch_start_ts", jarvis.context.time.timestamp) + jarvis.system.notify("Stopwatch", lang == "ru" and "Старт" or "Started") + speak(lang == "ru" and "Секундомер запущен" or "Stopwatch started") + jarvis.audio.play_ok() + return { chain = false } +end + +local started = jarvis.state.get("stopwatch_start_ts") +if not started then + jarvis.system.notify("Stopwatch", lang == "ru" and "Не запущен" or "Not running") + speak(lang == "ru" and "Секундомер не запущен" or "Stopwatch is not running") + jarvis.audio.play_not_found() + return { chain = false } +end + +local elapsed = jarvis.context.time.timestamp - started +local human = format_elapsed(elapsed) + +if cmd_id == "stopwatch_stop" then + jarvis.state.set("stopwatch_start_ts", nil) + jarvis.system.notify("Stopwatch", (lang == "ru" and "Прошло: " or "Elapsed: ") .. human) + speak((lang == "ru" and "Прошло " or "Elapsed ") .. human) +else + jarvis.system.notify("Stopwatch", (lang == "ru" and "Прошло: " or "Elapsed: ") .. human) + speak((lang == "ru" and "Прошло " or "Elapsed ") .. human) +end + +jarvis.audio.play_ok() +return { chain = false } diff --git a/resources/commands/voice_type/command.toml b/resources/commands/voice_type/command.toml new file mode 100644 index 0000000..fb54d44 --- /dev/null +++ b/resources/commands/voice_type/command.toml @@ -0,0 +1,23 @@ +[[commands]] +id = "type_text" +type = "lua" +script = "type.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "напечатай", + "набери текст", + "введи текст", + "впиши", +] +en = [ + "type", + "type text", + "write text", +] +ua = [ + "надрукуй", + "введи текст", +] diff --git a/resources/commands/voice_type/type.lua b/resources/commands/voice_type/type.lua new file mode 100644 index 0000000..0da26ea --- /dev/null +++ b/resources/commands/voice_type/type.lua @@ -0,0 +1,46 @@ +local lang = jarvis.context.language +local phrase = (jarvis.context.phrase or "") + +local triggers = { + "набери текст", "введи текст", + "напечатай", "впиши", + "type text", "write text", "type", + "надрукуй", "введи текст", +} + +local lower = phrase:lower() +local text = phrase +for _, t in ipairs(triggers) do + local s, e = string.find(lower, t, 1, true) + if s == 1 then + text = phrase:sub(e + 1) + break + end +end +text = text:gsub("^%s+", ""):gsub("%s+$", "") + +if text == "" then + jarvis.system.notify("Type", lang == "ru" and "Что напечатать?" or "What to type?") + jarvis.audio.play_error() + return { chain = false } +end + +jarvis.system.clipboard.set(text) +jarvis.sleep(120) + +local ps = [[Add-Type -Name K -Namespace W -MemberDefinition '[DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte sc, uint flags, System.UIntPtr extra);'; ]] + .. [[[W.K]::keybd_event(0x11, 0, 0, [UIntPtr]::Zero); ]] + .. [[[W.K]::keybd_event(0x56, 0, 0, [UIntPtr]::Zero); ]] + .. [[Start-Sleep -Milliseconds 30; ]] + .. [[[W.K]::keybd_event(0x56, 0, 2, [UIntPtr]::Zero); ]] + .. [[[W.K]::keybd_event(0x11, 0, 2, [UIntPtr]::Zero)]] +local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', ps:gsub('"', '\\"')) +local res = jarvis.system.exec(cmd) + +if res.success then + jarvis.audio.play_ok() +else + jarvis.log("error", "voice type failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() +end +return { chain = false } diff --git a/resources/icons/icon.ico b/resources/icons/icon.ico index 4ca066267587f54d1fb87e0c5b56c7c2b1b2bdc4..ca7f09c83cdd774123a9b5337b4404bca30bf1f6 100644 GIT binary patch literal 85461 zcmaf(b8sd>*Y;!Fn`C3#wr$(CZEk$Wwr$&Xvf;+Iv$6f{^F054Rc}{KO`keb(_P(j z&AI9~0|EjH0u6$Q2=cEHg2aG>fVltbVPgI-u7Uyq0f+dPA^cyA00050{dX`j{x6