Skip to content

feat: GNOME Shell extension for lock screen / GDM camera overlay#1120

Open
HackOverflow404 wants to merge 228 commits into
boltgolt:devfrom
HackOverflow404:master
Open

feat: GNOME Shell extension for lock screen / GDM camera overlay#1120
HackOverflow404 wants to merge 228 commits into
boltgolt:devfrom
HackOverflow404:master

Conversation

@HackOverflow404

@HackOverflow404 HackOverflow404 commented Jun 11, 2026

Copy link
Copy Markdown

What and why

When Howdy authenticates at the lock screen or GDM login screen, there is no visual feedback; the user stares at a static screen with no indication that the camera is active, whether their face is in frame, or if anything is happening at all. On the desktop (sudo/polkit), compare.py already opens an OpenCV window, but that approach doesn't work on the lock screen because Mutter blocks X11 windows from appearing above the lock shield.

This PR adds a GNOME Shell extension that shows the live annotated camera feed above the lock screen and GDM greeter while authentication is running.

Howdy Feature Image 1

How it works

compare.py writes two files to /tmp while the auth is in progress:

File Purpose
/tmp/howdy-active Sentinel: exists while auth is running, deleted on exit
/tmp/howdy-frame.jpg Latest annotated frame, atomically replaced via os.replace

The GNOME Shell extension (howdy-screen@howdy) polls /tmp/howdy-active at 250 ms intervals. When it appears, and the session is locked, or at the greeter, a small overlay panel is shown, and the JPEG is reloaded ~10×/s using GdkPixbuf + St.ImageContent.

Why file-based IPC: compare.py runs as root inside a PAM transaction. The extension runs as the user (or gdm). A Unix socket would work, but requires privilege management. Files in /tmp are simpler, atomic, and work across all session modes.

Why St.ImageContent and not St.Icon: StTextureCache caches gicons by URI with no file-change monitor, causing the feed to freeze on the first frame. GdkPixbuf + St.ImageContent bypasses the cache entirely.

Why session-modes: ["unlock-dialog", "gdm"]: Without unlock-dialog in metadata.json, GNOME Shell disables the extension the instant the screen locks. This is the key insight that makes the lock-screen case work.

Changes

howdy/src/compare.py

  • render_frame_to_window() - new function that draws face annotations and writes the JPEG. Runs on every non-black frame; the OpenCV window (cv2.imshow) is gated separately by do_imshow so JPEG frames are produced even at the Wayland GDM greeter, where no X display exists.
  • get_compositor() - scans the authenticating user's /proc environment to detect the running Wayland compositor (sway, Hyprland, KDE, GNOME, or generic X11/Wayland). Used by raise_window_once() to pick the right tool.
  • raise_window_once() - dispatches to swaymsg (sway), hyprctl (Hyprland), or xdotool (X11 / KDE / GNOME / unknown). Falls back to xdotool if the compositor-specific tool is not installed. Safe to call multiple times.
  • Signal files (/tmp/howdy-active, /tmp/howdy-frame.jpg) are cleaned up in exit() so the overlay closes as soon as auth ends.
  • Dark frames are no longer skipped before rendering; they are displayed with a "Too dark" annotation, so the user sees live camera output regardless.

howdy/src/config.ini

Three new options in [video] (all default to false, zero behaviour change for existing installs):

overlay = false      # write JPEG frames for the GNOME Shell extension
show_window = false  # OpenCV popup during sudo/polkit in logged-in sessions
mirror = false       # horizontally flip the feed (does not affect recognition)

howdy-gnome-extension/ (new directory)

  • howdy-screen@howdy/extension.js - the GNOME Shell extension (GNOME 45+)
  • howdy-screen@howdy/metadata.json
  • install.sh - deploys to ~/.local/share/ (user session + lock screen) and /usr/share/ + a gdm dconf keyfile (greeter). Re-run after edits to redeploy.
  • README.md - full setup docs including Wayland compatibility table, GNOME version requirements, PrivateTmp notes for distro maintainers, and optional dependency info.

Compatibility

GNOME version 45+ (ESM extension format). Ubuntu 22.04 LTS (GNOME 42) is not supported.
Session type Wayland and X11. The extension is Wayland-native; show_window uses XWayland and works on all compositors.
Compositors sway (swaymsg), Hyprland (hyprctl), KDE + X11 (xdotool), GNOME (xdotool; overlay extension preferred)
Optional deps xdotool for X11/KDE/GNOME window raising; swaymsg/hyprctl ship with their compositors

Security note

/tmp/howdy-frame.jpg is written world-readable (0644, owned by root). The gdm user is not root and must be able to read the file for the greeter overlay to work. This means any local user can read camera frames from that path while authentication is in progress. This is a deliberate design choice. Users who consider this unacceptable can leave overlay = false (the default).

Testing

  1. cd howdy-gnome-extension && ./install.sh
  2. sudo howdy config → set overlay = true in [video]
  3. Lock screen (Super+L) → move face in front of camera, overlay should appear
  4. Log out to GDM greeter → trigger howdy, overlay should appear there too
  5. Set show_window = true → run sudo ls, OpenCV popup should appear
Howdy Feature Image 3 Howdy Feature Image 4 - Matched

Logs: sudo cat /tmp/howdy-debug.log (written only when overlay=true).

saidsay-so and others added 30 commits December 26, 2020 09:33
Signed-off-by: MusiKid <musikid@outlook.com>
Signed-off-by: MusiKid <musikid@outlook.com>
Signed-off-by: MusiKid <musikid@outlook.com>
Signed-off-by: MusiKid <musikid@outlook.com>
Signed-off-by: MusiKid <musikid@outlook.com>
Add python-opencv as a dependency for ArchLinux
dthelegend and others added 29 commits May 20, 2024 20:10
Update datetime since datetime.utcnow() was deprecated since Python 3.12.
…lation

Since pam_python depends on python2 and python2 cannot be installed through fedoras repositories the installation will fail. The beta of Howdy has fixed this issue. 
This problem is also explained in the copr repo. But it requires some searching.
Remove use of Environment Python
fix(pam): use environ variable when getenv doesn't work
Fix unwanted unlocks when auth client subprocesses exit with 0
Connect to signals of the shown window
Update README.md for Fedora 41 users
The current EDITOR handling is a bit lacking.

Improvements:

- check that the editor exists before trying to spawn a subprocess
- add vi as nano alternative
- if everything else fails, perhaps the user is using sudo. Suggest
  using 'sudo -E howdy [...]', as otherwise the command would run in
  a non-interactive shell (i.e. without sourcing the initialization
  files), and no environment variable is carried over (including
  EDITOR) unless explicitly preserved
…ndling

feat(config): better EDITOR handling
The OpenCV window from compare.py can't draw above the GNOME lock-screen
shield or on the Wayland GDM greeter, so add a GNOME Shell extension
(howdy-screen@howdy) that renders the howdy camera feed there instead.

Extension (howdy-gnome-extension/):
- metadata.json declares session-modes ["user","unlock-dialog","gdm"];
  without "unlock-dialog" GNOME disables the extension the moment the
  screen locks, which is why it never showed on the lock screen.
- extension.js loads each frame via GdkPixbuf -> St.ImageContent instead
  of St.Icon + Gio.FileIcon, whose StTextureCache froze the feed on the
  first frame. Overlay is gated to non-user session modes so the
  logged-in case keeps using compare.py's OpenCV window.
- install.sh deploys to ~/.local (session + lock screen) and, via sudo,
  to /usr/share + a gdm dconf keyfile (the greeter runs as user gdm and
  can't read ~/.local).

compare.py:
- Decouple JPEG frame-writing (/tmp/howdy-frame.jpg, consumed by the
  extension) from cv2.imshow via a do_imshow flag and a new "overlay"
  config option (default on). The greeter has no usable X display for
  cv2.imshow but still needs the frames.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The extension skipped reloading a frame unless the JPEG's mtime changed,
but it read mtime at whole-second resolution (to_unix()). compare.py
writes ~3 frames/sec, so frames sharing a second were treated as
unchanged and the overlay refreshed only ~1x/sec.

Compare time::modified + time::modified-usec for microsecond resolution
so every written frame is picked up. Bump version to 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
: Cogl.PixelFormat.RGB_888;

const content = St.ImageContent.new_with_preferred_size(w, h);
const ok = content.set_bytes(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'const' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).

? Cogl.PixelFormat.RGBA_8888
: Cogl.PixelFormat.RGB_888;

const content = St.ImageContent.new_with_preferred_size(w, h);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'const' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).

const w = pixbuf.get_width();
const h = pixbuf.get_height();
const fmt = pixbuf.get_has_alpha()
? Cogl.PixelFormat.RGBA_8888

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading line break before '?'; readers may interpret this as an expression boundary.


const w = pixbuf.get_width();
const h = pixbuf.get_height();
const fmt = pixbuf.get_has_alpha()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'const' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).

this._frameTime = stamp;

const w = pixbuf.get_width();
const h = pixbuf.get_height();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'const' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).

this._pollId = null;
}
if (this._overlay) {
this._overlay.get_parent()?.remove_child(this._overlay);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected ':' and instead saw 'remove_child'.
Expected an assignment or function call and instead saw an expression.
Expected an identifier and instead saw '.'.

this._overlay.set_position(40, 80);

this._frameTime = 0;
this._pollId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, POLL_MS, () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'arrow function syntax (=>)' is only available in ES6 (use 'esversion: 6').


// screenShieldGroup sits above the lock-screen shield (and exists in
// the gdm greeter too); fall back to uiGroup just in case.
const parent = Main.layoutManager.screenShieldGroup ?? Main.layoutManager.uiGroup;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'const' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).
Expected ':' and instead saw 'Main'.
Expected an assignment or function call and instead saw an expression.
Expected an identifier and instead saw '.'.
Expected an identifier and instead saw '?'.
Missing semicolon.

reactive: false,
});

const label = new St.Label({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'const' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).

}

_modeAllowsOverlay() {
const mode = Main.sessionMode?.currentMode;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'const' is available in ES6 (use 'esversion: 6') or Mozilla JS extensions (use moz).
Expected ':' and instead saw 'currentMode'.
Expected an identifier and instead saw '.'.
Expected an identifier and instead saw ';'.
Missing semicolon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.