From c234c466425b3fc6ee9046ae64ba2ba83335fde6 Mon Sep 17 00:00:00 2001 From: Bossiara13 <236771060+DmitryBykov-ISPO@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:07:33 +0300 Subject: [PATCH 01/59] chore(app): instrument init steps with eprintln + label app::close calls --- crates/jarvis-app/src/app.rs | 5 ++-- crates/jarvis-app/src/main.rs | 52 +++++++++++++++++------------------ 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/crates/jarvis-app/src/app.rs b/crates/jarvis-app/src/app.rs index 492d4cc..0c48826 100644 --- a/crates/jarvis-app/src/app.rs +++ b/crates/jarvis-app/src/app.rs @@ -470,8 +470,9 @@ fn execute_command(text: &str, rt: &tokio::runtime::Runtime) -> bool { } -pub fn close(code: i32) { - info!("Closing application."); +pub fn close(code: i32, label: &'static str) { + eprintln!("[jarvis-app] app::close called: code={} label={}", code, label); + error!("Closing application: code={} label={}", code, label); voices::play_goodbye(); ipc::send(IpcEvent::Stopping); std::process::exit(code); diff --git a/crates/jarvis-app/src/main.rs b/crates/jarvis-app/src/main.rs index f704a70..9a57830 100644 --- a/crates/jarvis-app/src/main.rs +++ b/crates/jarvis-app/src/main.rs @@ -28,54 +28,51 @@ mod tray; static SHOULD_STOP: AtomicBool = AtomicBool::new(false); fn main() -> Result<(), String> { - // initialize directories + eprintln!("[jarvis-app] step: init_dirs"); config::init_dirs()?; - // initialize logging + eprintln!("[jarvis-app] step: init_logging"); log::init_logging()?; - // log some base info info!("Starting Jarvis v{} ...", config::APP_VERSION.unwrap()); info!("Config directory is: {}", APP_CONFIG_DIR.get().unwrap().display()); info!("Log directory is: {}", APP_LOG_DIR.get().unwrap().display()); - // initialize settings + eprintln!("[jarvis-app] step: db::init"); let settings = db::init(); - // set global DB (for core modules that read settings at init time) DB.set(settings.arc().clone()) .expect("DB already initialized"); - // init voices + eprintln!("[jarvis-app] step: voices::init"); let voice_id = settings.lock().voice.clone(); let language = settings.lock().language.clone(); if let Err(e) = voices::init(&voice_id, &language) { warn!("Failed to init voices: {}", e); } - // init i18n + eprintln!("[jarvis-app] step: i18n::init"); i18n::init(&settings.lock().language); - // init LLM fallback (no-op if GROQ_TOKEN missing) + eprintln!("[jarvis-app] step: llm_fallback::init"); llm_fallback::init(); - // init recorder + eprintln!("[jarvis-app] step: recorder::init"); if recorder::init().is_err() { - app::close(1); + app::close(1, "recorder::init failed"); } - // init models registry (scans available AI models) + eprintln!("[jarvis-app] step: models::init"); if let Err(e) = models::init() { warn!("Models registry init failed: {}", e); } - // init stt engine + eprintln!("[jarvis-app] step: stt::init"); if stt::init().is_err() { - // @TODO. Allow continuing even without STT, if commands is using keywords or smthng? - app::close(1); // cannot continue without stt + app::close(1, "stt::init failed"); } - // init commands + eprintln!("[jarvis-app] step: commands::parse_commands"); info!("Initializing commands."); let cmds = match commands::parse_commands() { Ok(c) => c, @@ -87,41 +84,40 @@ fn main() -> Result<(), String> { info!("Commands initialized. Count: {}, List: {:?}", cmds.len(), commands::list_paths(&cmds)); COMMANDS_LIST.set(cmds).unwrap(); - // init audio + eprintln!("[jarvis-app] step: audio::init"); if audio::init().is_err() { - // @TODO. Allow continuing even without audio? - app::close(1); // cannot continue without audio + app::close(1, "audio::init failed"); } - // init wake-word engine + eprintln!("[jarvis-app] step: listener::init"); if let Err(e) = listener::init() { error!("Wake-word engine init failed: {}", e); - app::close(1); + app::close(1, "listener::init failed"); } - // shared async runtime for intent classification, IPC, etc. + eprintln!("[jarvis-app] step: tokio runtime"); let rt = Arc::new( tokio::runtime::Runtime::new().expect("Failed to create tokio runtime") ); - // init intent-recognition engine + eprintln!("[jarvis-app] step: intent::init"); rt.block_on(async { if let Err(e) = intent::init(COMMANDS_LIST.get().unwrap()).await { error!("Failed to initialize intent classifier: {}", e); - app::close(1); + app::close(1, "intent::init failed"); } }); - // init slots parsing engine + eprintln!("[jarvis-app] step: slots::init"); slots::init().map_err(|e| error!("Slot extraction init failed: {}", e)).ok(); - // init audio processing + eprintln!("[jarvis-app] step: audio_processing::init"); info!("Initializing audio processing..."); if let Err(e) = audio_processing::init() { warn!("Audio processing init failed: {}", e); } - // init IPC + eprintln!("[jarvis-app] step: ipc::init"); info!("Initializing IPC..."); ipc::init(); @@ -161,14 +157,16 @@ fn main() -> Result<(), String> { ipc_rt.block_on(ipc::start_server()); }); - // start the app (in the background thread) + eprintln!("[jarvis-app] step: spawn app thread"); let app_rt = Arc::clone(&rt); std::thread::spawn(move || { let _ = app::start(text_cmd_rx, &app_rt); }); + eprintln!("[jarvis-app] step: tray::init_blocking"); tray::init_blocking(settings); + eprintln!("[jarvis-app] step: main returning Ok"); Ok(()) } From 16240609de0b16bbf42fb1a82dc8b813806240a5 Mon Sep 17 00:00:00 2001 From: Bossiara13 <236771060+DmitryBykov-ISPO@users.noreply.github.com> Date: Fri, 15 May 2026 00:54:34 +0300 Subject: [PATCH 02/59] fix(commands): repair weather set_city schema, retire broken AHK browser pack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit set_city used a plain `phrases = [...]` array which fails the lang-map schema in JCommand — the whole weather pack was being skipped at load. Normalized to per-language phrases. browser/command.toml pointed at Priler's bundled AutoHotKey exes that do not ship in this fork, so the entries were dead. Renamed to .disabled (the loader skips packs without command.toml) so we do not collide with the new Lua apps pack while keeping the directory for reference. --- .../browser/{command.toml => command.toml.disabled} | 0 resources/commands/weather/command.toml | 10 +++++----- 2 files changed, 5 insertions(+), 5 deletions(-) rename resources/commands/browser/{command.toml => command.toml.disabled} (100%) diff --git a/resources/commands/browser/command.toml b/resources/commands/browser/command.toml.disabled similarity index 100% rename from resources/commands/browser/command.toml rename to resources/commands/browser/command.toml.disabled diff --git a/resources/commands/weather/command.toml b/resources/commands/weather/command.toml index 268f3e5..6ad0bb5 100644 --- a/resources/commands/weather/command.toml +++ b/resources/commands/weather/command.toml @@ -24,8 +24,8 @@ type = "lua" script = "set_city.lua" sandbox = "standard" timeout = 5000 -phrases = [ - "установи город", - "set city", - "change city", -] \ No newline at end of file + +[commands.phrases] +ru = ["установи город", "смени город", "поменяй город"] +en = ["set city", "change city"] +ua = ["встанови місто", "змін місто"] \ No newline at end of file From 9c2510835699fa0e65b493dc47e2373fb0057d8f Mon Sep 17 00:00:00 2001 From: Bossiara13 <236771060+DmitryBykov-ISPO@users.noreply.github.com> Date: Fri, 15 May 2026 00:54:52 +0300 Subject: [PATCH 03/59] =?UTF-8?q?feat(commands):=20add=20PC=20control=20pa?= =?UTF-8?q?cks=20=E2=80=94=20volume,=20apps,=20file=5Fsearch,=20output=5Fd?= =?UTF-8?q?evice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All four packs are pure Lua (sandbox=full) and call out to PowerShell helpers where COM / native APIs are needed. No new Rust code, no rebuild required: post_build.py --sync copies them under target/{debug,release}/resources/. volume/ - volume_up / volume_down: 5x VK_VOLUME_UP|DOWN via keybd_event - volume_mute: VK_VOLUME_MUTE - volume_max: 50x VK_VOLUME_UP (system clamps) Helper _volume_helper.ps1 P/Invokes user32!keybd_event. apps/ - open_browser : start "" https://www.google.com (uses system default browser) - open_notepad : notepad.exe - open_calculator: calc.exe - open_explorer : explorer.exe - open_terminal : wt.exe, falls back to powershell.exe - open_settings : ms-settings: protocol - open_task_manager: taskmgr.exe - lock_screen : rundll32 user32.dll,LockWorkStation - screenshot : System.Drawing capture to ~/Pictures/jarvis_screenshot_*.png file_search/ - find file : recursive Get-ChildItem in Desktop/Documents/Downloads (depth 3, top 5 matches). Strips Russian/English/Ukrainian trigger prefixes from the recognized phrase, then opens explorer /select on the first hit and notifies with the total count. output_device/ - list_output_devices : enumerates active render endpoints via IMMDeviceEnumerator and notifies friendly names with indices - next_output_device : cycles the system default render device via the undocumented IPolicyConfig::SetDefaultEndpoint (eConsole/Multimedia/ Communications). Tested locally — listed 7 devices, current correctly identified at index 5. --- resources/commands/apps/command.toml | 230 ++++++++++++++++++ resources/commands/apps/lock_screen.lua | 8 + resources/commands/apps/open_browser.lua | 9 + resources/commands/apps/open_calculator.lua | 8 + resources/commands/apps/open_explorer.lua | 8 + resources/commands/apps/open_notepad.lua | 8 + resources/commands/apps/open_settings.lua | 8 + resources/commands/apps/open_task_manager.lua | 8 + resources/commands/apps/open_terminal.lua | 13 + resources/commands/apps/screenshot.lua | 24 ++ resources/commands/file_search/command.toml | 26 ++ resources/commands/file_search/search.lua | 78 ++++++ .../commands/output_device/_audio_devices.ps1 | 195 +++++++++++++++ resources/commands/output_device/command.toml | 55 +++++ .../commands/output_device/list_devices.lua | 31 +++ .../commands/output_device/next_device.lua | 22 ++ resources/commands/volume/_volume_helper.ps1 | 31 +++ resources/commands/volume/command.toml | 106 ++++++++ resources/commands/volume/volume_down.lua | 16 ++ resources/commands/volume/volume_max.lua | 16 ++ resources/commands/volume/volume_mute.lua | 16 ++ resources/commands/volume/volume_up.lua | 16 ++ 22 files changed, 932 insertions(+) create mode 100644 resources/commands/apps/command.toml create mode 100644 resources/commands/apps/lock_screen.lua create mode 100644 resources/commands/apps/open_browser.lua create mode 100644 resources/commands/apps/open_calculator.lua create mode 100644 resources/commands/apps/open_explorer.lua create mode 100644 resources/commands/apps/open_notepad.lua create mode 100644 resources/commands/apps/open_settings.lua create mode 100644 resources/commands/apps/open_task_manager.lua create mode 100644 resources/commands/apps/open_terminal.lua create mode 100644 resources/commands/apps/screenshot.lua create mode 100644 resources/commands/file_search/command.toml create mode 100644 resources/commands/file_search/search.lua create mode 100644 resources/commands/output_device/_audio_devices.ps1 create mode 100644 resources/commands/output_device/command.toml create mode 100644 resources/commands/output_device/list_devices.lua create mode 100644 resources/commands/output_device/next_device.lua create mode 100644 resources/commands/volume/_volume_helper.ps1 create mode 100644 resources/commands/volume/command.toml create mode 100644 resources/commands/volume/volume_down.lua create mode 100644 resources/commands/volume/volume_max.lua create mode 100644 resources/commands/volume/volume_mute.lua create mode 100644 resources/commands/volume/volume_up.lua diff --git a/resources/commands/apps/command.toml b/resources/commands/apps/command.toml new file mode 100644 index 0000000..5ce0817 --- /dev/null +++ b/resources/commands/apps/command.toml @@ -0,0 +1,230 @@ +[[commands]] +id = "open_browser" +type = "lua" +script = "open_browser.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "открой браузер", + "запусти браузер", + "открой хром", + "запусти хром", + "включи браузер", + "браузер открой", + "нужен браузер", +] +en = [ + "open browser", + "launch browser", + "open chrome", + "launch chrome", + "start browser", + "i need browser", +] +ua = [ + "відкрий браузер", + "запусти браузер", + "відкрий хром", + "запусти хром", + "увімкни браузер", +] + + +[[commands]] +id = "open_notepad" +type = "lua" +script = "open_notepad.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "открой блокнот", + "запусти блокнот", + "открой нотепад", +] +en = [ + "open notepad", + "launch notepad", +] +ua = [ + "відкрий блокнот", + "запусти блокнот", +] + + +[[commands]] +id = "open_calculator" +type = "lua" +script = "open_calculator.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "открой калькулятор", + "запусти калькулятор", + "нужен калькулятор", +] +en = [ + "open calculator", + "launch calculator", + "calculator please", +] +ua = [ + "відкрий калькулятор", + "запусти калькулятор", +] + + +[[commands]] +id = "open_explorer" +type = "lua" +script = "open_explorer.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "открой проводник", + "открой папку", + "запусти проводник", + "открой файлы", +] +en = [ + "open explorer", + "open file explorer", + "open files", + "open folder", +] +ua = [ + "відкрий провідник", + "відкрий папку", +] + + +[[commands]] +id = "open_terminal" +type = "lua" +script = "open_terminal.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "открой терминал", + "запусти терминал", + "открой консоль", + "запусти консоль", + "открой повершелл", +] +en = [ + "open terminal", + "open powershell", + "open console", + "launch terminal", +] +ua = [ + "відкрий термінал", + "відкрий консоль", +] + + +[[commands]] +id = "open_settings" +type = "lua" +script = "open_settings.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "открой настройки", + "открой параметры", + "запусти настройки", + "открой настройки виндоус", +] +en = [ + "open settings", + "launch settings", + "windows settings", +] +ua = [ + "відкрий налаштування", + "відкрий параметри", +] + + +[[commands]] +id = "open_task_manager" +type = "lua" +script = "open_task_manager.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "открой диспетчер задач", + "запусти диспетчер задач", + "диспетчер задач", +] +en = [ + "open task manager", + "launch task manager", + "task manager", +] +ua = [ + "відкрий диспетчер задач", + "диспетчер задач", +] + + +[[commands]] +id = "lock_screen" +type = "lua" +script = "lock_screen.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = [ + "заблокируй компьютер", + "заблокируй экран", + "блокировка экрана", + "залочь", +] +en = [ + "lock screen", + "lock the computer", + "lock pc", +] +ua = [ + "заблокуй комп'ютер", + "заблокуй екран", +] + + +[[commands]] +id = "screenshot" +type = "lua" +script = "screenshot.lua" +sandbox = "full" +timeout = 5000 + +[commands.phrases] +ru = [ + "сделай скриншот", + "скриншот", + "сними экран", +] +en = [ + "take screenshot", + "screenshot", + "capture screen", +] +ua = [ + "зроби скріншот", + "знімок екрана", +] diff --git a/resources/commands/apps/lock_screen.lua b/resources/commands/apps/lock_screen.lua new file mode 100644 index 0000000..956384f --- /dev/null +++ b/resources/commands/apps/lock_screen.lua @@ -0,0 +1,8 @@ +local res = jarvis.system.exec("rundll32.exe user32.dll,LockWorkStation") +if res.success then + jarvis.audio.play_ok() +else + jarvis.log("error", "lock screen failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() +end +return { chain = false } diff --git a/resources/commands/apps/open_browser.lua b/resources/commands/apps/open_browser.lua new file mode 100644 index 0000000..c1061ac --- /dev/null +++ b/resources/commands/apps/open_browser.lua @@ -0,0 +1,9 @@ +local cmd = "start \"\" \"https://www.google.com\"" +local res = jarvis.system.exec(cmd) +if res.success then + jarvis.audio.play_ok() +else + jarvis.log("error", "open browser failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() +end +return { chain = false } diff --git a/resources/commands/apps/open_calculator.lua b/resources/commands/apps/open_calculator.lua new file mode 100644 index 0000000..0afe795 --- /dev/null +++ b/resources/commands/apps/open_calculator.lua @@ -0,0 +1,8 @@ +local res = jarvis.system.exec("start \"\" calc.exe") +if res.success then + jarvis.audio.play_ok() +else + jarvis.log("error", "open calculator failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() +end +return { chain = false } diff --git a/resources/commands/apps/open_explorer.lua b/resources/commands/apps/open_explorer.lua new file mode 100644 index 0000000..5c6a2e4 --- /dev/null +++ b/resources/commands/apps/open_explorer.lua @@ -0,0 +1,8 @@ +local res = jarvis.system.exec("start \"\" explorer.exe") +if res.success then + jarvis.audio.play_ok() +else + jarvis.log("error", "open explorer failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() +end +return { chain = false } diff --git a/resources/commands/apps/open_notepad.lua b/resources/commands/apps/open_notepad.lua new file mode 100644 index 0000000..023c4f1 --- /dev/null +++ b/resources/commands/apps/open_notepad.lua @@ -0,0 +1,8 @@ +local res = jarvis.system.exec("start \"\" notepad.exe") +if res.success then + jarvis.audio.play_ok() +else + jarvis.log("error", "open notepad failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() +end +return { chain = false } diff --git a/resources/commands/apps/open_settings.lua b/resources/commands/apps/open_settings.lua new file mode 100644 index 0000000..786ad13 --- /dev/null +++ b/resources/commands/apps/open_settings.lua @@ -0,0 +1,8 @@ +local res = jarvis.system.exec("start \"\" ms-settings:") +if res.success then + jarvis.audio.play_ok() +else + jarvis.log("error", "open settings failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() +end +return { chain = false } diff --git a/resources/commands/apps/open_task_manager.lua b/resources/commands/apps/open_task_manager.lua new file mode 100644 index 0000000..b7d0f18 --- /dev/null +++ b/resources/commands/apps/open_task_manager.lua @@ -0,0 +1,8 @@ +local res = jarvis.system.exec("start \"\" taskmgr.exe") +if res.success then + jarvis.audio.play_ok() +else + jarvis.log("error", "open task manager failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() +end +return { chain = false } diff --git a/resources/commands/apps/open_terminal.lua b/resources/commands/apps/open_terminal.lua new file mode 100644 index 0000000..cf91242 --- /dev/null +++ b/resources/commands/apps/open_terminal.lua @@ -0,0 +1,13 @@ +local res = jarvis.system.exec("start \"\" wt.exe") +if not res.success then + jarvis.log("warn", "wt.exe not found, falling back to powershell") + res = jarvis.system.exec("start \"\" powershell.exe") +end + +if res.success then + jarvis.audio.play_ok() +else + jarvis.log("error", "open terminal failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() +end +return { chain = false } diff --git a/resources/commands/apps/screenshot.lua b/resources/commands/apps/screenshot.lua new file mode 100644 index 0000000..4d24243 --- /dev/null +++ b/resources/commands/apps/screenshot.lua @@ -0,0 +1,24 @@ +local ts = jarvis.context.time.year .. jarvis.context.time.month .. jarvis.context.time.day + .. "_" .. jarvis.context.time.hour .. jarvis.context.time.minute .. jarvis.context.time.second + +local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" +local target = userprofile .. "\\Pictures\\jarvis_screenshot_" .. ts .. ".png" + +local ps = string.format( + [[Add-Type -AssemblyName System.Windows.Forms,System.Drawing; $b=[System.Windows.Forms.Screen]::PrimaryScreen.Bounds; $bmp=New-Object Drawing.Bitmap $b.Width,$b.Height; $g=[Drawing.Graphics]::FromImage($bmp); $g.CopyFromScreen($b.Location,[Drawing.Point]::Empty,$b.Size); $bmp.Save('%s'); $g.Dispose(); $bmp.Dispose()]], + target:gsub("\\", "\\\\") +) + +local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', ps:gsub('"', '\\"')) +local res = jarvis.system.exec(cmd) + +if res.success then + jarvis.log("info", "screenshot saved to " .. target) + jarvis.system.notify("Скриншот", target) + jarvis.audio.play_ok() +else + jarvis.log("error", "screenshot failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() +end + +return { chain = false } diff --git a/resources/commands/file_search/command.toml b/resources/commands/file_search/command.toml new file mode 100644 index 0000000..f931b14 --- /dev/null +++ b/resources/commands/file_search/command.toml @@ -0,0 +1,26 @@ +[[commands]] +id = "file_search" +type = "lua" +script = "search.lua" +sandbox = "full" +timeout = 15000 + +[commands.phrases] +ru = [ + "найди файл", + "поиск файла", + "ищи файл", + "где файл", + "найди документ", +] +en = [ + "find file", + "search file", + "where is file", + "locate file", +] +ua = [ + "знайди файл", + "пошук файлу", + "де файл", +] diff --git a/resources/commands/file_search/search.lua b/resources/commands/file_search/search.lua new file mode 100644 index 0000000..40d9365 --- /dev/null +++ b/resources/commands/file_search/search.lua @@ -0,0 +1,78 @@ +local lang = jarvis.context.language +local phrase = (jarvis.context.phrase or ""):lower() + +local triggers = { + "найди документ", "найди файл", "поиск файла", "ищи файл", "где файл", + "find file", "search file", "where is file", "locate file", + "знайди файл", "пошук файлу", "де файл", +} + +local query = phrase +for _, t in ipairs(triggers) do + local start, finish = string.find(query, t, 1, true) + if start == 1 then + query = query:sub(finish + 1) + break + end +end +query = query:gsub("^%s+", ""):gsub("%s+$", "") + +if query == "" then + jarvis.log("warn", "file_search: empty query (phrase=" .. phrase .. ")") + jarvis.system.notify( + lang == "ru" and "Поиск файлов" or "File search", + lang == "ru" and "Что искать?" or "What to search for?" + ) + jarvis.audio.play_error() + return { chain = false } +end + +local userprofile = jarvis.system.env("USERPROFILE") or "C:\\Users\\Public" +local roots = { + userprofile .. "\\Desktop", + userprofile .. "\\Documents", + userprofile .. "\\Downloads", +} + +local escaped = query:gsub("'", "''") +local roots_arg = "'" .. table.concat(roots, "','") .. "'" +local ps = string.format( + [[$ErrorActionPreference='SilentlyContinue'; $r=@(%s); $q='*%s*'; $hits=Get-ChildItem -Path $r -Filter $q -Recurse -Depth 3 -File | Select-Object -First 5; foreach($h in $hits){ Write-Output $h.FullName }]], + roots_arg, escaped +) + +local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -Command "%s"', ps:gsub('"', '\\"')) +jarvis.log("info", "file_search query='" .. query .. "'") + +local res = jarvis.system.exec(cmd) + +if not res.success then + jarvis.log("error", "file_search exec failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() + return { chain = false } +end + +local stdout = res.stdout or "" +local lines = {} +for line in stdout:gmatch("[^\r\n]+") do + if line ~= "" then table.insert(lines, line) end +end + +if #lines == 0 then + jarvis.system.notify( + lang == "ru" and "Поиск файлов" or "File search", + (lang == "ru" and "Не найдено: " or "Not found: ") .. query + ) + jarvis.audio.play_not_found() +else + local first = lines[1] + local count = #lines + local title = lang == "ru" and "Найдено файлов: " or "Files found: " + jarvis.system.notify(title .. count, first) + + jarvis.log("info", "file_search opening: " .. first) + jarvis.system.exec(string.format('explorer.exe /select,"%s"', first)) + jarvis.audio.play_ok() +end + +return { chain = false } diff --git a/resources/commands/output_device/_audio_devices.ps1 b/resources/commands/output_device/_audio_devices.ps1 new file mode 100644 index 0000000..2cf76e9 --- /dev/null +++ b/resources/commands/output_device/_audio_devices.ps1 @@ -0,0 +1,195 @@ +param( + [Parameter(Mandatory=$true)] + [ValidateSet("list","set","next","current")] + [string]$Action, + + [int]$Index = -1 +) + +$ErrorActionPreference = 'Stop' + +Add-Type -TypeDefinition @' +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace JarvisAudio +{ + [Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IMMDeviceEnumerator + { + [PreserveSig] int EnumAudioEndpoints(int dataFlow, int stateMask, out IMMDeviceCollection devices); + [PreserveSig] int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice device); + [PreserveSig] int GetDevice([MarshalAs(UnmanagedType.LPWStr)] string id, out IMMDevice device); + } + + [Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IMMDeviceCollection + { + [PreserveSig] int GetCount(out int count); + [PreserveSig] int Item(int idx, out IMMDevice device); + } + + [Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IMMDevice + { + [PreserveSig] int Activate(ref Guid iid, int clsCtx, IntPtr activationParams, [MarshalAs(UnmanagedType.IUnknown)] out object iface); + [PreserveSig] int OpenPropertyStore(int access, out IPropertyStore store); + [PreserveSig] int GetId([MarshalAs(UnmanagedType.LPWStr)] out string id); + [PreserveSig] int GetState(out int state); + } + + [Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IPropertyStore + { + [PreserveSig] int GetCount(out int count); + [PreserveSig] int GetAt(int idx, out PROPERTYKEY key); + [PreserveSig] int GetValue(ref PROPERTYKEY key, out PROPVARIANT value); + } + + [StructLayout(LayoutKind.Sequential)] + public struct PROPERTYKEY + { + public Guid formatId; + public int propertyId; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PROPVARIANT + { + public ushort vt; + public ushort r1; + public ushort r2; + public ushort r3; + public IntPtr p; + public IntPtr p2; + } + + [ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")] + public class MMDeviceEnumerator {} + + [Guid("F8679F50-850A-41CF-9C72-430F290290C8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface IPolicyConfig + { + [PreserveSig] int GetMixFormat(); + [PreserveSig] int GetDeviceFormat(); + [PreserveSig] int ResetDeviceFormat(); + [PreserveSig] int SetDeviceFormat(); + [PreserveSig] int GetProcessingPeriod(); + [PreserveSig] int SetProcessingPeriod(); + [PreserveSig] int GetShareMode(); + [PreserveSig] int SetShareMode(); + [PreserveSig] int GetPropertyValue(); + [PreserveSig] int SetPropertyValue(); + [PreserveSig] int SetDefaultEndpoint([MarshalAs(UnmanagedType.LPWStr)] string deviceId, uint role); + [PreserveSig] int SetEndpointVisibility(); + } + + [ComImport, Guid("870AF99C-171D-4F9E-AF0D-E63DF40C2BC9")] + public class PolicyConfigClient {} + + public class Device + { + public string Id { get; set; } + public string Name { get; set; } + } + + public static class Manager + { + const int eRender = 0; + const int DEVICE_STATE_ACTIVE = 1; + + static PROPERTYKEY PKEY_Device_FriendlyName = new PROPERTYKEY { + formatId = new Guid("a45c254e-df1c-4efd-8020-67d146a850e0"), + propertyId = 14 + }; + + public static List ListRender() + { + var enumr = (IMMDeviceEnumerator)new MMDeviceEnumerator(); + IMMDeviceCollection coll; + int hr = enumr.EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, out coll); + if (hr != 0) throw new Exception("EnumAudioEndpoints failed: 0x" + hr.ToString("X")); + + int count; + coll.GetCount(out count); + + var result = new List(); + for (int i = 0; i < count; i++) + { + IMMDevice dev; + coll.Item(i, out dev); + string id; + dev.GetId(out id); + + IPropertyStore store; + dev.OpenPropertyStore(0, out store); + + PROPVARIANT v; + store.GetValue(ref PKEY_Device_FriendlyName, out v); + string name = v.p != IntPtr.Zero ? Marshal.PtrToStringUni(v.p) : "(unknown)"; + + result.Add(new Device { Id = id, Name = name }); + } + return result; + } + + public static string GetDefaultRenderId() + { + var enumr = (IMMDeviceEnumerator)new MMDeviceEnumerator(); + IMMDevice dev; + int hr = enumr.GetDefaultAudioEndpoint(eRender, 0, out dev); // eConsole = 0 + if (hr != 0) return null; + string id; + dev.GetId(out id); + return id; + } + + public static void SetDefault(string deviceId) + { + var cfg = (IPolicyConfig)new PolicyConfigClient(); + cfg.SetDefaultEndpoint(deviceId, 0); // eConsole + cfg.SetDefaultEndpoint(deviceId, 1); // eMultimedia + cfg.SetDefaultEndpoint(deviceId, 2); // eCommunications + } + } +} +'@ -Language CSharp + +$devs = [JarvisAudio.Manager]::ListRender() + +switch ($Action) { + "list" { + for ($i = 0; $i -lt $devs.Count; $i++) { + Write-Output ("{0}`t{1}" -f $i, $devs[$i].Name) + } + } + "current" { + $cur = [JarvisAudio.Manager]::GetDefaultRenderId() + for ($i = 0; $i -lt $devs.Count; $i++) { + if ($devs[$i].Id -eq $cur) { + Write-Output ("{0}`t{1}" -f $i, $devs[$i].Name) + return + } + } + Write-Output "-1`t(unknown)" + } + "set" { + if ($Index -lt 0 -or $Index -ge $devs.Count) { + Write-Error "Index out of range: $Index (have $($devs.Count) devices)" + exit 2 + } + [JarvisAudio.Manager]::SetDefault($devs[$Index].Id) + Write-Output ("OK`t{0}" -f $devs[$Index].Name) + } + "next" { + $cur = [JarvisAudio.Manager]::GetDefaultRenderId() + $curIdx = -1 + for ($i = 0; $i -lt $devs.Count; $i++) { + if ($devs[$i].Id -eq $cur) { $curIdx = $i; break } + } + $nextIdx = ($curIdx + 1) % $devs.Count + [JarvisAudio.Manager]::SetDefault($devs[$nextIdx].Id) + Write-Output ("OK`t{0}" -f $devs[$nextIdx].Name) + } +} diff --git a/resources/commands/output_device/command.toml b/resources/commands/output_device/command.toml new file mode 100644 index 0000000..422687b --- /dev/null +++ b/resources/commands/output_device/command.toml @@ -0,0 +1,55 @@ +[[commands]] +id = "list_output_devices" +type = "lua" +script = "list_devices.lua" +sandbox = "full" +timeout = 8000 + +[commands.phrases] +ru = [ + "какие устройства вывода", + "покажи устройства", + "список устройств", + "какие наушники", + "какие колонки", +] +en = [ + "list output devices", + "show audio devices", + "list speakers", + "what audio devices", +] +ua = [ + "які пристрої виводу", + "покажи пристрої", + "список пристроїв", +] + + +[[commands]] +id = "next_output_device" +type = "lua" +script = "next_device.lua" +sandbox = "full" +timeout = 8000 + +[commands.phrases] +ru = [ + "переключи устройство", + "переключи звук", + "следующее устройство", + "смени вывод", + "переключи на наушники", + "переключи на колонки", +] +en = [ + "switch audio device", + "next audio device", + "switch output", + "switch speakers", +] +ua = [ + "перемкни пристрій", + "перемкни звук", + "наступний пристрій", +] diff --git a/resources/commands/output_device/list_devices.lua b/resources/commands/output_device/list_devices.lua new file mode 100644 index 0000000..aefc6b2 --- /dev/null +++ b/resources/commands/output_device/list_devices.lua @@ -0,0 +1,31 @@ +local lang = jarvis.context.language +local helper = jarvis.context.command_path .. "\\_audio_devices.ps1" +local cmd = string.format( + 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action list', + helper +) + +local res = jarvis.system.exec(cmd) +if not res.success then + jarvis.log("error", "list devices failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() + return { chain = false } +end + +local lines = {} +for line in (res.stdout or ""):gmatch("[^\r\n]+") do + if line ~= "" then table.insert(lines, line) end +end + +local title = lang == "ru" and "Устройства вывода" or "Output devices" +local body +if #lines == 0 then + body = lang == "ru" and "Активных устройств не найдено" or "No active devices" +else + body = table.concat(lines, "\n") +end + +jarvis.system.notify(title, body) +jarvis.log("info", "audio devices: " .. body) +jarvis.audio.play_ok() +return { chain = false } diff --git a/resources/commands/output_device/next_device.lua b/resources/commands/output_device/next_device.lua new file mode 100644 index 0000000..852462a --- /dev/null +++ b/resources/commands/output_device/next_device.lua @@ -0,0 +1,22 @@ +local lang = jarvis.context.language +local helper = jarvis.context.command_path .. "\\_audio_devices.ps1" +local cmd = string.format( + 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action next', + helper +) + +local res = jarvis.system.exec(cmd) +if not res.success then + jarvis.log("error", "next device failed: " .. tostring(res.stderr)) + jarvis.audio.play_error() + return { chain = false } +end + +local out = (res.stdout or ""):gsub("[\r\n]+$", "") +local name = out:match("^OK\t(.+)$") or out + +local title = lang == "ru" and "Аудио переключено" or "Audio switched" +jarvis.system.notify(title, name) +jarvis.log("info", "audio switched to: " .. name) +jarvis.audio.play_ok() +return { chain = false } diff --git a/resources/commands/volume/_volume_helper.ps1 b/resources/commands/volume/_volume_helper.ps1 new file mode 100644 index 0000000..3cc0161 --- /dev/null +++ b/resources/commands/volume/_volume_helper.ps1 @@ -0,0 +1,31 @@ +param( + [Parameter(Mandatory=$true)] + [ValidateSet("up","down","mute","max")] + [string]$Action, + + [int]$Times = 1 +) + +Add-Type -Name VolumeKey -Namespace Win32 -MemberDefinition @' +[DllImport("user32.dll")] +public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, System.UIntPtr dwExtraInfo); +'@ + +$KEY_UP = 0xAF +$KEY_DOWN = 0xAE +$KEY_MUTE = 0xAD + +$vk = switch ($Action) { + "up" { $KEY_UP } + "down" { $KEY_DOWN } + "mute" { $KEY_MUTE } + "max" { $KEY_UP } +} + +$count = if ($Action -eq "max") { 50 } else { $Times } + +for ($i = 0; $i -lt $count; $i++) { + [Win32.VolumeKey]::keybd_event($vk, 0, 0, [UIntPtr]::Zero) + [Win32.VolumeKey]::keybd_event($vk, 0, 2, [UIntPtr]::Zero) + Start-Sleep -Milliseconds 25 +} diff --git a/resources/commands/volume/command.toml b/resources/commands/volume/command.toml new file mode 100644 index 0000000..288af96 --- /dev/null +++ b/resources/commands/volume/command.toml @@ -0,0 +1,106 @@ +[[commands]] +id = "volume_up" +type = "lua" +script = "volume_up.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = [ + "громче", + "сделай громче", + "прибавь звук", + "увеличь громкость", + "погромче", +] +en = [ + "louder", + "volume up", + "increase volume", + "turn it up", +] +ua = [ + "голосніше", + "зроби голосніше", + "збільш гучність", +] + + +[[commands]] +id = "volume_down" +type = "lua" +script = "volume_down.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = [ + "тише", + "сделай тише", + "убавь звук", + "уменьши громкость", + "потише", +] +en = [ + "quieter", + "volume down", + "decrease volume", + "turn it down", +] +ua = [ + "тихіше", + "зроби тихіше", + "зменш гучність", +] + + +[[commands]] +id = "volume_mute" +type = "lua" +script = "volume_mute.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = [ + "выключи звук", + "заткнись", + "тишина", + "приглуши звук", + "отключи звук", +] +en = [ + "mute", + "silence", + "shut up", + "be quiet", +] +ua = [ + "вимкни звук", + "тиша", + "стихни", +] + + +[[commands]] +id = "volume_max" +type = "lua" +script = "volume_max.lua" +sandbox = "full" +timeout = 3000 + +[commands.phrases] +ru = [ + "максимальная громкость", + "звук на максимум", + "на полную", +] +en = [ + "max volume", + "full volume", + "maximum", +] +ua = [ + "максимальна гучність", + "звук на максимум", +] diff --git a/resources/commands/volume/volume_down.lua b/resources/commands/volume/volume_down.lua new file mode 100644 index 0000000..db0f7f7 --- /dev/null +++ b/resources/commands/volume/volume_down.lua @@ -0,0 +1,16 @@ +local helper = jarvis.context.command_path .. "\\_volume_helper.ps1" +local cmd = string.format( + 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action down -Times 5', + helper +) + +local res = jarvis.system.exec(cmd) +if res.success then + jarvis.log("info", "volume down") + jarvis.audio.play_ok() +else + jarvis.log("error", "volume down failed: code=" .. tostring(res.code) .. " stderr=" .. tostring(res.stderr)) + jarvis.audio.play_error() +end + +return { chain = false } diff --git a/resources/commands/volume/volume_max.lua b/resources/commands/volume/volume_max.lua new file mode 100644 index 0000000..7935502 --- /dev/null +++ b/resources/commands/volume/volume_max.lua @@ -0,0 +1,16 @@ +local helper = jarvis.context.command_path .. "\\_volume_helper.ps1" +local cmd = string.format( + 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action max', + helper +) + +local res = jarvis.system.exec(cmd) +if res.success then + jarvis.log("info", "volume max") + jarvis.audio.play_ok() +else + jarvis.log("error", "volume max failed: code=" .. tostring(res.code) .. " stderr=" .. tostring(res.stderr)) + jarvis.audio.play_error() +end + +return { chain = false } diff --git a/resources/commands/volume/volume_mute.lua b/resources/commands/volume/volume_mute.lua new file mode 100644 index 0000000..bb29bdb --- /dev/null +++ b/resources/commands/volume/volume_mute.lua @@ -0,0 +1,16 @@ +local helper = jarvis.context.command_path .. "\\_volume_helper.ps1" +local cmd = string.format( + 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action mute', + helper +) + +local res = jarvis.system.exec(cmd) +if res.success then + jarvis.log("info", "mute toggled") + jarvis.audio.play_ok() +else + jarvis.log("error", "mute failed: code=" .. tostring(res.code) .. " stderr=" .. tostring(res.stderr)) + jarvis.audio.play_error() +end + +return { chain = false } diff --git a/resources/commands/volume/volume_up.lua b/resources/commands/volume/volume_up.lua new file mode 100644 index 0000000..0561954 --- /dev/null +++ b/resources/commands/volume/volume_up.lua @@ -0,0 +1,16 @@ +local helper = jarvis.context.command_path .. "\\_volume_helper.ps1" +local cmd = string.format( + 'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action up -Times 5', + helper +) + +local res = jarvis.system.exec(cmd) +if res.success then + jarvis.log("info", "volume up") + jarvis.audio.play_ok() +else + jarvis.log("error", "volume up failed: code=" .. tostring(res.code) .. " stderr=" .. tostring(res.stderr)) + jarvis.audio.play_error() +end + +return { chain = false } From 85a004e263df5382780fa82d1a9b4e8c2165b50e Mon Sep 17 00:00:00 2001 From: Bossiara13 <236771060+DmitryBykov-ISPO@users.noreply.github.com> Date: Fri, 15 May 2026 00:55:01 +0300 Subject: [PATCH 04/59] test(commands): schema and Lua script validation for resources/commands/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three unit tests catch the kind of bug that broke weather/set_city (a phrases array instead of lang→array map silently dropped the whole pack): - every_command_toml_parses: every resources/commands/*/command.toml round- trips through toml::from_str::. Reports all failures at once instead of failing on the first. - lua_command_scripts_exist: for every type="lua" command, the named (or default script.lua) script file exists in the pack dir. - every_command_has_phrases: structural commands (terminate/stop_chaining) excluded, every other command has ≥1 phrase across all languages. Tests use env!("CARGO_MANIFEST_DIR") -> ../.. -> resources/commands so they work from any cwd. NB: cargo test does not run on this machine right now — aws-lc-sys needs cl.exe (MSVC Build Tools missing). The release binary was built earlier with MSVC available; toolchain install is a follow-up. The tests are still valid for CI / a re-armed dev box. --- crates/jarvis-core/src/commands.rs | 106 +++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/crates/jarvis-core/src/commands.rs b/crates/jarvis-core/src/commands.rs index c250cd0..ae1c133 100644 --- a/crates/jarvis-core/src/commands.rs +++ b/crates/jarvis-core/src/commands.rs @@ -266,6 +266,112 @@ pub fn list_paths(commands: &[JCommandsList]) -> Vec<&Path> { commands.iter().map(|x| x.path.as_path()).collect() } +#[cfg(test)] +mod tests { + use super::*; + + fn resources_commands_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent().unwrap() + .parent().unwrap() + .join("resources/commands") + } + + #[test] + fn every_command_toml_parses() { + let cmds_dir = resources_commands_dir(); + assert!(cmds_dir.is_dir(), "missing {}", cmds_dir.display()); + + let mut failures = Vec::new(); + let mut packs_seen = 0; + + for entry in fs::read_dir(&cmds_dir).expect("read_dir") { + let entry = entry.expect("entry"); + let toml_file = entry.path().join("command.toml"); + if !toml_file.exists() { + continue; + } + packs_seen += 1; + let body = fs::read_to_string(&toml_file).expect("read"); + if let Err(e) = toml::from_str::(&body) { + failures.push(format!("{}: {}", toml_file.display(), e)); + } + } + + assert!(packs_seen > 0, "no command packs found in {}", cmds_dir.display()); + assert!( + failures.is_empty(), + "parse failures ({}):\n{}", + failures.len(), + failures.join("\n") + ); + } + + #[test] + fn lua_command_scripts_exist() { + let cmds_dir = resources_commands_dir(); + let mut missing = Vec::new(); + + for entry in fs::read_dir(&cmds_dir).expect("read_dir") { + let entry = entry.expect("entry"); + let pack_path = entry.path(); + let toml_file = pack_path.join("command.toml"); + if !toml_file.exists() { + continue; + } + let body = fs::read_to_string(&toml_file).expect("read"); + let pack: JCommandsList = toml::from_str(&body).expect("parse"); + for cmd in &pack.commands { + if cmd.cmd_type != "lua" { + continue; + } + let script = if cmd.script.is_empty() { + "script.lua".to_string() + } else { + cmd.script.clone() + }; + let script_path = pack_path.join(&script); + if !script_path.exists() { + missing.push(format!( + "{} -> {} (missing)", + cmd.id, + script_path.display() + )); + } + } + } + + assert!(missing.is_empty(), "missing Lua scripts:\n{}", missing.join("\n")); + } + + #[test] + fn every_command_has_phrases() { + let cmds_dir = resources_commands_dir(); + let mut empty = Vec::new(); + + for entry in fs::read_dir(&cmds_dir).expect("read_dir") { + let toml_file = entry.expect("entry").path().join("command.toml"); + if !toml_file.exists() { + continue; + } + let body = fs::read_to_string(&toml_file).expect("read"); + let pack: JCommandsList = toml::from_str(&body).expect("parse"); + for cmd in &pack.commands { + // structural commands (terminate, stop_chaining) may omit phrases + if cmd.cmd_type == "terminate" || cmd.cmd_type == "stop_chaining" { + continue; + } + let total: usize = cmd.phrases.values().map(|v| v.len()).sum(); + if total == 0 { + empty.push(cmd.id.clone()); + } + } + } + + assert!(empty.is_empty(), "commands with no phrases: {:?}", empty); + } +} + #[cfg(feature = "lua")] fn execute_lua_command( cmd_path: &PathBuf, From 24457ad6c93afc2e90e78c4278fbfd034bf8a518 Mon Sep 17 00:00:00 2001 From: Bossiara13 <236771060+DmitryBykov-ISPO@users.noreply.github.com> Date: Fri, 15 May 2026 01:09:14 +0300 Subject: [PATCH 05/59] =?UTF-8?q?chore(gui):=20rebrand=20=E2=80=94=20strip?= =?UTF-8?q?=20TG/Boosty/Patreon/feedback-bot,=20keep=20CC=20BY-NC-SA=20att?= =?UTF-8?q?ribution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous feature/gui-rebrand-polish work only updated the appInfo store keys; Footer.svelte, settings, and the commands page were still rendering upstream's Telegram channel, Boosty/Patreon support links, feedback bot, and the original author byline. This finishes the job. Rust side (applies after `cargo build` — currently a no-op until MSVC Build Tools are reinstalled): - Cargo.toml: authors = ["Bossiara13"], repository = J.A.R.V.I.S-rust fork. Original attribution stays in LICENSE.txt per CC BY-NC-SA 4.0; the UI no longer renders an author byline. - config.rs: dropped TG_OFFICIAL_LINK / FEEDBACK_LINK / SUPPORT_BOOSTY_LINK / SUPPORT_PATREON_LINK. Added UPSTREAM_REPOSITORY_LINK (Priler/jarvis for the licence attribution), ISSUES_LINK (this fork's GH issues), and PYTHON_FORK_LINK. - tauri_commands/etc.rs: dropped get_tg_official_link/get_boosty_link/ get_patreon_link/get_feedback_link. Added get_upstream_link, get_issues_link, get_python_fork_link. Tidied up the Option<&str> -> String boilerplate with unwrap_or. - main.rs: invoke_handler updated to match. Frontend side (visible only after Rust rebuild — Tauri bundles the dist into the exe): - stores.ts: appInfo shape reduced to repositoryLink / upstreamLink / issuesLink / pythonForkLink / logFilePath. loadAppInfo() uses fetchOr() with hardcoded fallbacks so the UI still populates correctly when run against an un-rebuilt binary; fetchRepoOr() additionally overrides CARGO_PKG_REPOSITORY if it still resolves to upstream's Priler/jarvis. - Footer.svelte: rewritten — © year + "J.A.R.V.I.S." + repo link + issues link + small "fork of Priler/jarvis · CC BY-NC-SA 4.0" attribution line. No author byline (it's in LICENSE.txt). - routes/settings/index.svelte: feedback link now points to GitHub Issues. - routes/commands/index.svelte: TG-channel reference replaced with a link to the Python fork (where the command builder lives) plus a pointer to resources/commands/ in this repo. Bruh GIF removed. - locales/{ru,en,ua}.ftl: removed footer-author / footer-telegram / footer-support, added footer-issues / footer-fork-of, rewrote commands-wip-* strings, retargeted settings-beta-bot to "GitHub Issues". Bundle identifier (com.priler.jarvis) kept intact to avoid moving the app data directory. --- Cargo.toml | 4 +- crates/jarvis-core/src/config.rs | 7 +- crates/jarvis-core/src/i18n/locales/en.ftl | 21 ++--- crates/jarvis-core/src/i18n/locales/ru.ftl | 21 ++--- crates/jarvis-core/src/i18n/locales/ua.ftl | 21 ++--- crates/jarvis-gui/src/main.rs | 7 +- crates/jarvis-gui/src/tauri_commands/etc.rs | 55 +++---------- frontend/src/components/Footer.svelte | 87 +++++---------------- frontend/src/routes/commands/index.svelte | 22 ++---- frontend/src/routes/settings/index.svelte | 6 +- frontend/src/stores.ts | 74 +++++++++++------- 11 files changed, 130 insertions(+), 195 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d3ea1d2..21386e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,9 @@ resolver = "2" [workspace.package] version = "0.1.0" -authors = ["Abraham Tugalov (original)", "Bossiara13 (fork)"] +authors = ["Bossiara13"] license = "GPL-3.0-only" -repository = "https://github.com/Priler/jarvis" +repository = "https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-rust" edition = "2021" [workspace.dependencies] diff --git a/crates/jarvis-core/src/config.rs b/crates/jarvis-core/src/config.rs index f7b55a4..a975cfc 100644 --- a/crates/jarvis-core/src/config.rs +++ b/crates/jarvis-core/src/config.rs @@ -82,10 +82,9 @@ pub const LOG_FILE_NAME: &str = "log.txt"; pub const APP_VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); pub const AUTHOR_NAME: Option<&str> = option_env!("CARGO_PKG_AUTHORS"); pub const REPOSITORY_LINK: Option<&str> = option_env!("CARGO_PKG_REPOSITORY"); -pub const TG_OFFICIAL_LINK: Option<&str> = Some("https://t.me/howdyho_official"); -pub const FEEDBACK_LINK: Option<&str> = Some("https://t.me/jarvis_feedback_bot"); -pub const SUPPORT_BOOSTY_LINK: Option<&str> = Some("https://boosty.to/howdyho"); -pub const SUPPORT_PATREON_LINK: Option<&str> = Some("https://www.patreon.com/c/priler"); +pub const UPSTREAM_REPOSITORY_LINK: &str = "https://github.com/Priler/jarvis"; +pub const ISSUES_LINK: &str = "https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-rust/issues"; +pub const PYTHON_FORK_LINK: &str = "https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-py"; /* Tray. diff --git a/crates/jarvis-core/src/i18n/locales/en.ftl b/crates/jarvis-core/src/i18n/locales/en.ftl index 49161b3..3860189 100644 --- a/crates/jarvis-core/src/i18n/locales/en.ftl +++ b/crates/jarvis-core/src/i18n/locales/en.ftl @@ -42,10 +42,9 @@ stats-not-selected = Not selected stats-loading = Loading... # ### FOOTER -footer-author = Project author -footer-telegram = Our Telegram channel -footer-github = Github repository -footer-support = Support the project on +footer-github = Project repository +footer-issues = Report a bug +footer-fork-of = Fork of # ### SETTINGS settings-title = Settings @@ -84,8 +83,8 @@ settings-disabled = Disabled # settings - beta notice settings-beta-title = BETA version! settings-beta-desc = Some features may not work correctly. -settings-beta-feedback = Report all bugs to -settings-beta-bot = our Telegram bot +settings-beta-feedback = Report bugs via +settings-beta-bot = GitHub Issues settings-open-logs = Open logs folder # settings - picovoice @@ -112,10 +111,12 @@ settings-openai-not-supported = ChatGPT is not currently supported. It will be a commands-title = Commands commands-search = Search commands... commands-count = { $count } commands -commands-wip-title = [404] This section is under development! -commands-wip-desc = Here will be a list of commands + full-featured command editor. -commands-wip-follow = Follow updates in -commands-wip-channel = our Telegram channel +commands-wip-title = Section under development +commands-wip-desc = This will host the command editor and list of installed packs. +commands-wip-builder = For now, use the Python fork with the command builder — +commands-wip-builder-link = open repository +commands-wip-or-fork = Or check the Rust repo and edit commands directly in +commands-wip-fork-link = resources/commands/ # ### ERRORS error-generic = An error occurred diff --git a/crates/jarvis-core/src/i18n/locales/ru.ftl b/crates/jarvis-core/src/i18n/locales/ru.ftl index 82f55bc..f4ea952 100644 --- a/crates/jarvis-core/src/i18n/locales/ru.ftl +++ b/crates/jarvis-core/src/i18n/locales/ru.ftl @@ -42,10 +42,9 @@ stats-not-selected = Не выбран stats-loading = Загрузка... # FOOTER -footer-author = Автор проекта -footer-telegram = Наш телеграм канал -footer-github = Github репозиторий проекта -footer-support = Поддержать проект на +footer-github = Репозиторий проекта +footer-issues = Сообщить о баге +footer-fork-of = Форк # SETTINGS settings-title = Настройки @@ -84,8 +83,8 @@ settings-disabled = Отключено # settings - beta notice settings-beta-title = БЕТА версия! settings-beta-desc = Часть функций может работать некорректно. -settings-beta-feedback = Сообщайте обо всех найденных багах в -settings-beta-bot = наш телеграм бот +settings-beta-feedback = Сообщайте обо всех найденных багах через +settings-beta-bot = GitHub Issues settings-open-logs = Открыть папку с логами # settings - picovoice @@ -112,10 +111,12 @@ settings-openai-not-supported = В данный момент ChatGPT не под commands-title = Команды commands-search = Поиск команд... commands-count = { $count } команд -commands-wip-title = [404] Этот раздел еще находится в разработке! -commands-wip-desc = Тут будет список команд + полноценный редактор команд. -commands-wip-follow = Следите за обновлениями в -commands-wip-channel = нашем телеграм канале +commands-wip-title = Раздел в разработке +commands-wip-desc = Здесь будет редактор команд и список установленных пакетов. +commands-wip-builder = Пока что используй Python-форк с конструктором команд — +commands-wip-builder-link = открыть репозиторий +commands-wip-or-fork = Или загляни в Rust-репозиторий и правь команды напрямую в +commands-wip-fork-link = resources/commands/ # ERRORS error-generic = Произошла ошибка diff --git a/crates/jarvis-core/src/i18n/locales/ua.ftl b/crates/jarvis-core/src/i18n/locales/ua.ftl index dcafc8a..e513352 100644 --- a/crates/jarvis-core/src/i18n/locales/ua.ftl +++ b/crates/jarvis-core/src/i18n/locales/ua.ftl @@ -42,10 +42,9 @@ stats-not-selected = Не вибрано stats-loading = Завантаження... # ### FOOTER -footer-author = Автор проєкту -footer-telegram = Наш телеграм канал -footer-github = Github репозиторій проєкту -footer-support = Підтримати проєкт на +footer-github = Репозиторій проєкту +footer-issues = Повідомити про баг +footer-fork-of = Форк # ### SETTINGS settings-title = Налаштування @@ -84,8 +83,8 @@ settings-disabled = Вимкнено # settings - beta notice settings-beta-title = БЕТА версія! settings-beta-desc = Частина функцій може працювати некоректно. -settings-beta-feedback = Повідомляйте про всі знайдені баги в -settings-beta-bot = наш телеграм бот +settings-beta-feedback = Повідомляйте про знайдені баги через +settings-beta-bot = GitHub Issues settings-open-logs = Відкрити папку з логами # settings - picovoice @@ -112,10 +111,12 @@ settings-openai-not-supported = Наразі ChatGPT не підтримуєть commands-title = Команди commands-search = Пошук команд... commands-count = { $count } команд -commands-wip-title = [404] Цей розділ ще в розробці! -commands-wip-desc = Тут буде список команд + повноцінний редактор команд. -commands-wip-follow = Слідкуйте за оновленнями в -commands-wip-channel = нашому телеграм каналі +commands-wip-title = Розділ у розробці +commands-wip-desc = Тут буде редактор команд та список встановлених пакетів. +commands-wip-builder = Поки що користуйся Python-форком з конструктором команд — +commands-wip-builder-link = відкрити репозиторій +commands-wip-or-fork = Або зазирни в Rust-репозиторій і правь команди безпосередньо в +commands-wip-fork-link = resources/commands/ # ### ERRORS error-generic = Сталася помилка diff --git a/crates/jarvis-gui/src/main.rs b/crates/jarvis-gui/src/main.rs index 3c090af..23cfaca 100644 --- a/crates/jarvis-gui/src/main.rs +++ b/crates/jarvis-gui/src/main.rs @@ -60,10 +60,9 @@ fn main() { tauri_commands::get_app_version, tauri_commands::get_author_name, tauri_commands::get_repository_link, - tauri_commands::get_tg_official_link, - tauri_commands::get_boosty_link, - tauri_commands::get_patreon_link, - tauri_commands::get_feedback_link, + tauri_commands::get_upstream_link, + tauri_commands::get_issues_link, + tauri_commands::get_python_fork_link, // fs tauri_commands::get_log_file_path, diff --git a/crates/jarvis-gui/src/tauri_commands/etc.rs b/crates/jarvis-gui/src/tauri_commands/etc.rs index 4bddc54..a547ed6 100644 --- a/crates/jarvis-gui/src/tauri_commands/etc.rs +++ b/crates/jarvis-gui/src/tauri_commands/etc.rs @@ -1,68 +1,33 @@ use jarvis_core::{config, APP_LOG_DIR}; -// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command - #[tauri::command] pub fn get_app_version() -> String { - if let Some(res) = config::APP_VERSION { - res.to_string() - } else { - String::from("error") - } + config::APP_VERSION.unwrap_or("error").to_string() } #[tauri::command] pub fn get_author_name() -> String { - if let Some(res) = config::AUTHOR_NAME { - res.to_string() - } else { - String::from("error") - } + config::AUTHOR_NAME.unwrap_or("error").to_string() } #[tauri::command] pub fn get_repository_link() -> String { - if let Some(res) = config::REPOSITORY_LINK { - res.to_string() - } else { - String::from("error") - } + config::REPOSITORY_LINK.unwrap_or("error").to_string() } #[tauri::command] -pub fn get_tg_official_link() -> String { - if let Some(ver) = config::TG_OFFICIAL_LINK { - ver.to_string() - } else { - String::from("error") - } +pub fn get_upstream_link() -> String { + config::UPSTREAM_REPOSITORY_LINK.to_string() } #[tauri::command] -pub fn get_boosty_link() -> String { - if let Some(ver) = config::SUPPORT_BOOSTY_LINK { - ver.to_string() - } else { - String::from("error") - } +pub fn get_issues_link() -> String { + config::ISSUES_LINK.to_string() } #[tauri::command] -pub fn get_patreon_link() -> String { - if let Some(ver) = config::SUPPORT_PATREON_LINK { - ver.to_string() - } else { - String::from("error") - } -} - -#[tauri::command] -pub fn get_feedback_link() -> String { - if let Some(res) = config::FEEDBACK_LINK { - res.to_string() - } else { - String::from("error") - } +pub fn get_python_fork_link() -> String { + config::PYTHON_FORK_LINK.to_string() } #[tauri::command] @@ -70,4 +35,4 @@ pub fn get_log_file_path() -> String { APP_LOG_DIR.get() .map(|p| p.display().to_string()) .unwrap_or_else(|| "unknown".to_string()) -} \ No newline at end of file +} diff --git a/frontend/src/components/Footer.svelte b/frontend/src/components/Footer.svelte index 2241d54..13796af 100644 --- a/frontend/src/components/Footer.svelte +++ b/frontend/src/components/Footer.svelte @@ -1,62 +1,39 @@ @@ -79,15 +56,17 @@ &.last { margin-top: -5px; + color: #8a8d90; + font-size: 11px; } } } a { - color: #555759!important; + color: #555759 !important; text-decoration: none; transition: 0.3s; - + & > span { color: #185876; border-bottom: 1px solid #185876; @@ -101,7 +80,7 @@ } &:hover { - color: #777a7d!important; + color: #777a7d !important; & > span { color: #2A9CD0; @@ -111,30 +90,6 @@ opacity: 1; } } - - &.telegram-link { - color: #185876; - display: inline-block; - - &:hover { - color: #2A9CD0; - // background: url(/media/images/bg/bg24.gif); - // background-repeat: no-repeat; - // background-size: contain; - } - } - - &.special-link { - color: #941d92; - display: inline-block; - - &:hover { - color: #FF07FC; - background: url(/media/images/bg/bg24.gif); - background-repeat: no-repeat; - background-size: contain; - } - } } } diff --git a/frontend/src/routes/commands/index.svelte b/frontend/src/routes/commands/index.svelte index 567be23..bc751bf 100644 --- a/frontend/src/routes/commands/index.svelte +++ b/frontend/src/routes/commands/index.svelte @@ -8,9 +8,11 @@ $: t = (key: string) => translate($translations, key) - let tgLink = "" + let pythonForkLink = "" + let repoLink = "" appInfo.subscribe(info => { - tgLink = info.tgOfficialLink + pythonForkLink = info.pythonForkLink + repoLink = info.repositoryLink }) @@ -23,19 +25,11 @@ withCloseButton={false} > {t('commands-wip-desc')}
- {t('commands-wip-follow')} {t('commands-wip-channel')}! + {t('commands-wip-builder')} + {t('commands-wip-builder-link')}. + {t('commands-wip-or-fork')} + {t('commands-wip-fork-link')}. -
- bruh -
-