Add pystray tray applet wiring and [tray] extra
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2fce7d412a
commit
1451cf7d58
4 changed files with 149 additions and 0 deletions
8
enodia_sentinel/tray/__main__.py
Normal file
8
enodia_sentinel/tray/__main__.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""`python -m enodia_sentinel.tray` entry point."""
|
||||
import sys
|
||||
|
||||
from .app import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
109
enodia_sentinel/tray/app.py
Normal file
109
enodia_sentinel/tray/app.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""System-tray applet wiring. Imports pystray/PIL (the [tray] extra).
|
||||
|
||||
This module is the GUI shell only. All testable logic lives in state.py,
|
||||
actions.py, and notify.py, which import no GUI libraries.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pystray
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from ..config import Config
|
||||
from . import actions, notify
|
||||
from .state import TrayState, parse_status
|
||||
|
||||
REFRESH_SECONDS = 15
|
||||
|
||||
_COLORS = {
|
||||
"green": (46, 160, 67),
|
||||
"amber": (210, 153, 34),
|
||||
"grey": (110, 110, 110),
|
||||
}
|
||||
|
||||
|
||||
def _make_image(color: str) -> "Image.Image":
|
||||
rgb = _COLORS.get(color, _COLORS["grey"])
|
||||
img = Image.new("RGBA", (64, 64), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw.ellipse((8, 8, 56, 56), fill=rgb)
|
||||
return img
|
||||
|
||||
|
||||
class TrayApp:
|
||||
def __init__(self, cfg: Config):
|
||||
self.cfg = cfg
|
||||
self.state: TrayState = parse_status(None)
|
||||
self.icon = pystray.Icon(
|
||||
"enodia-sentinel",
|
||||
icon=_make_image(self.state.color),
|
||||
title=self.state.tooltip,
|
||||
menu=self._menu(),
|
||||
)
|
||||
|
||||
def _menu(self) -> "pystray.Menu":
|
||||
return pystray.Menu(
|
||||
pystray.MenuItem(lambda _i: self.state.summary, None, enabled=False),
|
||||
pystray.Menu.SEPARATOR,
|
||||
pystray.MenuItem("Open dashboard", self._on_open),
|
||||
pystray.Menu.SEPARATOR,
|
||||
pystray.MenuItem("Start daemon", self._on_start),
|
||||
pystray.MenuItem("Stop daemon", self._on_stop),
|
||||
pystray.MenuItem("Restart daemon", self._on_restart),
|
||||
pystray.Menu.SEPARATOR,
|
||||
pystray.MenuItem("Run quick check", self._on_check),
|
||||
pystray.Menu.SEPARATOR,
|
||||
pystray.MenuItem("Quit", self._on_quit),
|
||||
)
|
||||
|
||||
def _async(self, fn) -> None:
|
||||
threading.Thread(target=fn, daemon=True).start()
|
||||
|
||||
def _do(self, action) -> None:
|
||||
res = action()
|
||||
notify.notify("Enodia Sentinel", res.message)
|
||||
self.refresh()
|
||||
|
||||
def _on_open(self, _icon=None, _item=None):
|
||||
self._async(lambda: self._do(lambda: actions.open_dashboard(self.cfg)))
|
||||
|
||||
def _on_start(self, _icon=None, _item=None):
|
||||
self._async(lambda: self._do(actions.start_daemon))
|
||||
|
||||
def _on_stop(self, _icon=None, _item=None):
|
||||
self._async(lambda: self._do(actions.stop_daemon))
|
||||
|
||||
def _on_restart(self, _icon=None, _item=None):
|
||||
self._async(lambda: self._do(actions.restart_daemon))
|
||||
|
||||
def _on_check(self, _icon=None, _item=None):
|
||||
self._async(lambda: self._do(actions.run_check))
|
||||
|
||||
def _on_quit(self, _icon=None, _item=None):
|
||||
self.icon.stop()
|
||||
|
||||
def refresh(self) -> None:
|
||||
self.state = parse_status(actions.fetch_status())
|
||||
self.icon.icon = _make_image(self.state.color)
|
||||
self.icon.title = self.state.tooltip
|
||||
self.icon.update_menu()
|
||||
|
||||
def _poll_loop(self) -> None:
|
||||
while True:
|
||||
self.refresh()
|
||||
time.sleep(REFRESH_SECONDS)
|
||||
|
||||
def run(self) -> None:
|
||||
def _setup(icon):
|
||||
icon.visible = True
|
||||
threading.Thread(target=self._poll_loop, daemon=True).start()
|
||||
self.icon.run(setup=_setup)
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
cfg = Config.load()
|
||||
TrayApp(cfg).run()
|
||||
return 0
|
||||
Loading…
Add table
Add a link
Reference in a new issue