#!/usr/bin/env python3
#--------------------------------------------------------------------------------------------------------
# Name: Linux Lite - Lite Update Tray
# Architecture: amd64
# Author: Jerry Bezencon
# Website: https://www.linuxliteos.com
# Language: Python/GTK3 (AyatanaAppIndicator3)
# Licence: GPLv2
#
# Tiny system-tray daemon that surfaces apt update state in the XFCE panel
# via StatusNotifier (Ayatana AppIndicator). Two icon states:
#   - White outline = no updates pending
#   - Green outline = updates available
# Polls `apt list --upgradable` every hour (no root needed; reads the apt
# cache as last refreshed by apt-daily.timer or a manual apt-get update).
# Right-click menu offers an immediate re-poll. Clicking the icon opens
# the lite-updates GUI.
#--------------------------------------------------------------------------------------------------------

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('AyatanaAppIndicator3', '0.1')

from gi.repository import Gtk, AyatanaAppIndicator3, GLib, Gio

import os
import signal
import subprocess

APP_ID = "lite-update-tray"
ICON_DIR = "/usr/share/lite-update-tray/icons"
ICON_IDLE = os.path.join(ICON_DIR, "lite-update-tray-idle.svg")
ICON_UPDATES = os.path.join(ICON_DIR, "lite-update-tray-updates.svg")
CHECK_INTERVAL_SECONDS = 3600  # 1 hour
DPKG_LOG = "/var/log/dpkg.log"
DPKG_DEBOUNCE_SECONDS = 5


class LiteUpdateTray:
    def __init__(self):
        self.indicator = AyatanaAppIndicator3.Indicator.new(
            APP_ID,
            ICON_IDLE,
            AyatanaAppIndicator3.IndicatorCategory.SYSTEM_SERVICES,
        )
        self.indicator.set_status(AyatanaAppIndicator3.IndicatorStatus.ACTIVE)
        self.indicator.set_title("Linux Lite Updates")
        self.indicator.set_icon_full(ICON_IDLE, "No updates available")

        menu = Gtk.Menu()
        check_item = Gtk.MenuItem(label="Check for Updates Now")
        check_item.connect("activate", self._on_check_now)
        menu.append(check_item)
        menu.show_all()

        self.indicator.set_menu(menu)
        # Clicking the indicator (where the host supports it) triggers
        # the check item — feels natural to most users.
        self.indicator.set_secondary_activate_target(check_item)

        # Schedule first check soon (don't block startup) + recurring.
        GLib.timeout_add_seconds(5, self._first_check)
        GLib.timeout_add_seconds(CHECK_INTERVAL_SECONDS, self._poll_updates)

        # Watch dpkg.log for any apt/dpkg activity (apt-get upgrade via
        # lite-updates, manual `sudo apt install`, autoremove, etc.) so the
        # icon turns white again as soon as upgrades are applied — without
        # waiting up to an hour for the next scheduled poll.
        self._dpkg_debounce_id = None
        try:
            log_file = Gio.File.new_for_path(DPKG_LOG)
            self._dpkg_monitor = log_file.monitor_file(Gio.FileMonitorFlags.NONE, None)
            self._dpkg_monitor.connect("changed", self._on_dpkg_log_changed)
        except Exception as e:
            print(f"[lite-update-tray] could not watch {DPKG_LOG}: {e}", flush=True)

    def _first_check(self):
        self._poll_updates()
        return False  # one-shot

    def _poll_updates(self):
        """Count pending upgrades against apt's cached package state.

        Uses `apt-get -s dist-upgrade` (simulate) rather than `apt list
        --upgradable`. The latter quietly excludes phased-update holds
        and packages whose upgrade requires additional installs/removals
        — so it can report 0 upgrades while a real `apt-get dist-upgrade`
        still pulls one. Simulating dist-upgrade is the only check that
        matches what a user-run `apt-get dist-upgrade` would actually do.

        LANG=C.UTF-8 forced so "Inst" line markers remain in English on
        non-English sessions. No root needed — simulate doesn't take the
        apt lock. Just reads /var/lib/apt/lists/."""
        try:
            env = dict(os.environ)
            env["LANG"] = "C.UTF-8"
            env["LC_ALL"] = "C.UTF-8"
            result = subprocess.run(
                ["apt-get", "-s", "dist-upgrade"],
                capture_output=True, text=True, timeout=60, env=env,
            )
            count = 0
            for line in result.stdout.splitlines():
                # Each upgrade/install action in the simulation prints
                # "Inst pkgname [oldver] (newver suite [arch])" on its
                # own line. Removals print "Remv " — we don't count those.
                if line.startswith("Inst "):
                    count += 1

            if count > 0:
                self._set_updates(count)
            else:
                self._set_idle()
        except Exception as e:
            print(f"[lite-update-tray] poll failed: {e}", flush=True)

        return True  # keep the recurring timer alive

    def _on_check_now(self, _item):
        # Immediate re-poll. Doesn't refresh apt cache itself
        # (that needs root) — surfaces whatever apt-daily.timer
        # last fetched, plus any manual apt-get update the user ran.
        self._poll_updates()

    def _on_dpkg_log_changed(self, _monitor, _file, _other_file, _event_type):
        # Debounce: apt writes many lines during a transaction. Reset the
        # timer on each change event so we only re-poll after activity has
        # settled (DPKG_DEBOUNCE_SECONDS of silence).
        if self._dpkg_debounce_id is not None:
            GLib.source_remove(self._dpkg_debounce_id)
        self._dpkg_debounce_id = GLib.timeout_add_seconds(
            DPKG_DEBOUNCE_SECONDS, self._debounced_repoll
        )

    def _debounced_repoll(self):
        self._dpkg_debounce_id = None
        self._poll_updates()
        return False  # one-shot

    def _set_idle(self):
        self.indicator.set_icon_full(ICON_IDLE, "No updates available")
        self.indicator.set_title("Linux Lite Updates — Up to date")

    def _set_updates(self, count):
        suffix = "" if count == 1 else "s"
        label = f"{count} update{suffix} available"
        self.indicator.set_icon_full(ICON_UPDATES, label)
        self.indicator.set_title(f"Linux Lite Updates — {label}")


def main():
    # Let Ctrl-C / SIGTERM kill the daemon cleanly when run interactively.
    signal.signal(signal.SIGINT, signal.SIG_DFL)
    signal.signal(signal.SIGTERM, signal.SIG_DFL)
    LiteUpdateTray()
    Gtk.main()


if __name__ == "__main__":
    main()
