diff --git a/src/siliv/app.py b/src/siliv/app.py index d3d4314..b9b9d7b 100644 --- a/src/siliv/app.py +++ b/src/siliv/app.py @@ -6,9 +6,9 @@ # PyQt6 Imports from PyQt6.QtWidgets import ( - QApplication, QSystemTrayIcon, QMenu, QMessageBox, - QWidgetAction -) + QApplication, QSystemTrayIcon, QMenu, QMessageBox, + QWidgetAction, QCheckBox + ) # Import QSettings from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSignal, QSettings from PyQt6.QtGui import QIcon, QCursor, QAction @@ -22,6 +22,7 @@ ORGANIZATION_NAME = "SilivProject" # Or your preferred organization name APPLICATION_NAME = "Siliv" SAVED_VRAM_KEY = "user/savedVramMb" +LAUNCH_AT_LOGIN_KEY = "user/launchAtLogin" # -------------------------- class MenuBarApp(QObject): @@ -57,6 +58,8 @@ def __init__(self, icon_path, parent=None): self.slider_value_action = None # The "Apply X GB" action self.refresh_action = None self.quit_action = None + self.launch_at_login_checkbox = None + self.launch_at_login_action = None # --- Application Setup --- self.app = QApplication.instance() @@ -301,11 +304,21 @@ def create_menu_actions(self): self.slider_widget_action = QWidgetAction(self.menu) self.slider_widget_action.setDefaultWidget(self.slider_widget) self.menu.addAction(self.slider_widget_action) - + self.slider_value_action = QAction("Allocate ... GB VRAM") self.slider_value_action.setEnabled(False) self.slider_value_action.triggered.connect(self.apply_slider_value_from_action) self.menu.addAction(self.slider_value_action) + + # --- Launch at Login --- + self.launch_at_login_checkbox = QCheckBox("Launch at Login") + saved_launch_pref = self.settings.value(LAUNCH_AT_LOGIN_KEY, False, type=bool) + self.launch_at_login_checkbox.setChecked(bool(saved_launch_pref)) + self.launch_at_login_checkbox.stateChanged.connect(self.handle_launch_at_login_toggled) + self.launch_at_login_action = QWidgetAction(self.menu) + self.launch_at_login_action.setDefaultWidget(self.launch_at_login_checkbox) + self.menu.addAction(self.launch_at_login_action) + self.menu.addSeparator() # --- Other Actions --- @@ -529,6 +542,21 @@ def set_preset_vram(self, value_mb): self.update_menu_items() self._set_vram_and_update(value_mb) + # --- Launch at Login handler --- + def handle_launch_at_login_toggled(self, state): + """Enables/Disables autostart when checkbox is toggled.""" + enabled = (state == Qt.CheckState.Checked) + success, message = utils.set_launch_at_login(enabled) + if success: + self.settings.setValue(LAUNCH_AT_LOGIN_KEY, enabled) + self.settings.sync() + else: + QMessageBox.warning(None, "Launch at Login", message) + # Revert state silently + self.launch_at_login_checkbox.blockSignals(True) + self.launch_at_login_checkbox.setChecked(not enabled) + self.launch_at_login_checkbox.blockSignals(False) + # --- Startup Application Method --- def apply_saved_vram_on_startup(self): # ... (This method remains unchanged from the previous version, but uses updated clamping) ... @@ -591,4 +619,4 @@ def quit_app(self): print("[App] Quitting application...") self.refresh_timer.stop() self.tray_icon.hide() - self.app.quit() \ No newline at end of file + self.app.quit() diff --git a/src/siliv/cli.py b/src/siliv/cli.py new file mode 100644 index 0000000..9f6187d --- /dev/null +++ b/src/siliv/cli.py @@ -0,0 +1,108 @@ +""" +Siliv – Command-line Interface +Run with: + python -m siliv.cli status + python -m siliv.cli set 8192 # 8 GB in MB + python -m siliv.cli set 8G # 8 GB shorthand + python -m siliv.cli default +""" +from __future__ import annotations + +import argparse +import sys +import re +from typing import Tuple + +from siliv import utils + + +def _parse_size(text: str) -> int: + """ + Parse a size string to MB. + 8192 -> 8192 MB + 8G / 8g -> 8192 MB + 8GB / 8gb -> 8192 MB + """ + txt = text.strip() + m = re.fullmatch(r"(\d+)([gG][bB]?|)", txt) + if not m: + raise argparse.ArgumentTypeError(f"Invalid size value '{text}'") + value = int(m.group(1)) + if m.group(2): # has G suffix + return value * 1024 + return value + + +# --------------------------------------------------------------------------- # +# helper printers +# --------------------------------------------------------------------------- # +def _format_mb(mb: int) -> str: + return f"{mb / 1024:.1f} GB ({mb} MB)" + + +def status_cmd(_: argparse.Namespace) -> None: + """Display total RAM, current VRAM, default VRAM and reserved RAM.""" + total_mb = utils.get_total_ram_mb() or 0 + current_mb = utils.get_current_vram_mb(total_mb) + default_mb = utils.calculate_default_vram_mb(total_mb) + reserved_mb = max(0, total_mb - current_mb) + + print("Siliv – VRAM status") + print("-------------------") + print(f"Total : {_format_mb(total_mb)}") + print(f"Current : {_format_mb(current_mb)}") + print(f"Default : {_format_mb(default_mb)}") + print(f"Reserved : {_format_mb(reserved_mb)}") + + +def set_cmd(args: argparse.Namespace) -> None: + """Set VRAM to the requested value (in MB).""" + target_mb = args.value_mb + ok, msg = utils.set_vram_mb(target_mb) + if ok: + print(f"✔ Set VRAM to {_format_mb(target_mb)}") + else: + print(f"✖ Failed to set VRAM – {msg}", file=sys.stderr) + sys.exit(1) + + +def default_cmd(_: argparse.Namespace) -> None: + """Reset VRAM to macOS default (0).""" + ok, msg = utils.set_vram_mb(0) + if ok: + print("✔ VRAM reset to macOS default (0)") + else: + print(f"✖ Failed to reset VRAM – {msg}", file=sys.stderr) + sys.exit(1) + + +# --------------------------------------------------------------------------- # +# main / argparse boilerplate +# --------------------------------------------------------------------------- # +def build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser(prog="siliv-cli", description="Siliv command-line interface") + sub = p.add_subparsers(dest="command", required=True) + + # status + sp = sub.add_parser("status", help="Show current VRAM / RAM information") + sp.set_defaults(func=status_cmd) + + # set + sp = sub.add_parser("set", help="Set VRAM to the specified amount (MB / GB)") + sp.add_argument("value_mb", type=_parse_size, help="Value to set (e.g. 8192 or 8G)") + sp.set_defaults(func=set_cmd) + + # default + sp = sub.add_parser("default", help="Reset VRAM to macOS default") + sp.set_defaults(func=default_cmd) + return p + + +def main(argv: list[str] | None = None) -> None: + parser = build_parser() + ns = parser.parse_args(argv) + ns.func(ns) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/siliv/utils.py b/src/siliv/utils.py index b4f90ef..be0eea3 100644 --- a/src/siliv/utils.py +++ b/src/siliv/utils.py @@ -171,4 +171,62 @@ def set_vram_mb(value_mb): error_msg = f"An exception occurred trying to set VRAM: {e}" print(error_msg) QMessageBox.critical(None, "VRAM Set Error", error_msg) - return False, error_msg \ No newline at end of file + return False, error_msg + +# --------------------------------------------------------------------------- +# Launch at Login helpers +# --------------------------------------------------------------------------- +def set_launch_at_login(enabled): + """ + Enables or disables launching Siliv automatically when the user logs in + by creating or removing a LaunchAgent plist in the user's LaunchAgents + folder. + + Args: + enabled (bool): True to enable autostart, False to disable. + + Returns: + tuple(bool success, str message) + """ + if platform.system() != "Darwin": + return False, "Launch at login only supported on macOS." + + plist_dir = os.path.expanduser("~/Library/LaunchAgents") + plist_path = os.path.join(plist_dir, "com.siliv.vramtool.plist") + + # Determine executable path: when bundled with PyInstaller this resolves + # to the deployed binary; during development it will be the Python script. + exec_path = os.path.abspath(sys.argv[0]) + + if enabled: + try: + os.makedirs(plist_dir, exist_ok=True) + plist_content = f''' + + + + Label + com.siliv.vramtool + ProgramArguments + + {exec_path} + + RunAtLoad + + +''' + with open(plist_path, "w", encoding="utf-8") as fp: + fp.write(plist_content) + # Load the agent; ignore non-zero exit codes if already loaded + subprocess.run(["launchctl", "load", plist_path], check=False) + return True, "Enabled launch at login" + except Exception as e: + return False, f"Failed to enable launch at login: {e}" + else: + try: + subprocess.run(["launchctl", "unload", plist_path], check=False) + if os.path.exists(plist_path): + os.remove(plist_path) + return True, "Disabled launch at login" + except Exception as e: + return False, f"Failed to disable launch at login: {e}"