Compare commits

...

5 commits

Author SHA1 Message Date
Bossiara13
b257f03f65 feat(commands): add media controls + system info packs
media/ — play_pause, next, prev, stop via user32!keybd_event (VK_MEDIA_*
0xB0..0xB3). Helper _media_helper.ps1 mirrors the volume/ pack pattern,
so the same C# P/Invoke style works for the four standard media keys
without needing any external tooling. Catches Spotify, foobar, browsers,
Yandex Music PWA — anything that responds to system media keys.

sysinfo/ — battery / time / cpu / ram / disk and an "all" combo.
_sysinfo.ps1 uses CIM queries (Win32_Battery, Win32_OperatingSystem,
Win32_LogicalDisk, Win32_Processor + Win32_PerfFormattedData_PerfOS_
Processor) — no external deps. Time is rendered in ru-RU culture
("01:17, 15 мая, пятница"). Battery falls back to a friendly "no
battery, looks like a desktop" when Win32_Battery is empty. SystemDrive
env var resolves the right disk on non-C:/ installs.

Each sysinfo command notifies the line as a Windows toast and logs it.

Triggered locally: sysinfo_all currently reports:
  Сейчас: 01:17, 15 мая, пятница
  Батарея не обнаружена (видимо, десктоп)
  CPU: AMD Ryzen 7 5700X3D 8-Core Processor, загрузка 4%
  RAM: 23.4 из 47.9 ГБ (49% занято)
  Диск C:: свободно 118.2 из 1906.8 ГБ

Total command packs is now 9 (verified via jarvis-gui startup log).

Portability: all PowerShell helpers use $env:SystemDrive and CIM
classes that ship with every Windows install; Lua wrappers resolve
the helper path via jarvis.context.command_path (no hardcoded user
profile paths or absolute C:\ refs).
2026-05-15 01:19:30 +03:00
Bossiara13
24457ad6c9 chore(gui): rebrand — strip TG/Boosty/Patreon/feedback-bot, keep CC BY-NC-SA attribution
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.
2026-05-15 01:09:14 +03:00
Bossiara13
85a004e263 test(commands): schema and Lua script validation for resources/commands/
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::<JCommandsList>. 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.
2026-05-15 00:55:01 +03:00
Bossiara13
9c25108356 feat(commands): add PC control packs — volume, apps, file_search, output_device
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 <query> : 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.
2026-05-15 00:54:52 +03:00
Bossiara13
16240609de fix(commands): repair weather set_city schema, retire broken AHK browser pack
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.
2026-05-15 00:54:34 +03:00
50 changed files with 1647 additions and 200 deletions

View file

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

View file

@ -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::<JCommandsList>(&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,

View file

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

View file

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

View file

@ -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 = Произошла ошибка

View file

@ -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 = Сталася помилка

View file

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

View file

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

View file

@ -1,62 +1,39 @@
<script lang="ts">
import { onMount } from "svelte"
import { invoke } from "@tauri-apps/api/core"
import { appInfo, currentLanguage, translations, translate } from "@/stores"
import { appInfo, translations, translate } from "@/stores"
$: t = (key: string) => translate($translations, key)
let authorName = ""
let tgLink = ""
let repoLink = ""
let boostyLink = ""
let patreonLink = ""
let upstreamLink = ""
let issuesLink = ""
const currentYear = new Date().getFullYear()
appInfo.subscribe(info => {
tgLink = info.tgOfficialLink
repoLink = info.repositoryLink
boostyLink = info.boostySupportLink
patreonLink = info.patreonSupportLink
})
onMount(async () => {
try {
authorName = await invoke<string>("get_author_name")
} catch (err) {
console.error("failed to get author name:", err)
}
upstreamLink = info.upstreamLink
issuesLink = info.issuesLink
})
</script>
<footer id="footer">
<p>© {currentYear}. {t('footer-author')}: <b>{authorName}</b></p>
<p>© {currentYear} J.A.R.V.I.S.</p>
<p class="links">
{#if $currentLanguage === "ru" || $currentLanguage === "ua"}
<a href={tgLink} target="_blank" class="telegram-link">
<img src="/media/icons/telegram.webp" alt="Telegram" width="18px" />
&nbsp;<span>{t('footer-telegram')}</span>
</a>
&nbsp;
{/if}
<a href={repoLink} target="_blank">
<a href={repoLink} target="_blank" rel="noopener noreferrer">
<img src="/media/icons/github-logo.png" alt="GitHub" width="18px" />
&nbsp;<span>{t('footer-github')}</span>
</a>
&nbsp;·&nbsp;
<a href={issuesLink} target="_blank" rel="noopener noreferrer">
<span>{t('footer-issues')}</span>
</a>
</p>
<p class="links last">
{#if $currentLanguage === "ru"}
{t('footer-support')} <a href={boostyLink} target="_blank" class="telegram-link">
<img src="/media/icons/boosty.webp" alt="Boosty" width="18px" />
<span>Boosty</span>
</a>.
{/if}
{#if $currentLanguage === "ua" || $currentLanguage === "en"}
{t('footer-support')} <a href={patreonLink} target="_blank" class="telegram-link">
<img src="/media/icons/patreon.png" alt="Patreon" width="18px" />
<span>Patreon</span>
</a>.
{/if}
<small>
{t('footer-fork-of')}
<a href={upstreamLink} target="_blank" rel="noopener noreferrer">Priler/jarvis</a>
· CC BY-NC-SA 4.0
</small>
</p>
</footer>
@ -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;
}
}
}
}
</style>

View file

@ -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
})
</script>
@ -23,19 +25,11 @@
withCloseButton={false}
>
{t('commands-wip-desc')}<br />
{t('commands-wip-follow')} <a href={tgLink} target="_blank">{t('commands-wip-channel')}</a>!
{t('commands-wip-builder')}
<a href={pythonForkLink} target="_blank" rel="noopener noreferrer">{t('commands-wip-builder-link')}</a>.
{t('commands-wip-or-fork')}
<a href={repoLink} target="_blank" rel="noopener noreferrer">{t('commands-wip-fork-link')}</a>.
</Notification>
<div class="placeholder-image">
<img src="/media/images/tenor.gif" alt="bruh" width="320px" />
</div>
<HDivider />
<Footer />
<style>
.placeholder-image {
text-align: center;
margin-top: 25px;
}
</style>

View file

@ -90,10 +90,10 @@
voiceVal = value
})
let feedbackLink = ""
let issuesLink = ""
let logFilePath = ""
appInfo.subscribe(info => {
feedbackLink = info.feedbackLink
issuesLink = info.issuesLink
logFilePath = info.logFilePath
})
@ -230,7 +230,7 @@
withCloseButton={false}
>
{t('settings-beta-desc')}<br />
{t('settings-beta-feedback')} <a href={feedbackLink} target="_blank">{t('settings-beta-bot')}</a>.
{t('settings-beta-feedback')} <a href={issuesLink} target="_blank" rel="noopener noreferrer">{t('settings-beta-bot')}</a>.
<Space h="sm" />
<Button
color="gray"

View file

@ -39,12 +39,21 @@ export const jarvisCpuUsage = writable(0)
export const assistantVoice = writable("")
// ### APP INFO
// Hardcoded fallbacks so the UI keeps working when the Rust side has not
// been rebuilt yet (the new get_upstream_link / get_issues_link / get_python_fork_link
// Tauri commands only exist after the next cargo build).
const BRANDING_FALLBACK = {
repositoryLink: "https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-rust",
upstreamLink: "https://github.com/Priler/jarvis",
issuesLink: "https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-rust/issues",
pythonForkLink: "https://github.com/DmitryBykov-ISPO/J.A.R.V.I.S-py",
}
export const appInfo = writable({
tgOfficialLink: "",
feedbackLink: "",
repositoryLink: "",
boostySupportLink: "",
patreonSupportLink: "",
repositoryLink: BRANDING_FALLBACK.repositoryLink,
upstreamLink: BRANDING_FALLBACK.upstreamLink,
issuesLink: BRANDING_FALLBACK.issuesLink,
pythonForkLink: BRANDING_FALLBACK.pythonForkLink,
logFilePath: ""
})
@ -59,27 +68,38 @@ export async function loadVoiceSetting() {
}
export async function loadAppInfo() {
try {
const [tg, feedback, repo, boosty, patreon, logPath] = await Promise.all([
invoke<string>("get_tg_official_link"),
invoke<string>("get_feedback_link"),
invoke<string>("get_repository_link"),
invoke<string>("get_boosty_link"),
invoke<string>("get_patreon_link"),
invoke<string>("get_log_file_path")
])
appInfo.set({
tgOfficialLink: tg,
feedbackLink: feedback,
repositoryLink: repo,
boostySupportLink: boosty,
patreonSupportLink: patreon,
logFilePath: logPath
})
} catch (err) {
console.error("failed to load app info:", err)
const fetchOr = async (name: string, fallback: string): Promise<string> => {
try {
const v = await invoke<string>(name)
return v && v !== "error" ? v : fallback
} catch {
return fallback
}
}
// The old binary's get_repository_link returns Priler's upstream — override
// so we never display the upstream repo as if it were the current fork.
const fetchRepoOr = async (fallback: string): Promise<string> => {
const v = await fetchOr("get_repository_link", fallback)
if (/priler\/jarvis/i.test(v)) return fallback
return v
}
const [repo, upstream, issues, pythonFork, logPath] = await Promise.all([
fetchRepoOr(BRANDING_FALLBACK.repositoryLink),
fetchOr("get_upstream_link", BRANDING_FALLBACK.upstreamLink),
fetchOr("get_issues_link", BRANDING_FALLBACK.issuesLink),
fetchOr("get_python_fork_link", BRANDING_FALLBACK.pythonForkLink),
fetchOr("get_log_file_path", "")
])
appInfo.set({
repositoryLink: repo,
upstreamLink: upstream,
issuesLink: issues,
pythonForkLink: pythonFork,
logFilePath: logPath
})
}
export async function updateJarvisStats() {
@ -98,7 +118,7 @@ let statsInterval: ReturnType<typeof setInterval> | null = null
export function startStatsPolling(intervalMs = 5000) {
if (statsInterval) return // already running
updateJarvisStats()
statsInterval = setInterval(updateJarvisStats, intervalMs)
}
@ -108,4 +128,4 @@ export function stopStatsPolling() {
clearInterval(statsInterval)
statsInterval = null
}
}
}

View file

@ -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 = [
"зроби скріншот",
"знімок екрана",
]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = [
"знайди файл",
"пошук файлу",
"де файл",
]

View file

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

View file

@ -0,0 +1,20 @@
param(
[Parameter(Mandatory=$true)]
[ValidateSet("play_pause","next","prev","stop")]
[string]$Action
)
Add-Type -Name MediaKey -Namespace Win32 -MemberDefinition @'
[DllImport("user32.dll")]
public static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, System.UIntPtr dwExtraInfo);
'@
$vk = switch ($Action) {
"play_pause" { 0xB3 }
"next" { 0xB0 }
"prev" { 0xB1 }
"stop" { 0xB2 }
}
[Win32.MediaKey]::keybd_event($vk, 0, 0, [UIntPtr]::Zero)
[Win32.MediaKey]::keybd_event($vk, 0, 2, [UIntPtr]::Zero)

View file

@ -0,0 +1,103 @@
[[commands]]
id = "media_play_pause"
type = "lua"
script = "play_pause.lua"
sandbox = "full"
timeout = 3000
[commands.phrases]
ru = [
"пауза",
"поставь на паузу",
"продолжи",
"включи музыку",
"запусти музыку",
"плей",
]
en = [
"pause",
"play",
"resume",
"play music",
]
ua = [
"пауза",
"продовж",
"включи музику",
]
[[commands]]
id = "media_next"
type = "lua"
script = "next_track.lua"
sandbox = "full"
timeout = 3000
[commands.phrases]
ru = [
"следующий трек",
"следующая песня",
"переключи трек",
"следующая",
"дальше",
]
en = [
"next track",
"next song",
"skip",
"next",
]
ua = [
"наступний трек",
"наступна пісня",
"далі",
]
[[commands]]
id = "media_prev"
type = "lua"
script = "prev_track.lua"
sandbox = "full"
timeout = 3000
[commands.phrases]
ru = [
"предыдущий трек",
"предыдущая песня",
"верни песню",
"назад",
]
en = [
"previous track",
"previous song",
"back",
]
ua = [
"попередній трек",
"назад",
]
[[commands]]
id = "media_stop"
type = "lua"
script = "stop_media.lua"
sandbox = "full"
timeout = 3000
[commands.phrases]
ru = [
"стоп музыка",
"останови музыку",
"выключи музыку",
]
en = [
"stop music",
"stop playback",
]
ua = [
"стоп музика",
"вимкни музику",
]

View file

@ -0,0 +1,13 @@
local helper = jarvis.context.command_path .. "\\_media_helper.ps1"
local cmd = string.format(
'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action next',
helper
)
local res = jarvis.system.exec(cmd)
if res.success then
jarvis.audio.play_ok()
else
jarvis.log("error", "media next failed: " .. tostring(res.stderr))
jarvis.audio.play_error()
end
return { chain = false }

View file

@ -0,0 +1,13 @@
local helper = jarvis.context.command_path .. "\\_media_helper.ps1"
local cmd = string.format(
'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action play_pause',
helper
)
local res = jarvis.system.exec(cmd)
if res.success then
jarvis.audio.play_ok()
else
jarvis.log("error", "play/pause failed: " .. tostring(res.stderr))
jarvis.audio.play_error()
end
return { chain = false }

View file

@ -0,0 +1,13 @@
local helper = jarvis.context.command_path .. "\\_media_helper.ps1"
local cmd = string.format(
'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action prev',
helper
)
local res = jarvis.system.exec(cmd)
if res.success then
jarvis.audio.play_ok()
else
jarvis.log("error", "media prev failed: " .. tostring(res.stderr))
jarvis.audio.play_error()
end
return { chain = false }

View file

@ -0,0 +1,13 @@
local helper = jarvis.context.command_path .. "\\_media_helper.ps1"
local cmd = string.format(
'powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Action stop',
helper
)
local res = jarvis.system.exec(cmd)
if res.success then
jarvis.audio.play_ok()
else
jarvis.log("error", "media stop failed: " .. tostring(res.stderr))
jarvis.audio.play_error()
end
return { chain = false }

View file

@ -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<Device> 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<Device>();
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)
}
}

View file

@ -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 = [
"перемкни пристрій",
"перемкни звук",
"наступний пристрій",
]

View file

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

View file

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

View file

@ -0,0 +1,69 @@
param(
[Parameter(Mandatory=$true)]
[ValidateSet("battery","time","cpu","ram","disk","all")]
[string]$Topic
)
$ErrorActionPreference = 'SilentlyContinue'
function Get-BatteryInfo {
$b = Get-CimInstance -ClassName Win32_Battery
if (-not $b) { return "Батарея не обнаружена (видимо, десктоп)" }
$pct = [int]$b.EstimatedChargeRemaining
$status = switch ($b.BatteryStatus) {
1 { "разряжается" }
2 { "от сети" }
3 { "заряжена" }
4 { "низкий заряд" }
5 { "критический заряд" }
default { "статус $($b.BatteryStatus)" }
}
return "Батарея: $pct%, $status"
}
function Get-TimeInfo {
$ru = [System.Globalization.CultureInfo]::GetCultureInfo("ru-RU")
$now = Get-Date
return "Сейчас: " + $now.ToString("HH:mm, dd MMMM, dddd", $ru)
}
function Get-CpuInfo {
$cpu = Get-CimInstance -ClassName Win32_Processor | Select-Object -First 1
$load = (Get-CimInstance -ClassName Win32_PerfFormattedData_PerfOS_Processor -Filter "Name='_Total'" -ErrorAction SilentlyContinue).PercentProcessorTime
if ($null -eq $load) { $load = $cpu.LoadPercentage }
return "CPU: $($cpu.Name.Trim()), загрузка $load%"
}
function Get-RamInfo {
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$totalGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 1)
$freeGB = [math]::Round($os.FreePhysicalMemory / 1MB, 1)
$usedGB = [math]::Round($totalGB - $freeGB, 1)
$pct = [int](($usedGB / $totalGB) * 100)
return "RAM: $usedGB из $totalGB ГБ ($pct% занято)"
}
function Get-DiskInfo {
$sys = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DeviceID='$($env:SystemDrive)'"
if (-not $sys) {
$sys = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" | Select-Object -First 1
}
$freeGB = [math]::Round($sys.FreeSpace / 1GB, 1)
$totalGB = [math]::Round($sys.Size / 1GB, 1)
return "Диск $($sys.DeviceID): свободно $freeGB из $totalGB ГБ"
}
switch ($Topic) {
"battery" { Get-BatteryInfo }
"time" { Get-TimeInfo }
"cpu" { Get-CpuInfo }
"ram" { Get-RamInfo }
"disk" { Get-DiskInfo }
"all" {
Get-TimeInfo
Get-BatteryInfo
Get-CpuInfo
Get-RamInfo
Get-DiskInfo
}
}

View file

@ -0,0 +1,13 @@
local helper = jarvis.context.command_path .. "\\_sysinfo.ps1"
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic all', helper)
local res = jarvis.system.exec(cmd)
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
if res.success and text ~= "" then
jarvis.system.notify("Статус системы", text)
jarvis.log("info", text)
jarvis.audio.play_ok()
else
jarvis.log("error", "sysinfo all failed: " .. tostring(res.stderr))
jarvis.audio.play_error()
end
return { chain = false }

View file

@ -0,0 +1,13 @@
local helper = jarvis.context.command_path .. "\\_sysinfo.ps1"
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic battery', helper)
local res = jarvis.system.exec(cmd)
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
if res.success and text ~= "" then
jarvis.system.notify("Батарея", text)
jarvis.log("info", text)
jarvis.audio.play_ok()
else
jarvis.log("error", "battery info failed: " .. tostring(res.stderr))
jarvis.audio.play_error()
end
return { chain = false }

View file

@ -0,0 +1,152 @@
[[commands]]
id = "sysinfo_battery"
type = "lua"
script = "battery.lua"
sandbox = "full"
timeout = 5000
[commands.phrases]
ru = [
"сколько заряда",
"сколько батарея",
"заряд батареи",
"сколько процентов",
]
en = [
"battery level",
"how much battery",
"battery status",
]
ua = [
"скільки заряду",
"заряд батареї",
]
[[commands]]
id = "sysinfo_time"
type = "lua"
script = "time.lua"
sandbox = "full"
timeout = 3000
[commands.phrases]
ru = [
"сколько времени",
"который час",
"сколько сейчас времени",
"какой день",
"какое сегодня число",
]
en = [
"what time is it",
"current time",
"what day is it",
"what is the date",
]
ua = [
"котра година",
"скільки часу",
"який сьогодні день",
]
[[commands]]
id = "sysinfo_cpu"
type = "lua"
script = "cpu.lua"
sandbox = "full"
timeout = 8000
[commands.phrases]
ru = [
"загрузка процессора",
"сколько процессор",
"нагрузка цпу",
"что с процессором",
]
en = [
"cpu load",
"cpu usage",
"how is cpu",
]
ua = [
"завантаження процесора",
"як процесор",
]
[[commands]]
id = "sysinfo_ram"
type = "lua"
script = "ram.lua"
sandbox = "full"
timeout = 5000
[commands.phrases]
ru = [
"сколько памяти",
"сколько оперативки",
"загрузка оперативки",
"сколько свободной памяти",
]
en = [
"how much ram",
"ram usage",
"memory usage",
]
ua = [
"скільки пам'яті",
"скільки оперативки",
]
[[commands]]
id = "sysinfo_disk"
type = "lua"
script = "disk.lua"
sandbox = "full"
timeout = 5000
[commands.phrases]
ru = [
"сколько свободно на диске",
"сколько места на диске",
"сколько места на жёстком",
"сколько свободно",
]
en = [
"free disk space",
"how much disk space",
"disk usage",
]
ua = [
"скільки вільно на диску",
"скільки місця на диску",
]
[[commands]]
id = "sysinfo_all"
type = "lua"
script = "all.lua"
sandbox = "full"
timeout = 10000
[commands.phrases]
ru = [
"статус системы",
"что с компьютером",
"состояние пк",
"что по системе",
"статус",
]
en = [
"system status",
"how is the system",
"pc status",
]
ua = [
"статус системи",
"стан пк",
]

View file

@ -0,0 +1,13 @@
local helper = jarvis.context.command_path .. "\\_sysinfo.ps1"
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic cpu', helper)
local res = jarvis.system.exec(cmd)
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
if res.success and text ~= "" then
jarvis.system.notify("CPU", text)
jarvis.log("info", text)
jarvis.audio.play_ok()
else
jarvis.log("error", "cpu info failed: " .. tostring(res.stderr))
jarvis.audio.play_error()
end
return { chain = false }

View file

@ -0,0 +1,13 @@
local helper = jarvis.context.command_path .. "\\_sysinfo.ps1"
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic disk', helper)
local res = jarvis.system.exec(cmd)
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
if res.success and text ~= "" then
jarvis.system.notify("Диск", text)
jarvis.log("info", text)
jarvis.audio.play_ok()
else
jarvis.log("error", "disk info failed: " .. tostring(res.stderr))
jarvis.audio.play_error()
end
return { chain = false }

View file

@ -0,0 +1,13 @@
local helper = jarvis.context.command_path .. "\\_sysinfo.ps1"
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic ram', helper)
local res = jarvis.system.exec(cmd)
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
if res.success and text ~= "" then
jarvis.system.notify("Память", text)
jarvis.log("info", text)
jarvis.audio.play_ok()
else
jarvis.log("error", "ram info failed: " .. tostring(res.stderr))
jarvis.audio.play_error()
end
return { chain = false }

View file

@ -0,0 +1,13 @@
local helper = jarvis.context.command_path .. "\\_sysinfo.ps1"
local cmd = string.format('powershell -NoProfile -ExecutionPolicy Bypass -File "%s" -Topic time', helper)
local res = jarvis.system.exec(cmd)
local text = (res.stdout or ""):gsub("[\r\n]+$", "")
if res.success and text ~= "" then
jarvis.system.notify("Время", text)
jarvis.log("info", text)
jarvis.audio.play_ok()
else
jarvis.log("error", "time info failed: " .. tostring(res.stderr))
jarvis.audio.play_error()
end
return { chain = false }

View file

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

View file

@ -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 = [
"максимальна гучність",
"звук на максимум",
]

View file

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

View file

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

View file

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

View file

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

View file

@ -24,8 +24,8 @@ type = "lua"
script = "set_city.lua"
sandbox = "standard"
timeout = 5000
phrases = [
"установи город",
"set city",
"change city",
]
[commands.phrases]
ru = ["установи город", "смени город", "поменяй город"]
en = ["set city", "change city"]
ua = ["встанови місто", "змін місто"]