diff --git a/.agents/MEMORY.md b/.agents/MEMORY.md index 1a4e413..1b5850b 100644 --- a/.agents/MEMORY.md +++ b/.agents/MEMORY.md @@ -65,6 +65,23 @@ loopback; current phase is Phase 7. before the fix). Cross-compiling on the dev machine was considered and dropped — the on-Pi build works and the toolchain/container effort was not needed. +- **App icons wired up** (`screencast_icon/`): single 256 px PNG used + everywhere. `meson install` ships it to + `share/icons/hicolor/256x256/apps/screencast.png`; the GTK panel sets it + via `set_icon_name("screencast")` (GTK4 removed the pixel-buffer window + icon — themed names only; `gtk_icon_theme_has_icon` verified OK); the + SDL receiver window embeds the PNG at build time (`icon_png_data.h` via + `scripts/icon_to_header.py` custom_target) and `SDL_SetWindowIcon` with + an optional SDL3_image dependency (no pkg-config ships with it → + `meson.get_compiler('cpp').find_library('SDL3_image', required: false)` + + disabler; `-DSC_HAS_WINDOW_ICON` gate; Pi builds without it still + work). The waybar widget now shows the icon as `background-image` in + `~/.config/waybar/style.css` (▶/⏸ text hidden; `.idle` dimmed, + `.streaming` green tint) instead of text glyphs; backup + `style.css.bak-20260909-*`. Lesson: icons appear in compositor + taskbars/switchers, not title bars (GTK4 CSD and Hyprland decorations + don't draw them), so validate via theme lookup, not title-bar + screenshots. `sudo pacman -S sdl3_image` done (extra/sdl3_image 3.4.6). - **GTK panel + waybar widget**: `screencast-gui` (gtkmm-4.0, behind `-Dgui=true`; app internals now live in the `sc_app_core` static lib so CLI and GUI share pipelines/session/state). `screencast waybar [--toggle]` diff --git a/README.md b/README.md index 5326501..8b44e3c 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,13 @@ screencast waybar # one JSON line for a waybar custom module screencast waybar --toggle ``` -The waybar module shows `▶` while streaming (tooltip: receiver, bitrate, -elapsed) and `⏸` while idle; left-click toggles streaming to the last -receiver, right-click opens the panel. All front-ends agree on state because -the sender publishes it to `$XDG_RUNTIME_DIR/screencast/sender.json`. +The waybar module shows the app icon (dimmed while idle, highlighted while +streaming; tooltip: receiver, bitrate, elapsed); left-click toggles +streaming to the last receiver, right-click opens the panel. All front-ends +agree on state because the sender publishes it to +`$XDG_RUNTIME_DIR/screencast/sender.json`. The icon (from `screencast_icon/`) +is installed into the hicolor theme by `meson install`, used by the GTK +panel and the waybar CSS, and embedded in the SDL receiver window. ## Under the hood: resilience diff --git a/docs/RUNBOOK.md b/docs/RUNBOOK.md index 6a24efe..fa7e5ec 100644 --- a/docs/RUNBOOK.md +++ b/docs/RUNBOOK.md @@ -146,11 +146,24 @@ screencast waybar --toggle # stop a running sender / restart the last ``` The waybar module (installed to this machine's `~/.config/waybar/` — see -`custom/screencast` there): `▶` while streaming (tooltip: receiver, bitrate, -elapsed), `⏸` while idle; left-click toggles streaming to the last receiver -via the state file, right-click opens the panel. The sender publishes its -state to `$XDG_RUNTIME_DIR/screencast/sender.json`, so every front-end — -CLI, GUI, widget — agrees on what is running. +`custom/screencast` there): the app icon as the module face (dimmed while +idle, green tint while streaming; the `▶`/`⏸` text from the JSON is hidden +in the CSS), tooltip with receiver, bitrate, elapsed; left-click toggles +streaming to the last receiver via the state file, right-click opens the +panel. The sender publishes its state to `$XDG_RUNTIME_DIR/screencast/sender.json`, +so every front-end — CLI, GUI, widget — agrees on what is running. + +**Icons**: `screencast_icon/screencast_256.png` is the single app icon. +`meson install` puts it in `share/icons/hicolor/256x256/apps/screencast.png`; +the GTK panel uses it by themed name (`set_icon_name` — GTK4 removed the +pixel-buffer window icon, so themed icons only), and the waybar CSS +references the installed path. The SDL receiver window embeds the PNG at +build time (`icon_png_data.h` generated by `scripts/icon_to_header.py`) and +sets it via `SDL_SetWindowIcon` when SDL3_image is available (optional +dependency; no pkg-config ships with it, so meson finds the library +directly). Icons surface in compositor taskbars/switchers, not in title +bars (neither GTK4's CSD nor Hyprland's decorations draw them). +Restart waybar after changing `style.css` to pick up icon changes. ## Under the hood: resilience (Phase 7) diff --git a/meson.build b/meson.build index f44b668..cd545c1 100644 --- a/meson.build +++ b/meson.build @@ -26,6 +26,12 @@ if enable_tests subdir('tests') endif +# Install the application icon into the hicolor theme: the waybar widget CSS +# and any future .desktop file reference it. +install_data('screencast_icon/screencast_256.png', + install_dir : get_option('datadir') / 'icons/hicolor/256x256/apps', + rename : 'screencast.png') + # Summary for the user summary({ 'tests': enable_tests, diff --git a/screencast_icon/android/ic_launcher_hdpi.png b/screencast_icon/android/ic_launcher_hdpi.png new file mode 100644 index 0000000..629b16f Binary files /dev/null and b/screencast_icon/android/ic_launcher_hdpi.png differ diff --git a/screencast_icon/android/ic_launcher_mdpi.png b/screencast_icon/android/ic_launcher_mdpi.png new file mode 100644 index 0000000..155dcf2 Binary files /dev/null and b/screencast_icon/android/ic_launcher_mdpi.png differ diff --git a/screencast_icon/android/ic_launcher_xhdpi.png b/screencast_icon/android/ic_launcher_xhdpi.png new file mode 100644 index 0000000..0830ba6 Binary files /dev/null and b/screencast_icon/android/ic_launcher_xhdpi.png differ diff --git a/screencast_icon/android/ic_launcher_xxhdpi.png b/screencast_icon/android/ic_launcher_xxhdpi.png new file mode 100644 index 0000000..1bfeeec Binary files /dev/null and b/screencast_icon/android/ic_launcher_xxhdpi.png differ diff --git a/screencast_icon/android/ic_launcher_xxxhdpi.png b/screencast_icon/android/ic_launcher_xxxhdpi.png new file mode 100644 index 0000000..924a185 Binary files /dev/null and b/screencast_icon/android/ic_launcher_xxxhdpi.png differ diff --git a/screencast_icon/screencast_1024.png b/screencast_icon/screencast_1024.png new file mode 100644 index 0000000..5b333be Binary files /dev/null and b/screencast_icon/screencast_1024.png differ diff --git a/screencast_icon/screencast_128.png b/screencast_icon/screencast_128.png new file mode 100644 index 0000000..877b803 Binary files /dev/null and b/screencast_icon/screencast_128.png differ diff --git a/screencast_icon/screencast_256.png b/screencast_icon/screencast_256.png new file mode 100644 index 0000000..aaa405e Binary files /dev/null and b/screencast_icon/screencast_256.png differ diff --git a/screencast_icon/screencast_32.png b/screencast_icon/screencast_32.png new file mode 100644 index 0000000..6d2fe79 Binary files /dev/null and b/screencast_icon/screencast_32.png differ diff --git a/screencast_icon/screencast_48.png b/screencast_icon/screencast_48.png new file mode 100644 index 0000000..2c8f451 Binary files /dev/null and b/screencast_icon/screencast_48.png differ diff --git a/screencast_icon/screencast_512.png b/screencast_icon/screencast_512.png new file mode 100644 index 0000000..39088e8 Binary files /dev/null and b/screencast_icon/screencast_512.png differ diff --git a/screencast_icon/screencast_64.png b/screencast_icon/screencast_64.png new file mode 100644 index 0000000..12b45a1 Binary files /dev/null and b/screencast_icon/screencast_64.png differ diff --git a/scripts/icon_to_header.py b/scripts/icon_to_header.py new file mode 100755 index 0000000..23038a3 --- /dev/null +++ b/scripts/icon_to_header.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Generate a C++ header that embeds an image file as a byte array. + +Usage: icon_to_header.py INPUT_IMAGE OUTPUT_HEADER +""" + +import sys + + +def main() -> int: + if len(sys.argv) != 3: + print(f"usage: {sys.argv[0]} INPUT_IMAGE OUTPUT_HEADER", file=sys.stderr) + return 2 + + with open(sys.argv[1], "rb") as src: + data = src.read() + + words = ", ".join(f"0x{byte:02x}" for byte in data) + header = f"""// GENERATED FILE - do not edit by hand. +// Produced by scripts/icon_to_header.py from screencast_icon/screencast_256.png. + +#pragma once + +#include +#include + +namespace sc {{ +inline constexpr std::array app_icon_png = {{ + {words} +}}; +}} // namespace sc +""" + with open(sys.argv[2], "w") as dst: + dst.write(header) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 62d4ff0..7b09d92 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -59,6 +59,9 @@ class SenderWindow : public Gtk::ApplicationWindow { SenderWindow() { set_title("screencast"); set_default_size(440, 440); + // GTK4 only supports themed icons: the PNG is installed into the + // hicolor theme (see meson.build) and picked up by name. + set_icon_name("screencast"); auto* box = Gtk::make_managed(Gtk::Orientation::VERTICAL, 8); set_child(*box); diff --git a/src/meson.build b/src/meson.build index 283d533..eeabd8b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,6 +1,16 @@ # Core public headers / include dependency. sc_core_inc = include_directories('../include') +# Embed the application icon so the GUI window and the SDL window icon work +# from the build tree, after install, and on headless targets without any +# runtime path lookup. +icon_to_header = find_program('../scripts/icon_to_header.py') +icon_png_data = custom_target('icon_png_data', + input : files('../screencast_icon/screencast_256.png'), + output : 'icon_png_data.h', + command : [icon_to_header, '@INPUT@', '@OUTPUT@']) +sc_icon_dep = declare_dependency(sources : [icon_png_data]) + # The sender needs the PipeWire / xdg-desktop-portal capture backend; # receiver-only builds skip it entirely. build_sender = get_option('sender') diff --git a/src/render/meson.build b/src/render/meson.build index 2ad6a1b..71153e1 100644 --- a/src/render/meson.build +++ b/src/render/meson.build @@ -2,14 +2,30 @@ dep_sdl3 = dependency('sdl3') +# SDL3_image (window icon) is optional: no pkg-config file ships with it, +# so find the library directly. Builds without it (e.g. the Pi receiver) +# simply skip the window icon. +cc = meson.get_compiler('cpp') +dep_sdl3_image = cc.find_library('SDL3_image', required: false) +if not dep_sdl3_image.found() + dep_sdl3_image = disabler() +endif + +sc_render_args = [] +if dep_sdl3_image.found() and cc.has_header('SDL3_image/SDL_image.h', dependencies : dep_sdl3_image) + sc_render_args += ['-DSC_HAS_WINDOW_ICON=1'] +endif + sc_render_sources = files('sdl_renderer.cpp') sc_render = static_library('sc_render', sc_render_sources, include_directories : sc_core_inc, - dependencies : [dep_sdl3]) + cpp_args : sc_render_args, + dependencies : [dep_sdl3, dep_sdl3_image, sc_icon_dep]) sc_render_dep = declare_dependency( link_with : sc_render, include_directories : sc_core_inc, - dependencies : [dep_sdl3]) \ No newline at end of file + compile_args : sc_render_args, + dependencies : [dep_sdl3, dep_sdl3_image]) \ No newline at end of file diff --git a/src/render/sdl_renderer.cpp b/src/render/sdl_renderer.cpp index 5528a2f..b25a0a5 100644 --- a/src/render/sdl_renderer.cpp +++ b/src/render/sdl_renderer.cpp @@ -4,6 +4,16 @@ #include +#ifdef SC_HAS_WINDOW_ICON + +#include + +#include + +#include "icon_png_data.h" + +#endif + namespace sc { namespace { @@ -52,6 +62,10 @@ class SdlRenderer final : public Renderer { (void)SDL_HideCursor(); } +#ifdef SC_HAS_WINDOW_ICON + set_window_icon(); +#endif + renderer_ = SDL_CreateRenderer(window_, nullptr); if (renderer_ == nullptr) { last_error_ = std::string{"SDL_CreateRenderer failed: "} + SDL_GetError(); @@ -155,6 +169,29 @@ class SdlRenderer final : public Renderer { } private: +#ifdef SC_HAS_WINDOW_ICON + // The icon is embedded at build time (see icon_png_data.h), so it works + // without any runtime file lookup. Harmless under KMSDRM, where there is + // no window manager to display it. + void set_window_icon() { + auto* stream = SDL_IOFromConstMem(sc::app_icon_png.data(), sc::app_icon_png.size()); + if (stream == nullptr) { + std::fprintf(stderr, "screencast: failed to create icon stream: %s\n", SDL_GetError()); + return; + } + // closeio=true: SDL_image consumes the stream, success or failure. + SDL_Surface* icon_surface = IMG_Load_IO(stream, true); + if (icon_surface == nullptr) { + std::fprintf(stderr, "screencast: failed to load window icon: %s\n", SDL_GetError()); + return; + } + if (SDL_SetWindowIcon(window_, icon_surface) != 0) { + std::fprintf(stderr, "screencast: failed to set window icon: %s\n", SDL_GetError()); + } + SDL_DestroySurface(icon_surface); + } +#endif + bool recreate_texture(int width, int height) { if (texture_ != nullptr) { SDL_DestroyTexture(texture_);