963 lines
39 KiB
Python
963 lines
39 KiB
Python
#computer_settings.py
|
|
import json
|
|
import re
|
|
import sys
|
|
import time
|
|
import subprocess
|
|
import platform
|
|
from pathlib import Path
|
|
|
|
try:
|
|
import pyautogui
|
|
pyautogui.FAILSAFE = True
|
|
pyautogui.PAUSE = 0.05
|
|
_PYAUTOGUI = True
|
|
except ImportError:
|
|
_PYAUTOGUI = False
|
|
|
|
try:
|
|
import pyperclip
|
|
_PYPERCLIP = True
|
|
except ImportError:
|
|
_PYPERCLIP = False
|
|
|
|
from core import confirm
|
|
from core.undo import push_undo
|
|
|
|
_OS = platform.system() # "Windows" | "Darwin" | "Linux"
|
|
|
|
if _OS == "Windows":
|
|
_WIN_HIDE: dict = {"creationflags": subprocess.CREATE_NO_WINDOW}
|
|
else:
|
|
_WIN_HIDE: dict = {}
|
|
|
|
|
|
def _get_base_dir() -> Path:
|
|
if getattr(sys, "frozen", False):
|
|
return Path(sys.executable).parent
|
|
return Path(__file__).resolve().parent.parent
|
|
|
|
def _get_api_key() -> str:
|
|
path = _get_base_dir() / "config" / "api_keys.json"
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
return json.load(f)["gemini_api_key"]
|
|
|
|
def _get_macos_wifi_interface() -> str:
|
|
try:
|
|
result = subprocess.run(
|
|
["networksetup", "-listallhardwareports"],
|
|
capture_output=True, text=True, timeout=5
|
|
)
|
|
lines = result.stdout.splitlines()
|
|
for i, line in enumerate(lines):
|
|
if "Wi-Fi" in line or "AirPort" in line:
|
|
for j in range(i, min(i + 4, len(lines))):
|
|
if lines[j].startswith("Device:"):
|
|
return lines[j].split(":", 1)[1].strip()
|
|
except Exception:
|
|
pass
|
|
return "en0"
|
|
|
|
def volume_up():
|
|
if _OS == "Windows":
|
|
for _ in range(5): pyautogui.press("volumeup")
|
|
elif _OS == "Darwin":
|
|
subprocess.run(["osascript", "-e",
|
|
"set volume output volume (output volume of (get volume settings) + 10)"],
|
|
capture_output=True)
|
|
else:
|
|
subprocess.run(["pactl", "set-sink-volume", "@DEFAULT_SINK@", "+10%"],
|
|
capture_output=True)
|
|
|
|
def volume_down():
|
|
if _OS == "Windows":
|
|
for _ in range(5): pyautogui.press("volumedown")
|
|
elif _OS == "Darwin":
|
|
subprocess.run(["osascript", "-e",
|
|
"set volume output volume (output volume of (get volume settings) - 10)"],
|
|
capture_output=True)
|
|
else:
|
|
subprocess.run(["pactl", "set-sink-volume", "@DEFAULT_SINK@", "-10%"],
|
|
capture_output=True)
|
|
|
|
def volume_mute():
|
|
if _OS == "Windows":
|
|
pyautogui.press("volumemute")
|
|
elif _OS == "Darwin":
|
|
subprocess.run(["osascript", "-e", "set volume with output muted"],
|
|
capture_output=True)
|
|
else:
|
|
subprocess.run(["pactl", "set-sink-mute", "@DEFAULT_SINK@", "toggle"],
|
|
capture_output=True)
|
|
|
|
def volume_get() -> int | None:
|
|
"""Current master volume 0-100, or None if this platform will not say.
|
|
|
|
Undo needs a "before" value, and reading one is cheap on every OS we
|
|
support. Where it is not readable the action simply is not registered as
|
|
undoable — a wrong undo is worse than no undo."""
|
|
try:
|
|
if _OS == "Windows":
|
|
import math
|
|
from ctypes import cast, POINTER
|
|
from comtypes import CLSCTX_ALL
|
|
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume
|
|
devices = AudioUtilities.GetSpeakers()
|
|
interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
|
|
vol = cast(interface, POINTER(IAudioEndpointVolume))
|
|
db = vol.GetMasterVolumeLevel()
|
|
if db <= -65.0:
|
|
return 0
|
|
return max(0, min(100, round(10 ** (db / 20) * 100)))
|
|
if _OS == "Darwin":
|
|
r = subprocess.run(["osascript", "-e", "output volume of (get volume settings)"],
|
|
capture_output=True, text=True, timeout=5)
|
|
return max(0, min(100, int(r.stdout.strip())))
|
|
r = subprocess.run(["pactl", "get-sink-volume", "@DEFAULT_SINK@"],
|
|
capture_output=True, text=True, timeout=5)
|
|
m = re.search(r"(\d+)%", r.stdout)
|
|
return max(0, min(100, int(m.group(1)))) if m else None
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def brightness_get() -> int | None:
|
|
"""Current brightness 0-100, or None where it cannot be read."""
|
|
try:
|
|
if _OS == "Windows":
|
|
r = subprocess.run(
|
|
["powershell", "-Command",
|
|
"(Get-WmiObject -Namespace root/wmi -Class WmiMonitorBrightness)"
|
|
".CurrentBrightness"],
|
|
capture_output=True, text=True, timeout=5, **_WIN_HIDE
|
|
)
|
|
return max(0, min(100, int(r.stdout.strip())))
|
|
if _OS == "Linux" and subprocess.run(
|
|
["which", "brightnessctl"], capture_output=True).returncode == 0:
|
|
cur = int(subprocess.run(["brightnessctl", "get"],
|
|
capture_output=True, text=True, timeout=5).stdout.strip())
|
|
mx = int(subprocess.run(["brightnessctl", "max"],
|
|
capture_output=True, text=True, timeout=5).stdout.strip())
|
|
return max(0, min(100, round(cur * 100 / mx))) if mx else None
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def brightness_set(value: int) -> None:
|
|
"""Set brightness to an absolute percentage. Only used to restore a value
|
|
captured before a change, so it is undo's counterpart to the up/down pair."""
|
|
value = max(0, min(100, int(value)))
|
|
if _OS == "Windows":
|
|
subprocess.run(
|
|
["powershell", "-Command",
|
|
"(Get-WmiObject -Namespace root/wmi -Class WmiMonitorBrightnessMethods)"
|
|
f".WmiSetBrightness(1, {value})"],
|
|
capture_output=True, timeout=5, **_WIN_HIDE
|
|
)
|
|
elif _OS == "Linux":
|
|
subprocess.run(["brightnessctl", "set", f"{value}%"], capture_output=True)
|
|
|
|
|
|
def volume_set(value: int):
|
|
value = max(0, min(100, int(value)))
|
|
if _OS == "Windows":
|
|
try:
|
|
import math
|
|
from ctypes import cast, POINTER
|
|
from comtypes import CLSCTX_ALL
|
|
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume
|
|
devices = AudioUtilities.GetSpeakers()
|
|
interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None)
|
|
vol = cast(interface, POINTER(IAudioEndpointVolume))
|
|
vol_db = -65.25 if value == 0 else max(-65.25, 20 * math.log10(value / 100))
|
|
vol.SetMasterVolumeLevel(vol_db, None)
|
|
return
|
|
except Exception as e:
|
|
print(f"[Settings] pycaw failed, using keypress fallback: {e}")
|
|
pyautogui.press("volumemute")
|
|
pyautogui.press("volumemute")
|
|
elif _OS == "Darwin":
|
|
subprocess.run(["osascript", "-e", f"set volume output volume {value}"],
|
|
capture_output=True)
|
|
return
|
|
else:
|
|
subprocess.run(["pactl", "set-sink-volume", "@DEFAULT_SINK@", f"{value}%"],
|
|
capture_output=True)
|
|
return
|
|
|
|
def brightness_up():
|
|
if _OS == "Darwin":
|
|
subprocess.run(["osascript", "-e",
|
|
'tell application "System Events" to key code 144'],
|
|
capture_output=True)
|
|
elif _OS == "Linux":
|
|
if subprocess.run(["which", "brightnessctl"],
|
|
capture_output=True).returncode == 0:
|
|
subprocess.run(["brightnessctl", "set", "+10%"], capture_output=True)
|
|
else:
|
|
subprocess.run(
|
|
'xrandr --output $(xrandr | grep " connected" | head -1 | cut -d " " -f1)'
|
|
' --brightness $(python3 -c "import subprocess; '
|
|
'b=float(subprocess.check_output([\"xrandr\",\"--verbose\"]).decode()'
|
|
'.split(\"Brightness:\")[1].split()[0]); print(min(1.0,b+0.1))")',
|
|
shell=True, capture_output=True
|
|
)
|
|
else:
|
|
try:
|
|
subprocess.run(
|
|
["powershell", "-Command",
|
|
"(Get-WmiObject -Namespace root/wmi -Class WmiMonitorBrightnessMethods)"
|
|
".WmiSetBrightness(1, [math]::Min(100, "
|
|
"(Get-WmiObject -Namespace root/wmi -Class WmiMonitorBrightness).CurrentBrightness + 10))"],
|
|
capture_output=True, timeout=5, **_WIN_HIDE
|
|
)
|
|
except Exception as e:
|
|
print(f"[Settings] Brightness up failed on Windows: {e}")
|
|
|
|
def brightness_down():
|
|
if _OS == "Darwin":
|
|
subprocess.run(["osascript", "-e",
|
|
'tell application "System Events" to key code 145'],
|
|
capture_output=True)
|
|
elif _OS == "Linux":
|
|
if subprocess.run(["which", "brightnessctl"],
|
|
capture_output=True).returncode == 0:
|
|
subprocess.run(["brightnessctl", "set", "10%-"], capture_output=True)
|
|
else:
|
|
subprocess.run(
|
|
'xrandr --output $(xrandr | grep " connected" | head -1 | cut -d " " -f1)'
|
|
' --brightness $(python3 -c "import subprocess; '
|
|
'b=float(subprocess.check_output([\"xrandr\",\"--verbose\"]).decode()'
|
|
'.split(\"Brightness:\")[1].split()[0]); print(max(0.1,b-0.1))")',
|
|
shell=True, capture_output=True
|
|
)
|
|
else:
|
|
try:
|
|
subprocess.run(
|
|
["powershell", "-Command",
|
|
"(Get-WmiObject -Namespace root/wmi -Class WmiMonitorBrightnessMethods)"
|
|
".WmiSetBrightness(1, [math]::Max(0, "
|
|
"(Get-WmiObject -Namespace root/wmi -Class WmiMonitorBrightness).CurrentBrightness - 10))"],
|
|
capture_output=True, timeout=5, **_WIN_HIDE
|
|
)
|
|
except Exception as e:
|
|
print(f"[Settings] Brightness down failed on Windows: {e}")
|
|
|
|
def close_app():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "q")
|
|
else: pyautogui.hotkey("alt", "f4")
|
|
|
|
def close_window():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "w")
|
|
else: pyautogui.hotkey("ctrl", "w")
|
|
|
|
def full_screen():
|
|
if _OS == "Darwin": pyautogui.hotkey("ctrl", "command", "f")
|
|
else: pyautogui.press("f11")
|
|
|
|
def minimize_window():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "m")
|
|
else: pyautogui.hotkey("win", "down")
|
|
|
|
def maximize_window():
|
|
if _OS == "Darwin":
|
|
subprocess.run(["osascript", "-e",
|
|
'tell application "System Events" to keystroke "f" '
|
|
'using {control down, command down}'],
|
|
capture_output=True)
|
|
elif _OS == "Windows":
|
|
pyautogui.hotkey("win", "up")
|
|
else:
|
|
try:
|
|
subprocess.run(["wmctrl", "-r", ":ACTIVE:", "-b", "add,maximized_vert,maximized_horz"],
|
|
capture_output=True)
|
|
except Exception:
|
|
pyautogui.hotkey("super", "up")
|
|
|
|
def snap_left():
|
|
if _OS == "Windows":
|
|
pyautogui.hotkey("win", "left")
|
|
elif _OS == "Darwin":
|
|
# macOS has no built-in snap; try Rectangle app shortcut if installed
|
|
try:
|
|
subprocess.run(["open", "-a", "Rectangle"], capture_output=True, timeout=1)
|
|
except Exception:
|
|
pass
|
|
pyautogui.hotkey("ctrl", "option", "left")
|
|
else: # Linux
|
|
try:
|
|
subprocess.run(["wmctrl", "-r", ":ACTIVE:", "-e", "0,0,0,960,1080"],
|
|
capture_output=True)
|
|
except Exception:
|
|
pass
|
|
|
|
def snap_right():
|
|
if _OS == "Windows":
|
|
pyautogui.hotkey("win", "right")
|
|
elif _OS == "Darwin":
|
|
try:
|
|
subprocess.run(["open", "-a", "Rectangle"], capture_output=True, timeout=1)
|
|
except Exception:
|
|
pass
|
|
pyautogui.hotkey("ctrl", "option", "right")
|
|
else: # Linux
|
|
try:
|
|
subprocess.run(["wmctrl", "-r", ":ACTIVE:", "-e", "0,960,0,960,1080"],
|
|
capture_output=True)
|
|
except Exception:
|
|
pass
|
|
|
|
def switch_window():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "tab")
|
|
else: pyautogui.hotkey("alt", "tab")
|
|
|
|
def show_desktop():
|
|
if _OS == "Darwin": pyautogui.hotkey("fn", "f11")
|
|
elif _OS == "Windows": pyautogui.hotkey("win", "d")
|
|
else: pyautogui.hotkey("super", "d")
|
|
|
|
def open_task_manager():
|
|
if _OS == "Windows":
|
|
pyautogui.hotkey("ctrl", "shift", "esc")
|
|
elif _OS == "Darwin":
|
|
subprocess.Popen(["open", "-a", "Activity Monitor"])
|
|
else:
|
|
for cmd in [["gnome-system-monitor"], ["xfce4-taskmanager"], ["htop"]]:
|
|
if subprocess.run(["which", cmd[0]], capture_output=True).returncode == 0:
|
|
subprocess.Popen(cmd)
|
|
break
|
|
|
|
|
|
def focus_search():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "l")
|
|
else: pyautogui.hotkey("ctrl", "l")
|
|
|
|
def pause_video(): pyautogui.press("space")
|
|
|
|
def refresh_page():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "r")
|
|
else: pyautogui.press("f5")
|
|
|
|
def close_tab():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "w")
|
|
else: pyautogui.hotkey("ctrl", "w")
|
|
|
|
def new_tab():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "t")
|
|
else: pyautogui.hotkey("ctrl", "t")
|
|
|
|
def next_tab():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "shift", "bracketright")
|
|
else: pyautogui.hotkey("ctrl", "tab")
|
|
|
|
def prev_tab():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "shift", "bracketleft")
|
|
else: pyautogui.hotkey("ctrl", "shift", "tab")
|
|
|
|
def go_back():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "left")
|
|
else: pyautogui.hotkey("alt", "left")
|
|
|
|
def go_forward():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "right")
|
|
else: pyautogui.hotkey("alt", "right")
|
|
|
|
def zoom_in():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "equal")
|
|
else: pyautogui.hotkey("ctrl", "equal")
|
|
|
|
def zoom_out():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "minus")
|
|
else: pyautogui.hotkey("ctrl", "minus")
|
|
|
|
def zoom_reset():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "0")
|
|
else: pyautogui.hotkey("ctrl", "0")
|
|
|
|
def find_on_page():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "f")
|
|
else: pyautogui.hotkey("ctrl", "f")
|
|
|
|
def reload_page_n(n: int):
|
|
for _ in range(max(1, n)):
|
|
refresh_page()
|
|
time.sleep(0.8)
|
|
|
|
|
|
def scroll_up(amount: int = 500): pyautogui.scroll(amount)
|
|
def scroll_down(amount: int = 500): pyautogui.scroll(-amount)
|
|
|
|
def scroll_top():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "up")
|
|
else: pyautogui.hotkey("ctrl", "home")
|
|
|
|
def scroll_bottom():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "down")
|
|
else: pyautogui.hotkey("ctrl", "end")
|
|
|
|
def page_up(): pyautogui.press("pageup")
|
|
def page_down(): pyautogui.press("pagedown")
|
|
|
|
|
|
def copy():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "c")
|
|
else: pyautogui.hotkey("ctrl", "c")
|
|
|
|
def paste():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "v")
|
|
else: pyautogui.hotkey("ctrl", "v")
|
|
|
|
def cut():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "x")
|
|
else: pyautogui.hotkey("ctrl", "x")
|
|
|
|
def undo():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "z")
|
|
else: pyautogui.hotkey("ctrl", "z")
|
|
|
|
def redo():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "shift", "z")
|
|
else: pyautogui.hotkey("ctrl", "y")
|
|
|
|
def select_all():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "a")
|
|
else: pyautogui.hotkey("ctrl", "a")
|
|
|
|
def save_file():
|
|
if _OS == "Darwin": pyautogui.hotkey("command", "s")
|
|
else: pyautogui.hotkey("ctrl", "s")
|
|
|
|
def press_enter(): pyautogui.press("enter")
|
|
def press_escape(): pyautogui.press("escape")
|
|
def press_key(key: str): pyautogui.press(key)
|
|
|
|
def type_text(text: str, press_enter_after: bool = False):
|
|
if not text:
|
|
return
|
|
if _PYPERCLIP:
|
|
pyperclip.copy(str(text))
|
|
time.sleep(0.15)
|
|
paste()
|
|
else:
|
|
pyautogui.write(str(text), interval=0.03)
|
|
if press_enter_after:
|
|
time.sleep(0.1)
|
|
pyautogui.press("enter")
|
|
|
|
def take_screenshot():
|
|
if _OS == "Windows":
|
|
pyautogui.hotkey("win", "shift", "s")
|
|
elif _OS == "Darwin":
|
|
pyautogui.hotkey("command", "shift", "3")
|
|
else:
|
|
for cmd in [["scrot"], ["gnome-screenshot"], ["import", "-window", "root", "screenshot.png"]]:
|
|
if subprocess.run(["which", cmd[0]], capture_output=True).returncode == 0:
|
|
subprocess.Popen(cmd)
|
|
return
|
|
pyautogui.hotkey("ctrl", "print_screen")
|
|
|
|
def lock_screen():
|
|
if _OS == "Windows":
|
|
pyautogui.hotkey("win", "l")
|
|
elif _OS == "Darwin":
|
|
subprocess.run(["pmset", "displaysleepnow"], capture_output=True)
|
|
else:
|
|
for cmd in [
|
|
["gnome-screensaver-command", "-l"],
|
|
["xdg-screensaver", "lock"],
|
|
["loginctl", "lock-session"],
|
|
]:
|
|
if subprocess.run(["which", cmd[0]], capture_output=True).returncode == 0:
|
|
subprocess.run(cmd, capture_output=True)
|
|
return
|
|
|
|
def open_system_settings():
|
|
if _OS == "Windows":
|
|
pyautogui.hotkey("win", "i")
|
|
elif _OS == "Darwin":
|
|
subprocess.Popen(["open", "-a", "System Preferences"])
|
|
else:
|
|
for cmd in [["gnome-control-center"], ["xfce4-settings-manager"], ["kcmshell5"]]:
|
|
if subprocess.run(["which", cmd[0]], capture_output=True).returncode == 0:
|
|
subprocess.Popen(cmd)
|
|
return
|
|
|
|
def open_file_explorer():
|
|
if _OS == "Windows":
|
|
pyautogui.hotkey("win", "e")
|
|
elif _OS == "Darwin":
|
|
subprocess.Popen(["open", str(Path.home())])
|
|
else:
|
|
for cmd in [["nautilus"], ["thunar"], ["dolphin"], ["nemo"]]:
|
|
if subprocess.run(["which", cmd[0]], capture_output=True).returncode == 0:
|
|
subprocess.Popen(cmd)
|
|
return
|
|
subprocess.Popen(["xdg-open", str(Path.home())])
|
|
|
|
def sleep_display():
|
|
if _OS == "Windows":
|
|
try:
|
|
import ctypes
|
|
ctypes.windll.user32.SendMessageW(0xFFFF, 0x0112, 0xF170, 2)
|
|
except Exception as e:
|
|
print(f"[Settings] sleep_display failed: {e}")
|
|
elif _OS == "Darwin":
|
|
subprocess.run(["pmset", "displaysleepnow"], capture_output=True)
|
|
else:
|
|
subprocess.run(["xset", "dpms", "force", "off"], capture_output=True)
|
|
|
|
def open_run():
|
|
if _OS == "Windows":
|
|
pyautogui.hotkey("win", "r")
|
|
|
|
def dark_mode():
|
|
if _OS == "Darwin":
|
|
subprocess.run(["osascript", "-e",
|
|
'tell app "System Events" to tell appearance preferences '
|
|
'to set dark mode to not dark mode'],
|
|
capture_output=True)
|
|
elif _OS == "Windows":
|
|
try:
|
|
import winreg
|
|
key_path = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize"
|
|
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path, 0, winreg.KEY_ALL_ACCESS)
|
|
current, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
|
|
winreg.SetValueEx(key, "AppsUseLightTheme", 0, winreg.REG_DWORD, 1 - current)
|
|
winreg.SetValueEx(key, "SystemUsesLightTheme", 0, winreg.REG_DWORD, 1 - current)
|
|
winreg.CloseKey(key)
|
|
except Exception as e:
|
|
print(f"[Settings] dark_mode registry failed: {e}")
|
|
else:
|
|
try:
|
|
result = subprocess.run(
|
|
["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"],
|
|
capture_output=True, text=True
|
|
)
|
|
current = result.stdout.strip()
|
|
new_scheme = "'default'" if "dark" in current else "'prefer-dark'"
|
|
subprocess.run(
|
|
["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", new_scheme],
|
|
capture_output=True
|
|
)
|
|
except Exception as e:
|
|
print(f"[Settings] dark_mode Linux failed: {e}")
|
|
|
|
def toggle_wifi():
|
|
if _OS == "Darwin":
|
|
iface = _get_macos_wifi_interface()
|
|
result = subprocess.run(
|
|
["networksetup", "-getairportpower", iface],
|
|
capture_output=True, text=True
|
|
)
|
|
state = "off" if "On" in result.stdout else "on"
|
|
subprocess.run(["networksetup", "-setairportpower", iface, state],
|
|
capture_output=True)
|
|
elif _OS == "Windows":
|
|
try:
|
|
subprocess.run(
|
|
["powershell", "-Command",
|
|
"$adapter = Get-NetAdapter | Where-Object {$_.PhysicalMediaType -eq 'Native 802.11'};"
|
|
"if ($adapter.Status -eq 'Up') { Disable-NetAdapter -Name $adapter.Name -Confirm:$false }"
|
|
"else { Enable-NetAdapter -Name $adapter.Name -Confirm:$false }"],
|
|
capture_output=True, timeout=10, **_WIN_HIDE
|
|
)
|
|
except Exception as e:
|
|
print(f"[Settings] toggle_wifi Windows failed: {e}")
|
|
else:
|
|
try:
|
|
result = subprocess.run(["nmcli", "radio", "wifi"], capture_output=True, text=True)
|
|
state = "off" if "enabled" in result.stdout else "on"
|
|
subprocess.run(["nmcli", "radio", "wifi", state], capture_output=True)
|
|
except Exception as e:
|
|
print(f"[Settings] toggle_wifi Linux failed: {e}")
|
|
|
|
def restart_computer():
|
|
if _OS == "Windows":
|
|
subprocess.run(["shutdown", "/r", "/t", "10"], capture_output=True, **_WIN_HIDE)
|
|
elif _OS == "Darwin":
|
|
subprocess.run(["osascript", "-e",
|
|
'tell application "System Events" to restart'],
|
|
capture_output=True)
|
|
else:
|
|
subprocess.run(["systemctl", "reboot"], capture_output=True)
|
|
|
|
def shutdown_computer():
|
|
if _OS == "Windows":
|
|
subprocess.run(["shutdown", "/s", "/t", "10"], capture_output=True)
|
|
elif _OS == "Darwin":
|
|
subprocess.run(["osascript", "-e",
|
|
'tell application "System Events" to shut down'],
|
|
capture_output=True)
|
|
else:
|
|
subprocess.run(["systemctl", "poweroff"], capture_output=True)
|
|
|
|
ACTION_MAP: dict[str, callable] = {
|
|
"volume_up": volume_up,
|
|
"volume_down": volume_down,
|
|
"mute": volume_mute,
|
|
"unmute": volume_mute,
|
|
"toggle_mute": volume_mute,
|
|
"brightness_up": brightness_up,
|
|
"brightness_down": brightness_down,
|
|
"sleep_display": sleep_display,
|
|
"screen_off": sleep_display,
|
|
"pause_video": pause_video,
|
|
"play_pause": pause_video,
|
|
"close_app": close_app,
|
|
"close_window": close_window,
|
|
"full_screen": full_screen,
|
|
"fullscreen": full_screen,
|
|
"minimize": minimize_window,
|
|
"maximize": maximize_window,
|
|
"snap_left": snap_left,
|
|
"snap_right": snap_right,
|
|
"switch_window": switch_window,
|
|
"show_desktop": show_desktop,
|
|
"task_manager": open_task_manager,
|
|
"focus_search": focus_search,
|
|
"refresh_page": refresh_page,
|
|
"reload": refresh_page,
|
|
"close_tab": close_tab,
|
|
"new_tab": new_tab,
|
|
"next_tab": next_tab,
|
|
"prev_tab": prev_tab,
|
|
"go_back": go_back,
|
|
"go_forward": go_forward,
|
|
"zoom_in": zoom_in,
|
|
"zoom_out": zoom_out,
|
|
"zoom_reset": zoom_reset,
|
|
"find_on_page": find_on_page,
|
|
"scroll_up": scroll_up,
|
|
"scroll_down": scroll_down,
|
|
"scroll_top": scroll_top,
|
|
"scroll_bottom": scroll_bottom,
|
|
"page_up": page_up,
|
|
"page_down": page_down,
|
|
"copy": copy,
|
|
"paste": paste,
|
|
"cut": cut,
|
|
"undo": undo,
|
|
"redo": redo,
|
|
"select_all": select_all,
|
|
"save": save_file,
|
|
"enter": press_enter,
|
|
"escape": press_escape,
|
|
"screenshot": take_screenshot,
|
|
"lock_screen": lock_screen,
|
|
"open_settings": open_system_settings,
|
|
"file_explorer": open_file_explorer,
|
|
"open_run": open_run,
|
|
"dark_mode": dark_mode,
|
|
"toggle_wifi": toggle_wifi,
|
|
"restart": restart_computer,
|
|
"shutdown": shutdown_computer,
|
|
}
|
|
|
|
# ── What needs a human, and what just needs an undo ──────────────────────────
|
|
#
|
|
# The old gate was `_DANGEROUS_ACTIONS = {"restart", "shutdown"}` checked against
|
|
# a `confirmed` parameter the MODEL filled in — so the model confirmed its own
|
|
# shutdowns, and everything else (switching off the WiFi this assistant is
|
|
# talking over) had no gate at all.
|
|
#
|
|
# Two lists now, and the split is about reversibility, not about how alarming
|
|
# the word sounds:
|
|
#
|
|
# _IRREVERSIBLE — a human presses a button on the HUD. Nothing else.
|
|
# everything else — done immediately, with an undo pushed if it can be undone.
|
|
#
|
|
# Asking before every action is what makes an assistant unusable, and every
|
|
# question costs a round trip. Undo is both faster and safer than a prompt.
|
|
_IRREVERSIBLE = {
|
|
"restart": ("Restart this computer",
|
|
"Anything unsaved will be lost. The computer restarts in 10 seconds."),
|
|
"shutdown": ("Shut this computer down",
|
|
"Anything unsaved will be lost. The computer powers off in 10 seconds."),
|
|
# Not obviously destructive, and that is exactly why it was missed: turning
|
|
# the WiFi off cuts the assistant's own connection to the Live API, so it
|
|
# cannot be asked to turn it back on.
|
|
"toggle_wifi": ("Switch WiFi off or on",
|
|
"If this switches WiFi off, JARVIS loses its connection and "
|
|
"cannot switch it back on by voice."),
|
|
}
|
|
|
|
# Kept so anything still importing the old name keeps working.
|
|
_DANGEROUS_ACTIONS = set(_IRREVERSIBLE)
|
|
|
|
|
|
# ── Local intent resolution ──────────────────────────────────────────────────
|
|
#
|
|
# This used to be an entire extra Gemini call, made INSIDE the tool: the Live
|
|
# model called computer_settings, and computer_settings then asked a second
|
|
# model which action was meant. Every "turn the volume down" paid for two round
|
|
# trips — and when that second call failed, the fallback was
|
|
# `description.lower().replace(" ", "_")`, which turns the Turkish for "turn it
|
|
# down" into `sesi_kis`, i.e. straight to "Unknown action".
|
|
#
|
|
# Nothing here needs a language model. The Live model already understands the
|
|
# sentence; it only needed the vocabulary, which the tool declaration now spells
|
|
# out in full. What is left is spelling tolerance, and difflib does that in
|
|
# microseconds instead of ~600 ms and a quota unit.
|
|
_ALIASES = {
|
|
"volume_up": ("louder", "raise volume", "turn it up", "increase volume"),
|
|
"volume_down": ("quieter", "lower volume", "turn it down", "decrease volume"),
|
|
"mute": ("silence", "sound off", "no sound"),
|
|
"brightness_up": ("brighter", "raise brightness", "increase brightness"),
|
|
"brightness_down": ("dimmer", "dim", "lower brightness", "decrease brightness"),
|
|
"close_window": ("close this", "close it"),
|
|
"full_screen": ("fullscreen", "maximise screen"),
|
|
"show_desktop": ("minimise everything", "go to desktop"),
|
|
"lock_screen": ("lock", "lock the pc", "lock computer"),
|
|
"sleep_display": ("screen off", "turn off the screen", "display off"),
|
|
"dark_mode": ("night mode", "light mode", "toggle theme"),
|
|
"toggle_wifi": ("wifi", "wi-fi", "internet off", "internet on"),
|
|
"task_manager": ("processes", "task list"),
|
|
"screenshot": ("capture screen", "take a screenshot", "snip"),
|
|
"refresh_page": ("refresh", "reload page"),
|
|
"new_tab": ("open a tab", "open new tab"),
|
|
"shutdown": ("power off", "turn off the computer", "switch off the pc"),
|
|
"restart": ("reboot", "restart the pc"),
|
|
}
|
|
|
|
_VALUE_ACTIONS = {"volume_set", "type_text", "press_key", "reload_n",
|
|
"scroll_up", "scroll_down"}
|
|
|
|
|
|
def _normalise(text: str) -> str:
|
|
return (text or "").strip().lower().replace("-", "_").replace(" ", "_")
|
|
|
|
|
|
def _detect_action(description: str) -> dict:
|
|
"""Resolve a free-text description to an action name, locally.
|
|
|
|
Returns {"action": name, "value": ...}; `action` is "" when nothing matched,
|
|
which the caller turns into a message naming real candidates — one round
|
|
trip, and only in the case that used to cost one anyway."""
|
|
raw = (description or "").strip()
|
|
norm = _normalise(raw)
|
|
if not norm:
|
|
return {"action": "", "value": None}
|
|
|
|
known = set(ACTION_MAP) | _VALUE_ACTIONS
|
|
|
|
# 1. Already an action name.
|
|
if norm in known:
|
|
return {"action": norm, "value": None}
|
|
|
|
low = raw.lower()
|
|
|
|
# 2. "set volume to 30", "sesi 30 yap" — a number next to a volume word.
|
|
num = re.search(r"(\d{1,3})\s*%?", low)
|
|
if num and any(w in low for w in ("volume", "ses", "sound", "lautstark", "громкость")):
|
|
return {"action": "volume_set", "value": max(0, min(100, int(num.group(1))))}
|
|
|
|
# 3. Alias phrases.
|
|
for action, phrases in _ALIASES.items():
|
|
if any(_normalise(p) == norm or p in low for p in phrases):
|
|
return {"action": action, "value": None}
|
|
|
|
# 4. Fuzzy match on the action names — catches "fullscren", "volumeup".
|
|
import difflib
|
|
close = difflib.get_close_matches(norm, sorted(known), n=1, cutoff=0.72)
|
|
if close:
|
|
return {"action": close[0], "value": None}
|
|
|
|
# 5. Substring: "increase_the_brightness" contains "brightness".
|
|
for action in sorted(known, key=len, reverse=True):
|
|
if len(action) > 4 and (action in norm or norm in action):
|
|
return {"action": action, "value": None}
|
|
|
|
return {"action": "", "value": None}
|
|
|
|
|
|
def _suggest(description: str) -> str:
|
|
"""What to tell the model when nothing matched. Names real actions so its
|
|
retry lands, instead of the old 'Unknown action' dead end."""
|
|
import difflib
|
|
near = difflib.get_close_matches(_normalise(description),
|
|
sorted(ACTION_MAP), n=5, cutoff=0.3)
|
|
hint = ", ".join(near) if near else ", ".join(sorted(ACTION_MAP)[:12])
|
|
return (f"I could not match '{description}' to a computer action. "
|
|
f"Call computer_settings again with an exact `action` from: {hint}.")
|
|
|
|
def computer_settings(
|
|
parameters: dict = None,
|
|
response=None,
|
|
player=None,
|
|
session_memory=None,
|
|
) -> str:
|
|
if not _PYAUTOGUI:
|
|
return "pyautogui is not installed. Run: pip install pyautogui"
|
|
|
|
params = parameters or {}
|
|
raw_action = params.get("action", "").strip()
|
|
description = params.get("description", "").strip()
|
|
value = params.get("value", None)
|
|
|
|
if not raw_action and description:
|
|
detected = _detect_action(description)
|
|
raw_action = detected.get("action", "")
|
|
if value is None:
|
|
value = detected.get("value")
|
|
|
|
action = raw_action.lower().strip().replace(" ", "_").replace("-", "_")
|
|
|
|
if not action:
|
|
return _suggest(description or raw_action)
|
|
|
|
print(f"[Settings] Action: {action} Value: {value} OS: {_OS}")
|
|
if player:
|
|
player.write_log(f"[Settings] {action}")
|
|
|
|
# ── The gate ─────────────────────────────────────────────────────────────
|
|
# A human presses a button, or this does not happen. The model can no longer
|
|
# write its own permission slip, and the action itself is handed to the UI
|
|
# rather than performed here — so returning early is not "declining", it is
|
|
# "parked until someone says yes".
|
|
if action in _IRREVERSIBLE:
|
|
title, detail = _IRREVERSIBLE[action]
|
|
func = ACTION_MAP.get(action)
|
|
if func is None:
|
|
return f"Unknown action: '{raw_action}'."
|
|
if confirm.pending_title():
|
|
return ("There is already a confirmation waiting on screen. "
|
|
"Ask the user to answer that one first.")
|
|
return confirm.request(
|
|
key=action, title=title, detail=detail,
|
|
run=lambda f=func, a=action: (f(), f"{a} done.")[1],
|
|
)
|
|
|
|
if action == "volume_set":
|
|
try:
|
|
target = int(value if value is not None else 50)
|
|
before = volume_get()
|
|
volume_set(target)
|
|
if before is not None:
|
|
push_undo(f"volume {before}% → {target}%",
|
|
lambda b=before: (volume_set(b), f"Back to {b}%.")[1])
|
|
return f"Volume set to {target}%."
|
|
except Exception as e:
|
|
return f"Could not set volume: {e}"
|
|
|
|
if action in ("type_text", "write_on_screen", "type", "write"):
|
|
text = str(value or params.get("text", "")).strip()
|
|
if not text:
|
|
return "No text provided to type."
|
|
enter_after = str(params.get("press_enter", "false")).lower() in ("true", "1", "yes")
|
|
type_text(text, press_enter_after=enter_after)
|
|
return f"Typed: {text[:80]}"
|
|
|
|
if action == "press_key":
|
|
key = str(value or params.get("key", "")).strip()
|
|
if not key:
|
|
return "No key specified."
|
|
press_key(key)
|
|
return f"Pressed: {key}"
|
|
|
|
if action in ("reload_n", "refresh_n", "reload_page_n"):
|
|
try:
|
|
reload_page_n(int(value or 1))
|
|
return f"Reloaded {value or 1} time(s)."
|
|
except Exception as e:
|
|
return f"Reload failed: {e}"
|
|
|
|
if action == "scroll_up":
|
|
scroll_up(int(value or 500))
|
|
return "Scrolled up."
|
|
|
|
if action == "scroll_down":
|
|
scroll_down(int(value or 500))
|
|
return "Scrolled down."
|
|
|
|
func = ACTION_MAP.get(action)
|
|
if not func:
|
|
return _suggest(raw_action or description)
|
|
|
|
# ── Capture "before" so the change can be taken back ─────────────────────
|
|
# Read-then-write is the whole mechanism for settings: there is no clever
|
|
# inverse to compute, just a value to remember. Where the platform will not
|
|
# tell us the current value, nothing is registered — an undo that restores
|
|
# a guess is worse than no undo at all.
|
|
_before = None
|
|
if action in ("volume_up", "volume_down", "mute", "unmute", "toggle_mute"):
|
|
_before = ("volume", volume_get())
|
|
elif action in ("brightness_up", "brightness_down"):
|
|
_before = ("brightness", brightness_get())
|
|
|
|
try:
|
|
func()
|
|
except Exception as e:
|
|
print(f"[Settings] Action failed ({action}): {e}")
|
|
return f"Action failed ({action}): {e}"
|
|
|
|
if _before:
|
|
kind, old = _before
|
|
if old is not None:
|
|
if kind == "volume":
|
|
push_undo(f"volume ({action})",
|
|
lambda b=old: (volume_set(b), f"Volume back to {b}%.")[1])
|
|
elif kind == "brightness":
|
|
push_undo(f"brightness ({action})",
|
|
lambda b=old: (brightness_set(b), f"Brightness back to {b}%.")[1])
|
|
elif action == "dark_mode":
|
|
# A pure toggle: calling it again is the undo.
|
|
push_undo("dark mode toggled",
|
|
lambda: (dark_mode(), "Theme switched back.")[1])
|
|
|
|
return f"Done: {action}."
|
|
|
|
|
|
# ── Tool declaration (auto-discovered by core/action_loader.py) ──────────────
|
|
TOOL = {
|
|
"name": "computer_settings",
|
|
"description": "Controls the computer: volume, brightness, window management, keyboard shortcuts, typing text on screen, closing apps, fullscreen, dark mode, WiFi, restart, shutdown, scrolling, tab management, zoom, screenshots, lock screen, refresh/reload page. Use for ANY single computer control command. restart, shutdown and toggle_wifi put a confirmation on the user's screen and do NOT happen until they press it — never claim they are done. Volume, brightness and dark mode can be reversed with the `undo` tool.",
|
|
"parameters": {
|
|
"type": "OBJECT",
|
|
"properties": {
|
|
# The exact vocabulary, spelled out.
|
|
#
|
|
# This used to say only "The action to perform", so the model
|
|
# usually filled `description` instead — and computer_settings then
|
|
# made a SECOND Gemini call, inside the tool, purely to translate
|
|
# that sentence into one of these names. Every "turn the volume
|
|
# down" cost two model round trips.
|
|
#
|
|
# Naming the actions here costs ~600 characters once per session and
|
|
# removes a whole round trip from every computer command.
|
|
"action": {
|
|
"type": "STRING",
|
|
"description": (
|
|
"The exact action. Prefer this over `description` — pick one of: "
|
|
"volume_up | volume_down | volume_set | mute | "
|
|
"brightness_up | brightness_down | sleep_display | "
|
|
"pause_video | close_app | close_window | full_screen | "
|
|
"minimize | maximize | snap_left | snap_right | "
|
|
"switch_window | show_desktop | task_manager | focus_search | "
|
|
"refresh_page | close_tab | new_tab | next_tab | prev_tab | "
|
|
"go_back | go_forward | zoom_in | zoom_out | zoom_reset | "
|
|
"find_on_page | scroll_up | scroll_down | scroll_top | "
|
|
"scroll_bottom | page_up | page_down | copy | paste | cut | "
|
|
"undo | redo | select_all | save | enter | escape | press_key | "
|
|
"type_text | screenshot | lock_screen | open_settings | "
|
|
"file_explorer | open_run | dark_mode | toggle_wifi | "
|
|
"restart | shutdown"
|
|
)
|
|
},
|
|
"description": {
|
|
"type": "STRING",
|
|
"description": (
|
|
"Fallback only, when no action name above fits. "
|
|
"Resolved locally — no extra model call."
|
|
)
|
|
},
|
|
"value": {
|
|
"type": "STRING",
|
|
"description": "Optional value: volume level 0-100, text to type, key name, etc."
|
|
}
|
|
},
|
|
"required": []
|
|
},
|
|
"handler": computer_settings,
|
|
}
|