Phase 1/2 complete: system tray (XEMBED) + Plymouth boot splash

System tray in skydeskbar (Phase 1 #4):
- Claims _NET_SYSTEM_TRAY_S<screen> selection on startup and
  broadcasts MANAGER so nm-applet and other tray clients connect
- Handles _NET_SYSTEM_TRAY_OPCODE dock requests: XReparentWindow
  icon into the deskbar, sends XEMBED_EMBEDDED_NOTIFY
- Tray section is zero-height when empty; expands to DB_TRAY_H=26px
  when first icon embeds, shrinking back when last icon exits
- DestroyNotify / ReparentNotify clean up the icon list and
  reposition remaining icons; SelectionClear hands off gracefully
- Icon positions are updated (db_tray_reposition) whenever the app
  list changes and shifts the tray section vertically

Plymouth boot splash (Phase 2 #10):
- 'skyos' script-module theme: dark navy (#111520) background,
  "SkyOS" logo in accent blue (#90BBEE), thin progress bar
- 1×1 pixel bar_bg.png / bar_fg.png scaled at runtime by the
  Plymouth script — no large PNGs committed to the repo
- logo.png generated at build time by ImageMagick (same tool used
  for the wallpaper); build continues if ImageMagick is absent
- mkinitcpio HOOKS: added 'plymouth' after 'udev'
- Kernel cmdline: added 'splash' for UEFI systemd-boot entry
- customize_airootfs.sh: plymouth-set-default-theme skyos

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-07-23 13:32:27 -04:00
1 parent 2b8ea90a57
commit 803ffe991f
3 files changed
+220 -5

No files matched your search

+14 -1
View File
@@ -25,7 +25,9 @@
* ├──────────────────────────┤ * ├──────────────────────────┤
* │ [1] [2] [3] [4] │ <- workspace strip * │ [1] [2] [3] [4] │ <- workspace strip
* ├──────────────────────────┤ * ├──────────────────────────┤
* │ CPU 12% MEM 45% │ <- status bar * │ [⊙][📶][🔋] │ <- system tray (XEMBED; hidden when empty)
* ├──────────────────────────┤
* │ VOL 50% CPU 12% MEM 45%│ <- status bar
* └──────────────────────────┘ * └──────────────────────────┘
*/ */
@@ -37,6 +39,9 @@
#define DB_ICON_SZ 18 /* app icon square size */ #define DB_ICON_SZ 18 /* app icon square size */
#define DB_WS_H 28 /* workspace strip height */ #define DB_WS_H 28 /* workspace strip height */
#define DB_STATUS_H 22 /* status bar height */ #define DB_STATUS_H 22 /* status bar height */
#define DB_TRAY_H 26 /* tray section height when icons are present */
#define DB_TRAY_ICON_SZ 20 /* embedded icon window size */
#define DB_TRAY_MAX 16 /* maximum simultaneous tray icons */
#define DB_MAX_APPS 32 #define DB_MAX_APPS 32
/* ── fly-out menu ────────────────────────────────────────────────────── */ /* ── fly-out menu ────────────────────────────────────────────────────── */
@@ -133,6 +138,14 @@ typedef struct Deskbar {
int sub_active; /* which sub-menu is open: 0=Apps, 1=Settings, -1=none */ int sub_active; /* which sub-menu is open: 0=Apps, 1=Settings, -1=none */
int sub_hov; /* hovered item in sub-menu */ int sub_hov; /* hovered item in sub-menu */
int sw_cache; /* cached screen width for menu positioning */ int sw_cache; /* cached screen width for menu positioning */
/* system tray (freedesktop.org XEmbed tray protocol) */
Window tray_icons[DB_TRAY_MAX];
int n_tray_icons;
Atom atom_tray_s0; /* _NET_SYSTEM_TRAY_S<screen> */
Atom atom_tray_opcode; /* _NET_SYSTEM_TRAY_OPCODE */
Atom atom_xembed; /* _XEMBED */
Atom atom_manager; /* MANAGER */
} Deskbar; } Deskbar;
void db_init(Deskbar *db); void db_init(Deskbar *db);
+123 -2
View File
@@ -1,5 +1,9 @@
#include "deskbar.h" #include "deskbar.h"
#define TRAY_REQUEST_DOCK 0
#define XEMBED_EMBEDDED_NOTIFY 0
#define XEMBED_VERSION 0
/* ── layout ───────────────────────────────────────────────────────────── */ /* ── layout ───────────────────────────────────────────────────────────── */
int db_apps_y(void) { return DB_TOP_H + 1; } int db_apps_y(void) { return DB_TOP_H + 1; }
@@ -8,8 +12,11 @@ int db_ws_y(Deskbar *db) {
return db_apps_y() + db->napps * DB_ITEM_H + 1; return db_apps_y() + db->napps * DB_ITEM_H + 1;
} }
static int db_tray_h(Deskbar *db) { return db->n_tray_icons > 0 ? DB_TRAY_H : 0; }
static int db_tray_y(Deskbar *db) { return db_ws_y(db) + DB_WS_H + 1; }
static int db_status_y(Deskbar *db) { static int db_status_y(Deskbar *db) {
return db_ws_y(db) + DB_WS_H + 1; int th = db_tray_h(db);
return db_tray_y(db) + th + (th > 0 ? 1 : 0);
} }
void db_resize(Deskbar *db) { void db_resize(Deskbar *db) {
@@ -141,6 +148,65 @@ void db_update_workspace(Deskbar *db) {
db->num_ws = (nws > 0) ? (int)nws : 4; db->num_ws = (nws > 0) ? (int)nws : 4;
} }
/* ── system tray (XEmbed) ─────────────────────────────────────────────── */
static void db_tray_reposition(Deskbar *db) {
int ty = db_tray_y(db);
int iy = (DB_TRAY_H - DB_TRAY_ICON_SZ) / 2;
for (int i = 0; i < db->n_tray_icons; i++) {
int ix = 4 + i * (DB_TRAY_ICON_SZ + 4);
XMoveWindow(db->dpy, db->tray_icons[i], ix, ty + iy);
}
}
static void db_tray_dock(Deskbar *db, Window icon_win) {
if (db->n_tray_icons >= DB_TRAY_MAX) return;
for (int i = 0; i < db->n_tray_icons; i++)
if (db->tray_icons[i] == icon_win) return;
int ty = db_tray_y(db);
int iy = (DB_TRAY_H - DB_TRAY_ICON_SZ) / 2;
int ix = 4 + db->n_tray_icons * (DB_TRAY_ICON_SZ + 4);
int was_empty = (db->n_tray_icons == 0);
XSelectInput(db->dpy, icon_win, StructureNotifyMask);
XReparentWindow(db->dpy, icon_win, db->win, ix, ty + iy);
XResizeWindow(db->dpy, icon_win, DB_TRAY_ICON_SZ, DB_TRAY_ICON_SZ);
XMapWindow(db->dpy, icon_win);
/* XEMBED_EMBEDDED_NOTIFY message to the icon */
XEvent ev = {0};
ev.xclient.type = ClientMessage;
ev.xclient.window = icon_win;
ev.xclient.message_type = db->atom_xembed;
ev.xclient.format = 32;
ev.xclient.data.l[0] = CurrentTime;
ev.xclient.data.l[1] = XEMBED_EMBEDDED_NOTIFY;
ev.xclient.data.l[2] = 0;
ev.xclient.data.l[3] = db->win;
ev.xclient.data.l[4] = XEMBED_VERSION;
XSendEvent(db->dpy, icon_win, False, NoEventMask, &ev);
db->tray_icons[db->n_tray_icons++] = icon_win;
if (was_empty) db_resize(db);
db_draw(db);
}
static void db_tray_remove(Deskbar *db, Window icon_win) {
int pos = -1;
for (int i = 0; i < db->n_tray_icons; i++)
if (db->tray_icons[i] == icon_win) { pos = i; break; }
if (pos < 0) return;
memmove(&db->tray_icons[pos], &db->tray_icons[pos + 1],
(db->n_tray_icons - pos - 1) * sizeof(Window));
db->n_tray_icons--;
db_tray_reposition(db);
if (db->n_tray_icons == 0) db_resize(db);
db_draw(db);
}
/* ── system status ────────────────────────────────────────────────────── */ /* ── system status ────────────────────────────────────────────────────── */
static int read_cpu_percent(void) { static int read_cpu_percent(void) {
@@ -429,6 +495,17 @@ void db_draw(Deskbar *db) {
XDrawLine(db->dpy, db->win, db->gc, XDrawLine(db->dpy, db->win, db->gc,
0, wy + DB_WS_H, DB_W, wy + DB_WS_H); 0, wy + DB_WS_H, DB_W, wy + DB_WS_H);
/* ── SYSTEM TRAY ── */
if (db->n_tray_icons > 0) {
int ty = db_tray_y(db);
XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BG));
XFillRectangle(db->dpy, db->win, db->gc, 0, ty, DB_W, DB_TRAY_H);
/* separator below tray */
XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BORDER_D));
XDrawLine(db->dpy, db->win, db->gc, 0, ty + DB_TRAY_H, DB_W, ty + DB_TRAY_H);
}
/* ── STATUS BAR ── */ /* ── STATUS BAR ── */
int sy = db_status_y(db); int sy = db_status_y(db);
XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BG)); XSetForeground(db->dpy, db->gc, db_color(db, DB_COL_BG));
@@ -807,6 +884,7 @@ void db_init(Deskbar *db) {
db->sub_active = -1; db->sub_active = -1;
db->sub_hov = -1; db->sub_hov = -1;
db->sw_cache = 0; db->sw_cache = 0;
db->n_tray_icons = 0;
db->net_client_list = XInternAtom(db->dpy, "_NET_CLIENT_LIST", False); db->net_client_list = XInternAtom(db->dpy, "_NET_CLIENT_LIST", False);
db->net_active_window = XInternAtom(db->dpy, "_NET_ACTIVE_WINDOW", False); db->net_active_window = XInternAtom(db->dpy, "_NET_ACTIVE_WINDOW", False);
@@ -815,6 +893,15 @@ void db_init(Deskbar *db) {
db->net_wm_name = XInternAtom(db->dpy, "_NET_WM_NAME", False); db->net_wm_name = XInternAtom(db->dpy, "_NET_WM_NAME", False);
db->utf8_string = XInternAtom(db->dpy, "UTF8_STRING", False); db->utf8_string = XInternAtom(db->dpy, "UTF8_STRING", False);
{
char sel_name[32];
snprintf(sel_name, sizeof(sel_name), "_NET_SYSTEM_TRAY_S%d", db->screen);
db->atom_tray_s0 = XInternAtom(db->dpy, sel_name, False);
}
db->atom_tray_opcode = XInternAtom(db->dpy, "_NET_SYSTEM_TRAY_OPCODE", False);
db->atom_xembed = XInternAtom(db->dpy, "_XEMBED", False);
db->atom_manager = XInternAtom(db->dpy, "MANAGER", False);
db->font_ui = XftFontOpenName(db->dpy, db->screen, DB_FONT_UI); db->font_ui = XftFontOpenName(db->dpy, db->screen, DB_FONT_UI);
db->font_menu = XftFontOpenName(db->dpy, db->screen, DB_FONT_MENU); db->font_menu = XftFontOpenName(db->dpy, db->screen, DB_FONT_MENU);
db->font_clock = XftFontOpenName(db->dpy, db->screen, DB_FONT_CLOCK); db->font_clock = XftFontOpenName(db->dpy, db->screen, DB_FONT_CLOCK);
@@ -829,7 +916,7 @@ void db_init(Deskbar *db) {
db_update_apps(db); db_update_apps(db);
db_update_workspace(db); db_update_workspace(db);
db->bar_h = DB_TOP_H + 1 + db->napps * DB_ITEM_H + 1 + DB_WS_H + 1 + DB_STATUS_H + 2; db->bar_h = db_status_y(db) + DB_STATUS_H + 2;
int sw = DisplayWidth(db->dpy, db->screen); int sw = DisplayWidth(db->dpy, db->screen);
@@ -847,6 +934,20 @@ void db_init(Deskbar *db) {
CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); CWOverrideRedirect | CWBackPixel | CWEventMask, &swa);
db->gc = XCreateGC(db->dpy, db->win, 0, NULL); db->gc = XCreateGC(db->dpy, db->win, 0, NULL);
/* Claim _NET_SYSTEM_TRAY_S<screen> selection and announce ourselves */
XSetSelectionOwner(db->dpy, db->atom_tray_s0, db->win, CurrentTime);
if (XGetSelectionOwner(db->dpy, db->atom_tray_s0) == db->win) {
XEvent sev = {0};
sev.xclient.type = ClientMessage;
sev.xclient.window = db->root;
sev.xclient.message_type = db->atom_manager;
sev.xclient.format = 32;
sev.xclient.data.l[0] = CurrentTime;
sev.xclient.data.l[1] = db->atom_tray_s0;
sev.xclient.data.l[2] = db->win;
XSendEvent(db->dpy, db->root, False, StructureNotifyMask, &sev);
}
Atom net_wm_window_type = XInternAtom(db->dpy, "_NET_WM_WINDOW_TYPE", False); Atom net_wm_window_type = XInternAtom(db->dpy, "_NET_WM_WINDOW_TYPE", False);
Atom net_wm_type_dock = XInternAtom(db->dpy, "_NET_WM_WINDOW_TYPE_DOCK", False); Atom net_wm_type_dock = XInternAtom(db->dpy, "_NET_WM_WINDOW_TYPE_DOCK", False);
XChangeProperty(db->dpy, db->win, net_wm_window_type, XA_ATOM, 32, XChangeProperty(db->dpy, db->win, net_wm_window_type, XA_ATOM, 32,
@@ -943,10 +1044,30 @@ void db_run(Deskbar *db) {
e.xvisibility.state != VisibilityUnobscured) e.xvisibility.state != VisibilityUnobscured)
XRaiseWindow(db->dpy, db->win); XRaiseWindow(db->dpy, db->win);
break; break;
case ClientMessage:
if (e.xclient.message_type == db->atom_tray_opcode &&
e.xclient.data.l[1] == TRAY_REQUEST_DOCK)
db_tray_dock(db, (Window)e.xclient.data.l[2]);
break;
case DestroyNotify:
db_tray_remove(db, e.xdestroywindow.window);
break;
case ReparentNotify:
if (e.xreparent.parent != db->win)
db_tray_remove(db, e.xreparent.window);
break;
case SelectionClear:
if (e.xselectionclear.selection == db->atom_tray_s0) {
db->n_tray_icons = 0;
db_resize(db);
db_draw(db);
}
break;
case PropertyNotify: case PropertyNotify:
if (e.xproperty.atom == db->net_client_list) { if (e.xproperty.atom == db->net_client_list) {
/* Window list changed — full refresh + resize */ /* Window list changed — full refresh + resize */
db_update_apps(db); db_update_apps(db);
db_tray_reposition(db);
db_resize(db); db_resize(db);
db_draw(db); db_draw(db);
} else if (e.xproperty.atom == db->net_active_window) { } else if (e.xproperty.atom == db->net_active_window) {
+83 -2
View File
@@ -101,6 +101,7 @@ git
sudo sudo
bash-completion bash-completion
fastfetch fastfetch
plymouth
PKGS PKGS
# ── 3. airootfs overlay — files that land in the live system ───────── # ── 3. airootfs overlay — files that land in the live system ─────────
@@ -188,7 +189,7 @@ cat > "$AIROOTFS/etc/mkinitcpio.conf.d/archiso.conf" <<'MKCONF'
MODULES=() MODULES=()
BINARIES=() BINARIES=()
FILES=() FILES=()
HOOKS=(base udev microcode archiso block filesystems keyboard) HOOKS=(base udev plymouth microcode archiso block filesystems keyboard)
MKCONF MKCONF
echo ">>> Live initramfs HOOKS: base udev microcode archiso block filesystems keyboard" echo ">>> Live initramfs HOOKS: base udev microcode archiso block filesystems keyboard"
@@ -531,6 +532,83 @@ gtk-icon-theme-name=Adwaita
gtk-cursor-theme-name=DMZ-White gtk-cursor-theme-name=DMZ-White
GTK3 GTK3
# ── Plymouth dark navy boot splash ───────────────────────────────────
echo ">>> Creating Plymouth theme..."
PLYM="$AIROOTFS/usr/share/plymouth/themes/skyos"
mkdir -p "$PLYM"
cat > "$PLYM/skyos.plymouth" <<'PLYMDEF'
[Plymouth Theme]
Name=SkyOS
Description=SkyOS dark navy boot splash
ModuleName=script
[script]
ImageDir=/usr/share/plymouth/themes/skyos
ScriptFile=/usr/share/plymouth/themes/skyos/skyos.script
PLYMDEF
cat > "$PLYM/skyos.script" <<'PLYMSCRIPT'
# SkyOS Plymouth boot splash — dark navy + accent blue progress bar
W = Window.GetWidth();
H = Window.GetHeight();
# Solid dark navy background
Window.SetBackgroundTopColor(0.067, 0.082, 0.125);
Window.SetBackgroundBottomColor(0.040, 0.055, 0.090);
# Logo (centered, upper third of screen)
logo = Image("logo.png");
logo_sprite = Sprite(logo);
logo_sprite.SetPosition(
Math.Int((W - logo.GetWidth()) / 2),
Math.Int(H * 0.40 - logo.GetHeight() / 2),
1
);
# Progress bar
BAR_W = Math.Int(Math.Min(W * 0.42, 440));
BAR_H = 4;
bar_x = Math.Int((W - BAR_W) / 2);
bar_y = Math.Int(H * 0.72);
# Bar background track (dark blue-grey)
bar_bg = Image("bar_bg.png").Scale(BAR_W, BAR_H);
bar_bg_sprite = Sprite(bar_bg);
bar_bg_sprite.SetPosition(bar_x, bar_y, 2);
# Bar fill (accent blue, grows with progress)
bar_fill_sprite = Sprite();
bar_fill_sprite.SetPosition(bar_x, bar_y, 3);
fun boot_progress_callback(time, progress) {
fill_w = Math.Int(BAR_W * progress);
if (fill_w < 2) fill_w = 2;
bar_fill = Image("bar_fg.png").Scale(fill_w, BAR_H);
bar_fill_sprite.SetImage(bar_fill);
}
Plymouth.SetBootProgressFunction(boot_progress_callback);
PLYMSCRIPT
# Generate PNG assets (ImageMagick is used above for the wallpaper)
if command -v convert &>/dev/null; then
# Logo: "SkyOS" in accent blue on transparent background
convert -size 340x90 xc:none \
-font DejaVu-Sans-Bold -pointsize 62 \
-fill "#90BBEE" -gravity center \
-annotate 0 "SkyOS" \
"$PLYM/logo.png" 2>/dev/null \
&& echo " Plymouth logo generated." \
|| echo " Plymouth logo generation failed (non-fatal)."
# 1×1 pixel images — scaled at runtime by the script
convert -size 1x1 xc:"#1C2840" "$PLYM/bar_bg.png" 2>/dev/null || true
convert -size 1x1 xc:"#1E58A8" "$PLYM/bar_fg.png" 2>/dev/null || true
else
echo " ImageMagick not available — Plymouth PNGs skipped."
fi
# ── Mac hardware: libinput touchpad config ──────────────────────────── # ── Mac hardware: libinput touchpad config ────────────────────────────
mkdir -p "$AIROOTFS/etc/X11/xorg.conf.d" mkdir -p "$AIROOTFS/etc/X11/xorg.conf.d"
cat > "$AIROOTFS/etc/X11/xorg.conf.d/40-libinput.conf" <<'TOUCHPAD' cat > "$AIROOTFS/etc/X11/xorg.conf.d/40-libinput.conf" <<'TOUCHPAD'
@@ -626,7 +704,7 @@ cat > "$PROFILE/efiboot/loader/entries/skyos.conf" <<'ENTRY'
title SkyOS title SkyOS
linux /arch/boot/x86_64/vmlinuz-linux linux /arch/boot/x86_64/vmlinuz-linux
initrd /arch/boot/x86_64/initramfs-linux.img initrd /arch/boot/x86_64/initramfs-linux.img
options archisobasedir=arch archisolabel=%ARCHISO_LABEL% quiet loglevel=3 options archisobasedir=arch archisolabel=%ARCHISO_LABEL% quiet splash loglevel=3
ENTRY ENTRY
@@ -694,6 +772,9 @@ update-desktop-database 2>/dev/null || true
# Fontconfig cache # Fontconfig cache
fc-cache -f 2>/dev/null || true fc-cache -f 2>/dev/null || true
# Plymouth: set SkyOS as the default theme (archiso runs mkinitcpio after this)
plymouth-set-default-theme skyos 2>/dev/null || true
# xscreensaver config (screen lock) # xscreensaver config (screen lock)
mkdir -p /etc/skel mkdir -p /etc/skel
cat > /etc/skel/.xscreensaver <<'SS' cat > /etc/skel/.xscreensaver <<'SS'