#!/usr/bin/env python3
"""LinuxDoors PowerToys -- a small hub of desktop utility modules.

Modeled on the classic PowerToys-style utility suite: one central hub
listing independent modules, each toggleable/launchable on its own. Every
module here is either a DE-agnostic X11/GTK3 trick (works the same under
XFCE/MATE/GNOME/Cinnamon/Budgie/LXQt/Enlightenment/KDE Plasma) or shells
out to `wmctrl` for the one thing that genuinely needs real EWMH/NetWM
window-manager cooperation (Always on Top).
"""
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib, GdkPixbuf, Pango
import os
import re
import sys
import glob
import shlex
import signal
import subprocess

CSS = """
window, .background {
  background-color: #1c1038;
  background-image: linear-gradient(160deg, #241246 0%, #140a26 100%);
  color: #fbeed2;
}
label { color: #fbeed2; }
button label { color: #1c1038; }
button.flat-nav label { color: #fbeed2; }
button.flat-nav { background: transparent; border: none; }
button.flat-nav:hover { background-color: rgba(224,180,74,0.15); }
button.suggested-action, button.suggested-action label {
  background-color: #E0B44A; color: #1c1038;
}
row:selected { background-color: rgba(224,180,74,0.25); }
entry { background-color: #f4eeff; background-image: none; color: #1c1038; }
entry text { color: #1c1038; }
.module-title { font-size: 16px; font-weight: bold; color: #E0B44A; }
.module-desc { color: #cbb9e8; }
.swatch { border: 2px solid #E0B44A; border-radius: 4px; }
/* GtkListBoxRow defaults to a plain white/light row background,
   independent of the window's own dark gradient -- the blanket
   `label { color: #fbeed2 }` rule above is unreadable there (a real
   bug caught by a full screenshot, same "light text on a themed light
   background" class already hit elsewhere in this project). Row titles
   and descriptions get their own explicit dark-on-light colors. */
list, listview { background-color: #fbeed2; }
row { background-color: #fbeed2; }
row:hover { background-color: #f4e2c0; }
.row-title { color: #1c1038; font-weight: bold; }
.row-desc { color: #5b359e; }
"""


def apply_css():
    provider = Gtk.CssProvider()
    provider.load_from_data(CSS.encode())
    Gtk.StyleContext.add_provider_for_screen(
        Gdk.Screen.get_default(), provider,
        Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)


def draw_door(widget, cr, w, h):
    """Same crisp door mark used by every other LinuxDoors native app."""
    cr.set_source_rgb(0x1c / 255, 0x10 / 255, 0x38 / 255)
    cr.rectangle(0, 0, w, h)
    cr.fill()
    fx, fy, fw, fh = w * 0.18, h * 0.10, w * 0.64, h * 0.80
    cr.set_source_rgb(0xfb / 255, 0xee / 255, 0xd2 / 255)
    cr.rectangle(fx, fy, fw, fh)
    cr.fill()
    ox, oy, ow, oh = fx + fw * 0.08, fy + fh * 0.06, fw * 0.84, fh * 0.88
    cr.set_source_rgb(0xE0 / 255, 0xB4 / 255, 0x4A / 255)
    cr.rectangle(ox, oy, ow, oh)
    cr.fill()
    lx = ox + ow * 0.10
    pts = [(lx, oy), (ox + ow * 0.62, oy + oh * 0.06),
           (ox + ow * 0.72, oy + oh), (lx, oy + oh * 0.96)]
    cr.set_source_rgb(0x5b / 255, 0x35 / 255, 0x9e / 255)
    cr.move_to(*pts[0])
    for p in pts[1:]:
        cr.line_to(*p)
    cr.close_path()
    cr.fill()
    cr.set_source_rgb(0xE0 / 255, 0xB4 / 255, 0x4A / 255)
    cr.set_line_width(2)
    cr.move_to(*pts[0])
    for p in pts[1:]:
        cr.line_to(*p)
    cr.close_path()
    cr.stroke()
    kx = ox + ow * 0.58
    ky = oy + oh * 0.5
    cr.arc(kx, ky, ow * 0.045, 0, 2 * 3.14159265)
    cr.set_source_rgb(0xE0 / 255, 0xB4 / 255, 0x4A / 255)
    cr.fill()


# ---------------------------------------------------------------------
# Module: Awake -- inhibit idle/sleep via systemd-inhibit, matching the
# exact tool PowerToys' own Awake module wraps on Windows conceptually
# (a lock, not a wake-lock reinvention).
# ---------------------------------------------------------------------
class AwakeModule:
    def __init__(self):
        self.proc = None

    def is_active(self):
        return self.proc is not None and self.proc.poll() is None

    def enable(self):
        if self.is_active():
            return
        self.proc = subprocess.Popen(
            ["systemd-inhibit", "--what=idle:sleep:handle-lid-switch",
             "--who=LinuxDoors PowerToys", "--why=Awake module enabled",
             "sleep", "infinity"])

    def disable(self):
        if self.is_active():
            self.proc.terminate()
            try:
                self.proc.wait(timeout=3)
            except subprocess.TimeoutExpired:
                self.proc.kill()
        self.proc = None


# ---------------------------------------------------------------------
# Module: Always on Top -- via wmctrl (real EWMH _NET_WM_STATE_ABOVE),
# not a hand-rolled X11 ClientMessage -- same job, a well-tested tool.
# ---------------------------------------------------------------------
def toggle_always_on_top():
    try:
        subprocess.run(["wmctrl", "-r", ":ACTIVE:", "-b", "toggle,above"],
                        check=True, timeout=5)
        return True, None
    except FileNotFoundError:
        return False, "wmctrl is not installed"
    except subprocess.CalledProcessError as e:
        return False, f"wmctrl failed (exit {e.returncode})"
    except subprocess.TimeoutExpired:
        return False, "wmctrl timed out"


# ---------------------------------------------------------------------
# Module: Color Picker
# ---------------------------------------------------------------------
class ColorPickerWindow(Gtk.Window):
    def __init__(self):
        super().__init__(title="LinuxDoors PowerToys -- Color Picker")
        self.set_default_size(50, 50)
        self.set_decorated(False)
        self.set_keep_above(True)
        self.get_style_context().add_class("background")
        screen = Gdk.Screen.get_default()
        visual = screen.get_rgba_visual()
        if visual:
            self.set_visual(visual)
        self.connect("realize", self._on_realize)
        self.connect("button-press-event", self._on_click)
        self.connect("key-press-event", self._on_key)
        self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        lbl = Gtk.Label(label="Click anywhere on screen to pick a color\n"
                               "(Esc to cancel)")
        lbl.set_margin_top(20)
        lbl.set_margin_bottom(20)
        lbl.set_margin_start(20)
        lbl.set_margin_end(20)
        self.add(lbl)

    def _on_realize(self, *_a):
        win = self.get_window()
        cursor = Gdk.Cursor.new_from_name(self.get_display(), "crosshair")
        if cursor:
            win.set_cursor(cursor)
        self.fullscreen()
        self.set_opacity(0.15)

    def _on_key(self, _w, ev):
        if ev.keyval == Gdk.KEY_Escape:
            self.destroy()
        return False

    def _on_click(self, _w, ev):
        x, y = int(ev.x_root), int(ev.y_root)
        root = Gdk.get_default_root_window()
        pb = Gdk.pixbuf_get_from_window(root, x, y, 1, 1)
        self.destroy()
        if pb is None:
            _show_error("Color Picker", "Could not read pixel color.")
            return
        r, g, b = pb.get_pixels()[0:3]
        hexcode = f"#{r:02x}{g:02x}{b:02x}"
        clip = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clip.set_text(hexcode, -1)
        clip.store()
        _show_swatch(hexcode, r, g, b)


def _show_swatch(hexcode, r, g, b):
    dlg = Gtk.MessageDialog(
        transient_for=None, modal=True,
        message_type=Gtk.MessageType.INFO, buttons=Gtk.ButtonsType.OK,
        text=f"Picked {hexcode}")
    dlg.format_secondary_text(f"rgb({r}, {g}, {b}) -- copied to clipboard")
    area = dlg.get_message_area()
    swatch = Gtk.DrawingArea()
    swatch.set_size_request(48, 48)
    swatch.get_style_context().add_class("swatch")

    def draw(_w, cr):
        cr.set_source_rgb(r / 255, g / 255, b / 255)
        cr.rectangle(0, 0, 48, 48)
        cr.fill()
        return False
    swatch.connect("draw", draw)
    area.pack_start(swatch, False, False, 6)
    swatch.show()
    dlg.get_style_context().add_class("background")
    dlg.run()
    dlg.destroy()


def _show_error(title, text):
    dlg = Gtk.MessageDialog(
        transient_for=None, modal=True, message_type=Gtk.MessageType.ERROR,
        buttons=Gtk.ButtonsType.OK, text=title)
    dlg.format_secondary_text(text)
    dlg.get_style_context().add_class("background")
    dlg.run()
    dlg.destroy()


# ---------------------------------------------------------------------
# Module: Screen Ruler -- click-drag anywhere on screen, shows pixel
# distance live, released measurement stays until Esc/click-away.
# ---------------------------------------------------------------------
class ScreenRulerWindow(Gtk.Window):
    def __init__(self):
        super().__init__(title="LinuxDoors PowerToys -- Screen Ruler")
        self.set_decorated(False)
        self.fullscreen()
        screen = Gdk.Screen.get_default()
        visual = screen.get_rgba_visual()
        if visual:
            self.set_visual(visual)
        self.set_app_paintable(True)
        self.start = None
        self.end = None
        self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK
                         | Gdk.EventMask.BUTTON_RELEASE_MASK
                         | Gdk.EventMask.POINTER_MOTION_MASK)
        area = Gtk.DrawingArea()
        area.connect("draw", self._draw)
        self.add(area)
        self.area = area
        self.connect("button-press-event", self._press)
        self.connect("button-release-event", self._release)
        self.connect("motion-notify-event", self._motion)
        self.connect("key-press-event", self._key)

    def _press(self, _w, ev):
        self.start = (ev.x, ev.y)
        self.end = (ev.x, ev.y)
        self.area.queue_draw()

    def _motion(self, _w, ev):
        if self.start:
            self.end = (ev.x, ev.y)
            self.area.queue_draw()

    def _release(self, _w, ev):
        self.end = (ev.x, ev.y)
        self.area.queue_draw()

    def _key(self, _w, ev):
        if ev.keyval == Gdk.KEY_Escape:
            self.destroy()
        return False

    def _draw(self, _w, cr):
        cr.set_source_rgba(0.11, 0.06, 0.22, 0.25)
        cr.paint()
        if self.start and self.end:
            x0, y0 = self.start
            x1, y1 = self.end
            cr.set_source_rgb(0xE0 / 255, 0xB4 / 255, 0x4A / 255)
            cr.set_line_width(2)
            cr.move_to(x0, y0)
            cr.line_to(x1, y1)
            cr.stroke()
            dist = int(((x1 - x0) ** 2 + (y1 - y0) ** 2) ** 0.5)
            dx, dy = int(abs(x1 - x0)), int(abs(y1 - y0))
            text = f"{dist}px  (dx {dx}, dy {dy})"
            cr.set_font_size(18)
            cr.select_font_face("sans-serif")
            mx, my = (x0 + x1) / 2, (y0 + y1) / 2 - 12
            extents = cr.text_extents(text)
            pad = 6
            cr.set_source_rgba(0.11, 0.06, 0.22, 0.85)
            cr.rectangle(mx - extents.width / 2 - pad, my - extents.height - pad,
                         extents.width + 2 * pad, extents.height + 2 * pad)
            cr.fill()
            cr.set_source_rgb(0xfb / 255, 0xee / 255, 0xd2 / 255)
            cr.move_to(mx - extents.width / 2, my)
            cr.show_text(text)
        return False


# ---------------------------------------------------------------------
# Module: Hosts File Editor -- root-elevated via the same su -c +
# XAUTHORITY pattern already used by every other LinuxDoors admin tool.
# ---------------------------------------------------------------------
class HostsEditorWindow(Gtk.Window):
    def __init__(self):
        super().__init__(title="LinuxDoors PowerToys -- Hosts File Editor")
        self.set_default_size(640, 480)
        self.get_style_context().add_class("background")
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        box.set_margin_top(12); box.set_margin_bottom(12)
        box.set_margin_start(12); box.set_margin_end(12)
        self.add(box)

        title = Gtk.Label(label="/etc/hosts")
        title.get_style_context().add_class("module-title")
        title.set_halign(Gtk.Align.START)
        box.pack_start(title, False, False, 0)

        sw = Gtk.ScrolledWindow()
        sw.set_vexpand(True)
        self.textview = Gtk.TextView()
        self.textview.set_monospace(True)
        try:
            with open("/etc/hosts") as f:
                self.textview.get_buffer().set_text(f.read())
        except OSError as e:
            self.textview.get_buffer().set_text(f"# could not read /etc/hosts: {e}")
        sw.add(self.textview)
        box.pack_start(sw, True, True, 0)

        self.status = Gtk.Label(label="")
        self.status.set_halign(Gtk.Align.START)
        box.pack_start(self.status, False, False, 0)

        btnbox = Gtk.Box(spacing=8)
        btnbox.set_halign(Gtk.Align.END)
        save = Gtk.Button(label="Save (admin password required)")
        save.get_style_context().add_class("suggested-action")
        save.connect("clicked", self._save)
        btnbox.pack_start(save, False, False, 0)
        box.pack_start(btnbox, False, False, 0)

    def _save(self, _b):
        buf = self.textview.get_buffer()
        text = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), True)
        tmp = "/tmp/.linuxdoors-powertoys-hosts-edit"
        try:
            with open(tmp, "w") as f:
                f.write(text)
        except OSError as e:
            self.status.set_text(f"Could not write temp file: {e}")
            return
        display = os.environ.get("DISPLAY", ":0")
        xauth = os.environ.get("XAUTHORITY", os.path.expanduser("~/.Xauthority"))
        pw = _ask_password(self)
        if pw is None:
            return
        cmd = (f"env DISPLAY={shlex.quote(display)} "
               f"XAUTHORITY={shlex.quote(xauth)} "
               f"cp {shlex.quote(tmp)} /etc/hosts")
        ok, err = _run_as_root(cmd, pw)
        if ok:
            self.status.set_text("Saved /etc/hosts.")
        else:
            self.status.set_text(f"Failed: {err}")


def _ask_password(parent):
    dlg = Gtk.Dialog(title="Administrator password", transient_for=parent, modal=True)
    dlg.add_buttons("Cancel", Gtk.ResponseType.CANCEL, "OK", Gtk.ResponseType.OK)
    dlg.get_style_context().add_class("background")
    box = dlg.get_content_area()
    entry = Gtk.Entry()
    entry.set_visibility(False)
    entry.set_activates_default(True)
    entry.set_margin_top(8); entry.set_margin_bottom(8)
    entry.set_margin_start(8); entry.set_margin_end(8)
    box.add(entry)
    dlg.set_default_response(Gtk.ResponseType.OK)
    dlg.show_all()
    resp = dlg.run()
    pw = entry.get_text() if resp == Gtk.ResponseType.OK else None
    dlg.destroy()
    return pw


def _run_as_root(cmd, password):
    try:
        proc = subprocess.run(
            ["su", "-c", cmd, "root"],
            input=(password + "\n").encode(), capture_output=True, timeout=15)
        out = (proc.stdout + proc.stderr).decode(errors="replace")
        if proc.returncode != 0 or "su: Authentication failure" in out:
            return False, out.strip()[-200:] or f"exit {proc.returncode}"
        return True, None
    except subprocess.TimeoutExpired:
        return False, "timed out"


# ---------------------------------------------------------------------
# Module: PowerRename -- regex batch rename with a live preview.
# ---------------------------------------------------------------------
class PowerRenameWindow(Gtk.Window):
    def __init__(self):
        super().__init__(title="LinuxDoors PowerToys -- PowerRename")
        self.set_default_size(620, 460)
        self.get_style_context().add_class("background")
        self.files = []

        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
        box.set_margin_top(12); box.set_margin_bottom(12)
        box.set_margin_start(12); box.set_margin_end(12)
        self.add(box)

        pick = Gtk.Button(label="Choose files...")
        pick.connect("clicked", self._choose)
        box.pack_start(pick, False, False, 0)

        grid = Gtk.Grid(column_spacing=8, row_spacing=6)
        grid.attach(Gtk.Label(label="Find (regex):"), 0, 0, 1, 1)
        self.find_entry = Gtk.Entry()
        self.find_entry.connect("changed", self._refresh)
        grid.attach(self.find_entry, 1, 0, 1, 1)
        grid.attach(Gtk.Label(label="Replace with:"), 0, 1, 1, 1)
        self.replace_entry = Gtk.Entry()
        self.replace_entry.connect("changed", self._refresh)
        grid.attach(self.replace_entry, 1, 1, 1, 1)
        box.pack_start(grid, False, False, 0)

        self.store = Gtk.ListStore(str, str)
        tv = Gtk.TreeView(model=self.store)
        for i, title in enumerate(["Original", "Renamed to"]):
            tv.append_column(Gtk.TreeViewColumn(title, Gtk.CellRendererText(), text=i))
        sw = Gtk.ScrolledWindow()
        sw.set_vexpand(True)
        sw.add(tv)
        box.pack_start(sw, True, True, 0)

        self.status = Gtk.Label(label="Choose files to begin.")
        self.status.set_halign(Gtk.Align.START)
        box.pack_start(self.status, False, False, 0)

        apply_btn = Gtk.Button(label="Apply Rename")
        apply_btn.get_style_context().add_class("suggested-action")
        apply_btn.connect("clicked", self._apply)
        box.pack_start(apply_btn, False, False, 0)

    def _choose(self, _b):
        dlg = Gtk.FileChooserDialog(title="Choose files", transient_for=self,
                                     action=Gtk.FileChooserAction.OPEN)
        dlg.add_buttons("Cancel", Gtk.ResponseType.CANCEL, "Open", Gtk.ResponseType.OK)
        dlg.set_select_multiple(True)
        if dlg.run() == Gtk.ResponseType.OK:
            self.files = dlg.get_filenames()
        dlg.destroy()
        self._refresh()

    def _refresh(self, *_a):
        self.store.clear()
        pattern = self.find_entry.get_text()
        repl = self.replace_entry.get_text()
        errors = 0
        for path in self.files:
            base = os.path.basename(path)
            try:
                new = re.sub(pattern, repl, base) if pattern else base
            except re.error as e:
                new = f"<regex error: {e}>"
                errors += 1
            self.store.append([base, new])
        if self.files:
            self.status.set_text(
                f"{len(self.files)} file(s)"
                + (f", {errors} regex error(s)" if errors else ""))

    def _apply(self, _b):
        pattern = self.find_entry.get_text()
        repl = self.replace_entry.get_text()
        if not pattern:
            self.status.set_text("Enter a find pattern first.")
            return
        done, failed = 0, 0
        for path in self.files:
            d, base = os.path.split(path)
            try:
                new = re.sub(pattern, repl, base)
            except re.error:
                failed += 1
                continue
            if new == base:
                continue
            try:
                os.rename(path, os.path.join(d, new))
                done += 1
            except OSError:
                failed += 1
        self.status.set_text(f"Renamed {done} file(s)" + (f", {failed} failed" if failed else "."))
        self.files = []
        self.store.clear()


# ---------------------------------------------------------------------
# Module: Peek -- quick file preview (image / text / PDF via Poppler).
# ---------------------------------------------------------------------
class PeekWindow(Gtk.Window):
    def __init__(self, path=None):
        super().__init__(title="LinuxDoors PowerToys -- Peek")
        self.set_default_size(700, 560)
        self.get_style_context().add_class("background")
        if path:
            self._preview(path)
        else:
            self._choose()

    def _choose(self):
        dlg = Gtk.FileChooserDialog(title="Choose a file to peek at",
                                     transient_for=self,
                                     action=Gtk.FileChooserAction.OPEN)
        dlg.add_buttons("Cancel", Gtk.ResponseType.CANCEL, "Open", Gtk.ResponseType.OK)
        if dlg.run() == Gtk.ResponseType.OK:
            path = dlg.get_filename()
            dlg.destroy()
            self._preview(path)
        else:
            dlg.destroy()
            self.destroy()

    def _preview(self, path):
        ext = os.path.splitext(path)[1].lower()
        try:
            if ext in (".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".svg"):
                pb = GdkPixbuf.Pixbuf.new_from_file_at_scale(path, 680, 520, True)
                img = Gtk.Image.new_from_pixbuf(pb)
                sw = Gtk.ScrolledWindow()
                sw.add(img)
                self.add(sw)
            elif ext == ".pdf":
                self._preview_pdf(path)
            else:
                self._preview_text(path)
        except Exception as e:
            lbl = Gtk.Label(label=f"Could not preview this file:\n{e}")
            self.add(lbl)
        self.show_all()

    def _preview_text(self, path):
        tv = Gtk.TextView()
        tv.set_editable(False)
        tv.set_monospace(True)
        try:
            with open(path, "r", errors="replace") as f:
                data = f.read(200_000)
        except OSError as e:
            data = f"(could not read file: {e})"
        tv.get_buffer().set_text(data)
        sw = Gtk.ScrolledWindow()
        sw.add(tv)
        self.add(sw)

    def _preview_pdf(self, path):
        try:
            gi.require_version('Poppler', '0.18')
            from gi.repository import Poppler
        except (ImportError, ValueError):
            self._preview_text(path)
            return
        doc = Poppler.Document.new_from_file(f"file://{os.path.abspath(path)}", None)
        page = doc.get_page(0)
        pw, ph = page.get_size()
        scale = min(680 / pw, 520 / ph)
        surf_w, surf_h = int(pw * scale), int(ph * scale)
        import cairo
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, surf_w, surf_h)
        cr = cairo.Context(surface)
        cr.set_source_rgb(1, 1, 1)
        cr.paint()
        cr.scale(scale, scale)
        page.render(cr)
        buf = surface.get_data()
        pb = GdkPixbuf.Pixbuf.new_from_data(
            bytes(buf), GdkPixbuf.Colorspace.RGB, True, 8,
            surf_w, surf_h, surface.get_stride())
        img = Gtk.Image.new_from_pixbuf(pb)
        label = Gtk.Label(label=f"Page 1 of {doc.get_n_pages()}")
        box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        box.pack_start(img, True, True, 0)
        box.pack_start(label, False, False, 4)
        self.add(box)


# ---------------------------------------------------------------------
# Hub window
# ---------------------------------------------------------------------
MODULES = [
    ("Awake", "Keep the computer from sleeping or locking while enabled.", "awake"),
    ("Always on Top", "Pin the currently focused window above all others.", "aot"),
    ("Color Picker", "Click anywhere on screen to grab a color as hex/RGB.", "color"),
    ("Screen Ruler", "Click and drag anywhere to measure a distance in pixels.", "ruler"),
    ("Hosts File Editor", "Edit /etc/hosts with admin elevation.", "hosts"),
    ("PowerRename", "Batch-rename files with a regex find/replace preview.", "rename"),
    ("Peek", "Quickly preview an image, text, or PDF file.", "peek"),
]


class HubWindow(Gtk.Window):
    def __init__(self):
        super().__init__(title="LinuxDoors PowerToys")
        self.set_default_size(560, 420)
        self.get_style_context().add_class("background")
        self.awake = AwakeModule()

        outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
        self.add(outer)

        header = Gtk.Box(spacing=10)
        header.set_margin_top(14); header.set_margin_bottom(10)
        header.set_margin_start(16); header.set_margin_end(16)
        door = Gtk.DrawingArea()
        door.set_size_request(40, 40)
        door.connect("draw", lambda w, cr: draw_door(w, cr, 40, 40))
        header.pack_start(door, False, False, 0)
        title = Gtk.Label(label="LinuxDoors PowerToys")
        title.get_style_context().add_class("module-title")
        header.pack_start(title, False, False, 0)
        outer.pack_start(header, False, False, 0)

        sep = Gtk.Separator()
        outer.pack_start(sep, False, False, 0)

        sw = Gtk.ScrolledWindow()
        sw.set_vexpand(True)
        listbox = Gtk.ListBox()
        listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        for name, desc, key in MODULES:
            listbox.add(self._make_row(name, desc, key))
        sw.add(listbox)
        outer.pack_start(sw, True, True, 0)

    def _make_row(self, name, desc, key):
        row = Gtk.ListBoxRow()
        box = Gtk.Box(spacing=12)
        box.set_margin_top(8); box.set_margin_bottom(8)
        box.set_margin_start(12); box.set_margin_end(12)
        row.add(box)

        textbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        title = Gtk.Label(label=name)
        title.set_halign(Gtk.Align.START)
        title.get_style_context().add_class("row-title")
        desc_lbl = Gtk.Label(label=desc)
        desc_lbl.set_halign(Gtk.Align.START)
        desc_lbl.get_style_context().add_class("row-desc")
        desc_lbl.set_line_wrap(True)
        textbox.pack_start(title, False, False, 0)
        textbox.pack_start(desc_lbl, False, False, 0)
        box.pack_start(textbox, True, True, 0)

        if key == "awake":
            switch = Gtk.Switch()
            switch.set_valign(Gtk.Align.CENTER)
            switch.connect("state-set", self._on_awake_toggle)
            box.pack_start(switch, False, False, 0)
        elif key == "aot":
            btn = Gtk.Button(label="Toggle on active window")
            btn.connect("clicked", self._on_aot)
            box.pack_start(btn, False, False, 0)
        else:
            btn = Gtk.Button(label="Launch")
            btn.get_style_context().add_class("suggested-action")
            btn.connect("clicked", self._on_launch, key)
            box.pack_start(btn, False, False, 0)
        return row

    def _on_awake_toggle(self, switch, state):
        if state:
            self.awake.enable()
        else:
            self.awake.disable()
        return False

    def _on_aot(self, _b):
        ok, err = toggle_always_on_top()
        if not ok:
            _show_error("Always on Top", err)

    def _on_launch(self, _b, key):
        if key == "color":
            ColorPickerWindow().show_all()
        elif key == "ruler":
            ScreenRulerWindow().show_all()
        elif key == "hosts":
            HostsEditorWindow().show_all()
        elif key == "rename":
            PowerRenameWindow().show_all()
        elif key == "peek":
            PeekWindow()


def main():
    apply_css()
    if len(sys.argv) > 1 and sys.argv[1] == "--peek" and len(sys.argv) > 2:
        win = PeekWindow(sys.argv[2])
        win.connect("destroy", Gtk.main_quit)
        win.show_all()
        Gtk.main()
        return
    win = HubWindow()
    win.connect("destroy", Gtk.main_quit)
    win.show_all()
    Gtk.main()


if __name__ == "__main__":
    signal.signal(signal.SIGINT, signal.SIG_DFL)
    main()
