Files
reterminal-dm4/emmc-provisioning/cloud-init/fileserver/brightness-overlay.py
nearxos 0844adbcbe Update cloud-init scripts and documentation for enhanced DNS management and provisioning steps</message>
<message>Modify the first-boot.sh script to include an additional step for managing screen brightness during the provisioning process. Update user-data.bootstrap to improve DNS configuration by ensuring NetworkManager manages /etc/resolv.conf correctly, and remove obsolete scripts related to systemd-resolved. Enhance documentation to reflect these changes and clarify the setup process for users, improving overall network boot functionality and user experience.
2026-03-06 14:45:23 +02:00

303 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Brightness control overlay — always-on-top icon in bottom-right; click to show controls.
Uses Wayland layer-shell OVERLAY so it stays above Chromium fullscreen/kiosk.
Requires: brightness-api.service running (listens 127.0.0.1:8765).
"""
import logging
import sys
import urllib.request
import urllib.error
LOG_TAG = "brightness-overlay"
logging.basicConfig(
stream=sys.stderr,
level=logging.INFO,
format=f"{LOG_TAG}: %(message)s",
)
log = logging.getLogger(LOG_TAG)
try:
import gi
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk, GLib
except Exception as e:
log.error("need PyGObject Gtk: %s", e)
sys.exit(1)
HAS_LAYER_SHELL = False
try:
gi.require_version("GtkLayerShell", "0.1")
from gi.repository import GtkLayerShell
HAS_LAYER_SHELL = True
except Exception:
log.warning("GtkLayerShell not available, using regular window")
API_URL = "http://127.0.0.1:8765/brightness"
API_STATE_URL = "http://127.0.0.1:8765/state"
# Sizes
ICON_SIZE = 44
EXPANDED_WIDTH = 300
PANEL_HEIGHT = 48
def get_current_state():
"""Return (level 15, is_auto). On error return (None, True) so Auto appears active."""
try:
with urllib.request.urlopen(API_STATE_URL, timeout=1) as r:
text = r.read().decode()
level = None
override = "0"
for line in text.strip().splitlines():
line = line.strip()
if line.startswith("level="):
level = line.split("=", 1)[1].strip()
elif line.startswith("override="):
override = line.split("=", 1)[1].strip()
if level and level.isdigit() and 1 <= int(level) <= 5:
return int(level), override == "0"
return (None, True)
except Exception:
return (None, True)
def set_brightness(level):
try:
req = urllib.request.Request(
API_URL,
data=f"level={level}".encode(),
method="POST",
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
with urllib.request.urlopen(req, timeout=2) as r:
log.info("brightness %s: %s", level, r.read().decode().strip())
except urllib.error.URLError as e:
log.warning("API call failed (is brightness-api.service running?): %s", e)
except Exception as e:
log.warning("set_brightness failed: %s", e)
def update_active_buttons(level_buttons, auto_btn, current_level, is_auto):
"""Set .active style on the button that matches current state."""
for i, btn in enumerate(level_buttons):
ctx = btn.get_style_context()
if not is_auto and current_level == i + 1:
ctx.add_class("active")
else:
ctx.remove_class("active")
if auto_btn:
ctx = auto_btn.get_style_context()
if is_auto:
ctx.add_class("active")
else:
ctx.remove_class("active")
def make_window_layer_shell(win):
GtkLayerShell.init_for_window(win)
GtkLayerShell.set_layer(win, GtkLayerShell.Layer.OVERLAY)
# Anchor only to bottom and right so the surface sits in the bottom-right corner
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.TOP, False)
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.LEFT, False)
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.BOTTOM, True)
GtkLayerShell.set_anchor(win, GtkLayerShell.Edge.RIGHT, True)
GtkLayerShell.set_keyboard_mode(win, GtkLayerShell.KeyboardMode.NONE)
GtkLayerShell.set_margin(win, GtkLayerShell.Edge.BOTTOM, 12)
GtkLayerShell.set_margin(win, GtkLayerShell.Edge.RIGHT, 12)
if hasattr(GtkLayerShell, "set_namespace"):
GtkLayerShell.set_namespace(win, "brightness-overlay")
if hasattr(GtkLayerShell, "set_exclusive_zone"):
GtkLayerShell.set_exclusive_zone(win, 0)
log.info("layer-shell OVERLAY anchored bottom-right (always on top)")
def position_fallback_bottom_right(win, width=None, height=None):
"""Position window in bottom-right (X11: only works with toplevel, not POPUP)."""
if width is None:
width = ICON_SIZE
if height is None:
height = ICON_SIZE
display = Gdk.Display.get_default()
if not display:
return
n = display.get_n_monitors()
monitor = display.get_monitor(0) if n else None
if not monitor:
return
geom = monitor.get_geometry()
x = geom.x + geom.width - width - 12
y = geom.y + geom.height - height - 12
win.move(x, y)
def make_window_fallback(win):
# Toplevel (not POPUP) so we can position and have no decorations on X11
win.set_keep_above(True)
win.set_decorated(False)
win.set_type_hint(Gdk.WindowTypeHint.UTILITY) # tool-style window, often undecorated
# Position after map so WM doesn't override
win.connect("map-event", lambda w, e: position_fallback_bottom_right(w))
log.info("X11 fallback: keep-above, undecorated, bottom-right")
def main():
# Normal toplevel: on X11 we can position it and set_decorated(False) removes title bar.
# (POPUP without parent cannot be positioned on X11 and gets "temporary window" / centering.)
win = Gtk.Window()
win.set_decorated(False)
win.set_resizable(False)
win.set_default_size(ICON_SIZE, ICON_SIZE)
win.set_skip_taskbar_hint(True)
win.set_skip_pager_hint(True)
# Transparent window: need RGBA visual and app-paintable
screen = Gdk.Screen.get_default()
if screen and screen.get_rgba_visual():
win.set_visual(screen.get_rgba_visual())
win.set_app_paintable(True)
# CSS: transparent window and overlay; only subtle hover so icon stays visible
style = """
GtkWindow, window {
background-color: transparent;
background-image: none;
}
.brightness-icon-btn {
background-color: transparent;
border-radius: 8px;
padding: 4px;
min-width: 36px;
min-height: 36px;
}
.brightness-icon-btn:hover {
background-color: rgba(0, 0, 0, 0.25);
}
.brightness-panel {
background-color: transparent;
border-radius: 8px;
padding: 6px;
}
.brightness-panel.expanded {
background-color: rgba(0, 0, 0, 0.5);
}
.brightness-panel button {
min-width: 36px;
font-weight: bold;
}
.brightness-panel button.level-btn.active {
background-color: rgba(255, 255, 255, 0.35);
border: 1px solid rgba(255, 255, 255, 0.6);
}
"""
css = Gtk.CssProvider()
css.load_from_data(style.encode())
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
css,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
)
# Main horizontal box: [revealer with controls] [icon button]
main_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
# Left part: revealer with Brightness + 15 + Auto
strip_box = Gtk.Box(spacing=6)
strip_box.set_margin_start(8)
strip_box.set_margin_top(6)
strip_box.set_margin_bottom(6)
strip_box.set_margin_end(4)
strip_box.get_style_context().add_class("brightness-panel")
strip_box.pack_start(Gtk.Label(label="Brightness "), False, False, 0)
level_buttons = []
for i in range(1, 6):
btn = Gtk.Button(label=str(i))
btn.get_style_context().add_class("level-btn")
btn.set_can_focus(False)
strip_box.pack_start(btn, False, False, 0)
level_buttons.append(btn)
auto_btn = Gtk.Button(label="Auto")
auto_btn.get_style_context().add_class("level-btn")
auto_btn.set_can_focus(False)
strip_box.pack_start(auto_btn, False, False, 0)
def refresh_active_state():
level, is_auto = get_current_state()
update_active_buttons(level_buttons, auto_btn, level or 3, is_auto)
def on_level_click(btn, level):
set_brightness(level)
# Optimistic update so active state shows immediately
if level == "auto":
update_active_buttons(level_buttons, auto_btn, 3, True)
else:
update_active_buttons(level_buttons, auto_btn, int(level), False)
# Sync from API after a short delay (daemon updates state file periodically)
def delayed_refresh():
refresh_active_state()
return False
GLib.timeout_add(500, delayed_refresh)
for i, btn in enumerate(level_buttons):
btn.connect("clicked", on_level_click, str(i + 1))
auto_btn.connect("clicked", on_level_click, "auto")
revealer = Gtk.Revealer()
revealer.set_reveal_child(False)
revealer.set_transition_type(Gtk.RevealerTransitionType.SLIDE_LEFT)
revealer.set_transition_duration(150)
revealer.add(strip_box)
main_box.pack_start(revealer, False, False, 0)
# Right part: always-visible icon button (click toggles panel)
try:
icon_img = Gtk.Image.new_from_icon_name("display-brightness-symbolic", Gtk.IconSize.BUTTON)
except Exception:
icon_img = Gtk.Label(label="")
icon_btn = Gtk.Button()
icon_btn.get_style_context().add_class("brightness-icon-btn")
icon_btn.set_can_focus(False)
icon_btn.add(icon_img)
icon_btn.set_relief(Gtk.ReliefStyle.NONE)
expanded = [False] # use list so closure can mutate
use_fallback = not (HAS_LAYER_SHELL and GtkLayerShell.is_supported())
def on_icon_clicked(btn):
expanded[0] = not expanded[0]
revealer.set_reveal_child(expanded[0])
ctx = strip_box.get_style_context()
if expanded[0]:
ctx.add_class("expanded")
GLib.idle_add(refresh_active_state)
win.resize(EXPANDED_WIDTH, PANEL_HEIGHT)
if use_fallback:
position_fallback_bottom_right(win, EXPANDED_WIDTH, PANEL_HEIGHT)
else:
ctx.remove_class("expanded")
win.resize(ICON_SIZE, ICON_SIZE)
if use_fallback:
position_fallback_bottom_right(win, ICON_SIZE, ICON_SIZE)
icon_btn.connect("clicked", on_icon_clicked)
main_box.pack_end(icon_btn, False, False, 0)
win.add(main_box)
if HAS_LAYER_SHELL and GtkLayerShell.is_supported():
make_window_layer_shell(win)
else:
make_window_fallback(win)
win.connect("destroy", Gtk.main_quit)
win.show_all()
GLib.idle_add(refresh_active_state)
log.info("brightness icon visible (click to show controls)")
Gtk.main()
if __name__ == "__main__":
main()