feat: 5 new packs + dotenvy + regen icon.ico + shortcuts redo

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 (секунд|минут|
часов) <body>" 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).
This commit is contained in:
Bossiara13 2026-05-15 12:28:44 +03:00
parent ae279aeef7
commit dcee98bddf
18 changed files with 516 additions and 1 deletions

View file

@ -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 = ["стоп секундомір"]

View file

@ -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 }