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:
parent
ae279aeef7
commit
dcee98bddf
18 changed files with 516 additions and 1 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
|
@ -1554,6 +1554,12 @@ dependencies = [
|
||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenvy"
|
||||||
|
version = "0.15.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "downcast-rs"
|
name = "downcast-rs"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
|
|
@ -3264,6 +3270,7 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
name = "jarvis-app"
|
name = "jarvis-app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"dotenvy",
|
||||||
"glib 0.21.5",
|
"glib 0.21.5",
|
||||||
"gtk",
|
"gtk",
|
||||||
"image",
|
"image",
|
||||||
|
|
@ -3278,6 +3285,7 @@ dependencies = [
|
||||||
"tray-icon",
|
"tray-icon",
|
||||||
"winapi",
|
"winapi",
|
||||||
"winit",
|
"winit",
|
||||||
|
"winrt-notification",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3339,6 +3347,7 @@ dependencies = [
|
||||||
name = "jarvis-gui"
|
name = "jarvis-gui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"dotenvy",
|
||||||
"jarvis-core",
|
"jarvis-core",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
|
|
|
||||||
|
|
@ -54,4 +54,5 @@ tokenizers = { version = "0.22", default-features = false }
|
||||||
regex = "1"
|
regex = "1"
|
||||||
sys-locale = "0.3"
|
sys-locale = "0.3"
|
||||||
webrtc-vad = "0.4"
|
webrtc-vad = "0.4"
|
||||||
|
dotenvy = "0.15"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ jarvis-core = { path = "../jarvis-core", features = ["intent"] }
|
||||||
once_cell.workspace = true
|
once_cell.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
simple-log = "2.4"
|
simple-log = "2.4"
|
||||||
|
dotenvy.workspace = true
|
||||||
tray-icon = "0.21"
|
tray-icon = "0.21"
|
||||||
winit = "0.30"
|
winit = "0.30"
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ mod tray;
|
||||||
static SHOULD_STOP: AtomicBool = AtomicBool::new(false);
|
static SHOULD_STOP: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
fn main() -> Result<(), String> {
|
fn main() -> Result<(), String> {
|
||||||
|
load_dotenv_near_exe();
|
||||||
|
|
||||||
eprintln!("[jarvis-app] step: init_dirs");
|
eprintln!("[jarvis-app] step: init_dirs");
|
||||||
config::init_dirs()?;
|
config::init_dirs()?;
|
||||||
|
|
||||||
|
|
@ -175,6 +177,27 @@ pub fn should_stop() -> bool {
|
||||||
SHOULD_STOP.load(Ordering::SeqCst)
|
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")]
|
#[cfg(target_os = "windows")]
|
||||||
fn notify_mic_problem() {
|
fn notify_mic_problem() {
|
||||||
use winrt_notification::{Toast, Duration as ToastDuration};
|
use winrt_notification::{Toast, Duration as ToastDuration};
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ sysinfo.workspace = true
|
||||||
once_cell.workspace = true
|
once_cell.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
simple-log = "2.4"
|
simple-log = "2.4"
|
||||||
|
dotenvy.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
platform-dirs.workspace = true
|
platform-dirs.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ pub struct AppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
load_dotenv_near_exe();
|
||||||
|
|
||||||
config::init_dirs().expect("Failed to init dirs");
|
config::init_dirs().expect("Failed to init dirs");
|
||||||
|
|
||||||
// basic logging setup (simpler for GUI)
|
// basic logging setup (simpler for GUI)
|
||||||
|
|
@ -102,3 +104,22 @@ fn main() {
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.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();
|
||||||
|
}
|
||||||
|
|
|
||||||
8
make_ico.py
Normal file
8
make_ico.py
Normal file
|
|
@ -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})")
|
||||||
37
resources/commands/date_query/answer.lua
Normal file
37
resources/commands/date_query/answer.lua
Normal file
|
|
@ -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 }
|
||||||
37
resources/commands/date_query/command.toml
Normal file
37
resources/commands/date_query/command.toml
Normal file
|
|
@ -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 = ["який був вчора день"]
|
||||||
37
resources/commands/dice/command.toml
Normal file
37
resources/commands/dice/command.toml
Normal file
|
|
@ -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 = ["випадкове число"]
|
||||||
35
resources/commands/dice/roll.lua
Normal file
35
resources/commands/dice/roll.lua
Normal file
|
|
@ -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 }
|
||||||
27
resources/commands/reminders/command.toml
Normal file
27
resources/commands/reminders/command.toml
Normal 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 = [
|
||||||
|
"нагадай через",
|
||||||
|
"постав нагадування через",
|
||||||
|
"таймер на",
|
||||||
|
]
|
||||||
119
resources/commands/reminders/set.lua
Normal file
119
resources/commands/reminders/set.lua
Normal 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 }
|
||||||
37
resources/commands/stopwatch/command.toml
Normal file
37
resources/commands/stopwatch/command.toml
Normal 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 = ["стоп секундомір"]
|
||||||
53
resources/commands/stopwatch/ctl.lua
Normal file
53
resources/commands/stopwatch/ctl.lua
Normal 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 }
|
||||||
23
resources/commands/voice_type/command.toml
Normal file
23
resources/commands/voice_type/command.toml
Normal file
|
|
@ -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 = [
|
||||||
|
"надрукуй",
|
||||||
|
"введи текст",
|
||||||
|
]
|
||||||
46
resources/commands/voice_type/type.lua
Normal file
46
resources/commands/voice_type/type.lua
Normal file
|
|
@ -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 }
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 84 KiB |
Loading…
Add table
Add a link
Reference in a new issue