Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions src/siliv/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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 ---
Expand Down Expand Up @@ -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) ...
Expand Down Expand Up @@ -591,4 +619,4 @@ def quit_app(self):
print("[App] Quitting application...")
self.refresh_timer.stop()
self.tray_icon.hide()
self.app.quit()
self.app.quit()
108 changes: 108 additions & 0 deletions src/siliv/cli.py
Original file line number Diff line number Diff line change
@@ -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()
60 changes: 59 additions & 1 deletion src/siliv/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
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'''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.siliv.vramtool</string>
<key>ProgramArguments</key>
<array>
<string>{exec_path}</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>'''
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}"