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.
This commit is contained in:
parent
16240609de
commit
9c25108356
22 changed files with 932 additions and 0 deletions
230
resources/commands/apps/command.toml
Normal file
230
resources/commands/apps/command.toml
Normal 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 = [
|
||||||
|
"зроби скріншот",
|
||||||
|
"знімок екрана",
|
||||||
|
]
|
||||||
8
resources/commands/apps/lock_screen.lua
Normal file
8
resources/commands/apps/lock_screen.lua
Normal 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 }
|
||||||
9
resources/commands/apps/open_browser.lua
Normal file
9
resources/commands/apps/open_browser.lua
Normal 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 }
|
||||||
8
resources/commands/apps/open_calculator.lua
Normal file
8
resources/commands/apps/open_calculator.lua
Normal 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 }
|
||||||
8
resources/commands/apps/open_explorer.lua
Normal file
8
resources/commands/apps/open_explorer.lua
Normal 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 }
|
||||||
8
resources/commands/apps/open_notepad.lua
Normal file
8
resources/commands/apps/open_notepad.lua
Normal 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 }
|
||||||
8
resources/commands/apps/open_settings.lua
Normal file
8
resources/commands/apps/open_settings.lua
Normal 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 }
|
||||||
8
resources/commands/apps/open_task_manager.lua
Normal file
8
resources/commands/apps/open_task_manager.lua
Normal 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 }
|
||||||
13
resources/commands/apps/open_terminal.lua
Normal file
13
resources/commands/apps/open_terminal.lua
Normal 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 }
|
||||||
24
resources/commands/apps/screenshot.lua
Normal file
24
resources/commands/apps/screenshot.lua
Normal 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 }
|
||||||
26
resources/commands/file_search/command.toml
Normal file
26
resources/commands/file_search/command.toml
Normal 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 = [
|
||||||
|
"знайди файл",
|
||||||
|
"пошук файлу",
|
||||||
|
"де файл",
|
||||||
|
]
|
||||||
78
resources/commands/file_search/search.lua
Normal file
78
resources/commands/file_search/search.lua
Normal 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 }
|
||||||
195
resources/commands/output_device/_audio_devices.ps1
Normal file
195
resources/commands/output_device/_audio_devices.ps1
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
55
resources/commands/output_device/command.toml
Normal file
55
resources/commands/output_device/command.toml
Normal 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 = [
|
||||||
|
"перемкни пристрій",
|
||||||
|
"перемкни звук",
|
||||||
|
"наступний пристрій",
|
||||||
|
]
|
||||||
31
resources/commands/output_device/list_devices.lua
Normal file
31
resources/commands/output_device/list_devices.lua
Normal 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 }
|
||||||
22
resources/commands/output_device/next_device.lua
Normal file
22
resources/commands/output_device/next_device.lua
Normal 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 }
|
||||||
31
resources/commands/volume/_volume_helper.ps1
Normal file
31
resources/commands/volume/_volume_helper.ps1
Normal 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
|
||||||
|
}
|
||||||
106
resources/commands/volume/command.toml
Normal file
106
resources/commands/volume/command.toml
Normal 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 = [
|
||||||
|
"максимальна гучність",
|
||||||
|
"звук на максимум",
|
||||||
|
]
|
||||||
16
resources/commands/volume/volume_down.lua
Normal file
16
resources/commands/volume/volume_down.lua
Normal 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 }
|
||||||
16
resources/commands/volume/volume_max.lua
Normal file
16
resources/commands/volume/volume_max.lua
Normal 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 }
|
||||||
16
resources/commands/volume/volume_mute.lua
Normal file
16
resources/commands/volume/volume_mute.lua
Normal 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 }
|
||||||
16
resources/commands/volume/volume_up.lua
Normal file
16
resources/commands/volume/volume_up.lua
Normal 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 }
|
||||||
Loading…
Add table
Add a link
Reference in a new issue