#!/usr/bin/env python3 """Acer RGB Keyboard Tray Applet - Menu-based controls.""" import gi gi.require_version("Gtk", "3.0") gi.require_version("AppIndicator3", "0.1") from gi.repository import Gtk, AppIndicator3, Gdk from daemon_client import ( RGBState, EFFECTS, is_daemon_running, get_states, set_rgb ) # Color presets (name, r, g, b) COLOR_PRESETS = [ ("Red", 255, 0, 0), ("Green", 0, 255, 0), ("Blue", 0, 0, 255), ("White", 255, 255, 255), ("Cyan", 0, 255, 255), ("Magenta", 255, 0, 255), ("Yellow", 255, 255, 0), ("Orange", 255, 128, 0), ] class AcerRGBTray: """System tray application with menu-based controls.""" def __init__(self): self.current_state = None self.indicator = AppIndicator3.Indicator.new( "acer-rgb-tray", "media-view-subtitles-symbolic", AppIndicator3.IndicatorCategory.HARDWARE ) self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) self.load_state() self.build_menu() def build_menu(self): """Build the dropdown menu with all controls.""" menu = Gtk.Menu() # Status line at top self.status_item = Gtk.MenuItem(label="Loading...") self.status_item.set_sensitive(False) menu.append(self.status_item) menu.append(Gtk.SeparatorMenuItem()) # Color presets directly in menu for name, r, g, b in COLOR_PRESETS: item = Gtk.MenuItem(label=name) item.connect("activate", self.on_color_preset, r, g, b) menu.append(item) # Custom color option custom_item = Gtk.MenuItem(label="Custom...") custom_item.connect("activate", self.on_custom_color) menu.append(custom_item) menu.append(Gtk.SeparatorMenuItem()) # Effect submenu effect_item = Gtk.MenuItem(label="Effect") effect_submenu = Gtk.Menu() self.effect_group = [] for effect_id, effect_name in EFFECTS: item = Gtk.RadioMenuItem(label=effect_name) if self.effect_group: item.set_property("group", self.effect_group[0]) self.effect_group.append(item) item.connect("toggled", self.on_effect_changed, effect_id) effect_submenu.append(item) effect_item.set_submenu(effect_submenu) menu.append(effect_item) menu.show_all() self.indicator.set_menu(menu) self.update_ui_from_state() def load_state(self): """Load current state from daemon.""" if not is_daemon_running(): self.current_state = RGBState(device="keyboard") return try: states = get_states() if "keyboard" in states: self.current_state = states["keyboard"] else: self.current_state = RGBState(device="keyboard") except Exception: self.current_state = RGBState(device="keyboard") def update_ui_from_state(self): """Update menu items to reflect current state.""" if self.current_state is None: self.status_item.set_label("Daemon not running") return state = self.current_state # Update status line color_name = self.get_color_name(state.r, state.g, state.b) effect_name = dict(EFFECTS).get(state.effect, state.effect) self.status_item.set_label(f"{color_name} | {effect_name}") # Update effect radio buttons for i, (effect_id, _) in enumerate(EFFECTS): if effect_id == state.effect: self.effect_group[i].set_active(True) break def get_color_name(self, r, g, b): """Get color name from RGB values, or hex if not a preset.""" for name, pr, pg, pb in COLOR_PRESETS: if (r, g, b) == (pr, pg, pb): return name return f"#{r:02X}{g:02X}{b:02X}" def on_color_preset(self, item, r, g, b): """Handle color preset selection.""" self.current_state.r = r self.current_state.g = g self.current_state.b = b self.apply_state() def on_custom_color(self, item): """Show color chooser dialog.""" dialog = Gtk.ColorChooserDialog( title="Choose Color", transient_for=None ) dialog.set_use_alpha(False) dialog.set_rgba(Gdk.RGBA( self.current_state.r / 255, self.current_state.g / 255, self.current_state.b / 255, 1.0 )) if dialog.run() == Gtk.ResponseType.OK: rgba = dialog.get_rgba() self.current_state.r = int(rgba.red * 255) self.current_state.g = int(rgba.green * 255) self.current_state.b = int(rgba.blue * 255) self.apply_state() dialog.destroy() def on_effect_changed(self, item, effect_id): """Handle effect selection.""" if not item.get_active(): return self.current_state.effect = effect_id if effect_id == "static": self.current_state.speed = 0 else: self.current_state.speed = 5 self.apply_state() def apply_state(self): """Apply current state to daemon.""" self.current_state.device = "keyboard" success, msg = set_rgb(self.current_state) if success: self.update_ui_from_state() else: self.status_item.set_label(f"Error: {msg}") def run(self): Gtk.main() if __name__ == "__main__": app = AcerRGBTray() app.run()