You are not logged in.

#1 2026-03-08 18:53:57

Heikete
Member
Registered: 2007-04-28
Posts: 42

Alacritty run by sxhkd is not getting focus in cinnamon 6.6.7

Relevant setting in sxhkd:

alt + control + {t,f,k,j}
    {alacritty,firefox,kydpdict,kiten}

In cinnamon setting for new windows run from terminal is set to ON.

I've set:

gsettings set org.gnome.desktop.wm.preferences focus-new-windows 'strict'

Still the alacritty windows which are launched by sxhkd are not getting focus.
sxhkd is run from xinitrc.

Alacritty run from other terminal behaves the same.

I've tried adding 'Alacritty' to:

gsettings set org.cinnamon demands-attention-passthru-wm-classes

Still without success.
What can I do to make it focused when run, without an explicit click?

Offline

#2 2026-03-08 20:47:52

seth
Member
From: Won't reply 2 private help req
Registered: 2012-09-03
Posts: 75,662

Offline

#3 Yesterday 17:19:14

Heikete
Member
Registered: 2007-04-28
Posts: 42

Re: Alacritty run by sxhkd is not getting focus in cinnamon 6.6.7

Because the situation doesn't seem to move forward: namely alacritty doesn't set

_NET_WM_USER_TIME

as it is supposed to, with a little help from AI I managed to compile a little wrapper to put in sxhkdrc.

/ * Build: gcc -O2 -Wall focus-spawn.c -lX11 -o focus-spawn
 * Usage: focus-spawn [--log-file PATH] <command> [args...]
 */

#define _POSIX_C_SOURCE 200809L

#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

/* EWMH _NET_ACTIVE_WINDOW client-message source-indication values. */
#define EWMH_SOURCE_NONE 0
#define EWMH_SOURCE_APPLICATION 1
#define EWMH_SOURCE_PAGER 2

/* X11 ClientMessage uses 32-bit data items for EWMH messages. */
#define EWMH_CLIENT_MESSAGE_FORMAT 32

/* Polling for the child window and its WM_STATE. */
#define POLL_ATTEMPTS 60
#define POLL_INTERVAL_NS (50L * 1000L * 1000L)

#define LOG_PATH_MAX 512

static FILE *logfp = NULL;

static void lg(const char *fmt, ...) {
    if (!logfp) return;
    struct timespec tp;
    struct tm tm;
    clock_gettime(CLOCK_REALTIME, &tp);
    localtime_r(&tp.tv_sec, &tm);
    fprintf(logfp, "%02d:%02d:%02d.%03ld [%d] ", tm.tm_hour, tm.tm_min, tm.tm_sec,
        tp.tv_nsec/1000000, (int)getpid());
    va_list ap;
    va_start(ap, fmt);
    vfprintf(logfp, fmt, ap);
    va_end(ap);
    fputc('\n', logfp);
    fflush(logfp);
}

static int window_pid_matches(Display *dpy, Window w, pid_t pid,
                              Atom net_wm_pid) {
    Atom actual_type;
    int actual_format;
    unsigned long nitems = 0, bytes_after = 0;
    unsigned char *data = NULL;
    int ok = 0;

    if (XGetWindowProperty(dpy, w, net_wm_pid, 0, 1, False, XA_CARDINAL,
                           &actual_type, &actual_format, &nitems, &bytes_after,
                           &data) == Success) {
        if (data && actual_format == EWMH_CLIENT_MESSAGE_FORMAT && nitems >= 1)
            ok = ((pid_t)(*(unsigned long *)data) == pid);
        if (data) XFree(data);
    }
    return ok;
}

static Window find_window_by_pid(Display *dpy, Window root, pid_t pid,
                                 Atom net_wm_pid) {
    Window root_ret, parent_ret, *children = NULL;
    unsigned int nchildren = 0;
    Window result = 0;

    if (!XQueryTree(dpy, root, &root_ret, &parent_ret, &children, &nchildren))
        return 0;

    for (unsigned int i = 0; i < nchildren && !result; ++i) {
        Window c = children[i];
        if (window_pid_matches(dpy, c, pid, net_wm_pid)) {
            result = c;
            break;
        }
        /* Some toolkits wrap top-levels — peek one level deeper. */
        Window sr, sp, *sc = NULL;
        unsigned int sn = 0;
        if (XQueryTree(dpy, c, &sr, &sp, &sc, &sn)) {
            for (unsigned int j = 0; j < sn; ++j) {
                if (window_pid_matches(dpy, sc[j], pid, net_wm_pid)) {
                    result = sc[j];
                    break;
                }
            }
            if (sc) XFree(sc);
        }
    }
    if (children) XFree(children);
    return result;
}

/* ICCCM WM_STATE: NormalState (1) means the window is mapped. */
static int window_is_mapped(Display *dpy, Window w, Atom wm_state) {
    Atom actual_type;
    int actual_format;
    unsigned long nitems = 0, bytes_after = 0;
    unsigned char *data = NULL;
    int mapped = 0;

    if (XGetWindowProperty(dpy, w, wm_state, 0, 2, False, wm_state,
                           &actual_type, &actual_format, &nitems, &bytes_after,
                           &data) == Success) {
        if (data && nitems >= 1 &&
            actual_format == EWMH_CLIENT_MESSAGE_FORMAT)
            mapped = (*(unsigned long *)data == NormalState);
        if (data) XFree(data);
    }
    return mapped;
}

/* Wait for predicate(w) to become true, polling every POLL_INTERVAL_NS. */
typedef int (*window_predicate)(Display *, Window, Atom);
static int wait_for_window(Display *dpy, Window w, Atom arg,
                           window_predicate pred) {
    struct timespec ts = {0, POLL_INTERVAL_NS};
    for (int i = 0; i < POLL_ATTEMPTS; ++i) {
        if (pred(dpy, w, arg)) return 1;
        nanosleep(&ts, NULL);
    }
    return 0;
}

/*
 * Standard trick to fetch the current X server time: append zero bytes to a
 * property on a helper window with PropertyChangeMask, then read the time
 * from the resulting PropertyNotify event.
 */
static Time get_server_time(Display *dpy) {
    Window root = DefaultRootWindow(dpy);
    XSetWindowAttributes attr;
    memset(&attr, 0, sizeof(attr));
    Window helper = XCreateWindow(dpy, root, -1, -1, 1, 1, 0, 0, InputOnly,
                                  CopyFromParent, 0, &attr);
    XSelectInput(dpy, helper, PropertyChangeMask);
    Atom prop = XInternAtom(dpy, "_FOCUS_SPAWN_TIMESTAMP", False);
    XChangeProperty(dpy, helper, prop, XA_STRING, 8, PropModeAppend,
                    (unsigned char *)"", 0);

    XEvent ev;
    do {
        XNextEvent(dpy, &ev);
    } while (ev.type != PropertyNotify || ev.xproperty.window != helper);

    Time t = ev.xproperty.time;
    XDestroyWindow(dpy, helper);
    return t;
}

static void send_activate(Display *dpy, Window target, Time t,
                          Atom net_active, Atom net_wm_user_time) {
    Window root = DefaultRootWindow(dpy);
    unsigned long ut = (unsigned long)t;

    XChangeProperty(dpy, target, net_wm_user_time, XA_CARDINAL,
                    EWMH_CLIENT_MESSAGE_FORMAT, PropModeReplace,
                    (unsigned char *)&ut, 1);

    XEvent ev;
    memset(&ev, 0, sizeof(ev));
    ev.xclient.type = ClientMessage;
    ev.xclient.window = target;
    ev.xclient.message_type = net_active;
    ev.xclient.format = EWMH_CLIENT_MESSAGE_FORMAT;
    ev.xclient.data.l[0] = EWMH_SOURCE_PAGER;
    ev.xclient.data.l[1] = (long)t;

    XSendEvent(dpy, root, False,
               SubstructureNotifyMask | SubstructureRedirectMask, &ev);
    XRaiseWindow(dpy, target);
    /* Direct fallback: alacritty publishes an empty WM_HINTS (no InputHint),
     * which some WMs treat as "do not focus". XSetInputFocus bypasses the WM. */
    XSetInputFocus(dpy, target, RevertToParent, t);
    XFlush(dpy);
}

static void usage(const char *prog) {
    fprintf(stderr, "Usage: %s [--log-file PATH] <command> [args...]\n", prog);
}

int main(int argc, char *argv[]) {
    int argi = 1;
    if (argi < argc && strcmp(argv[argi], "--log-file") == 0) {
        if (argi + 1 >= argc) {
            usage(argv[0]);
            return 2;
        }
        logfp = fopen(argv[argi + 1], "a");
        if (!logfp)
            fprintf(stderr, "focus-spawn: cannot open log %s: %s\n",
                    argv[argi + 1], strerror(errno));
        argi += 2;
    }
    if (argi >= argc) {
        usage(argv[0]);
        return 2;
    }

    lg("start cmd=%s DISPLAY=%s", argv[argi],
       getenv("DISPLAY") ? getenv("DISPLAY") : "(unset)");

    pid_t pid = fork();
    if (pid < 0) {
        lg("fork: %s", strerror(errno));
        perror("fork");
        return 1;
    }
    if (pid == 0) {
        if (setsid() < 0) lg("setsid: %s", strerror(errno));
        execvp(argv[argi], &argv[argi]);
        lg("execvp %s: %s", argv[argi], strerror(errno));
        fprintf(stderr, "execvp %s: %s\n", argv[argi], strerror(errno));
        _exit(127);
    }
    lg("forked pid=%d", pid);

    Display *dpy = XOpenDisplay(NULL);
    if (!dpy) {
        lg("XOpenDisplay failed");
        fprintf(stderr, "focus-spawn: cannot open X display\n");
        return 1;
    }

    Atom net_wm_pid = XInternAtom(dpy, "_NET_WM_PID", False);
    Atom net_active = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
    Atom net_wm_user_time = XInternAtom(dpy, "_NET_WM_USER_TIME", False);
    Atom wm_state = XInternAtom(dpy, "WM_STATE", False);
    Window root = DefaultRootWindow(dpy);

    Window target = 0;
    struct timespec ts = {0, POLL_INTERVAL_NS};
    for (int i = 0; i < POLL_ATTEMPTS && !target; ++i) {
        target = find_window_by_pid(dpy, root, pid, net_wm_pid);
        if (!target) nanosleep(&ts, NULL);
    }
    if (!target) {
        lg("no window for pid %d", pid);
        fprintf(stderr, "focus-spawn: no window found for pid %d\n", pid);
        XCloseDisplay(dpy);
        return 0;
    }
    lg("found window 0x%lx", target);

    int mapped = wait_for_window(dpy, target, wm_state, window_is_mapped);
    lg("mapped=%d", mapped);

    Time t = get_server_time(dpy);
    lg("server time=%lu", (unsigned long)t);

    send_activate(dpy, target, t, net_active, net_wm_user_time);
    lg("activate sent");

    XCloseDisplay(dpy);
    if (logfp) fclose(logfp);
    return 0;
}

Offline

Board footer

Powered by FluxBB