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,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 = [
"нагадай через",
"постав нагадування через",
"таймер на",
]

View file

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