Skip to content

[Bug] Windows are mapped with an empty title, making window-manager rules unreliable on Linux #1430

Description

@dagimg-dot

Description

Every window except the main one is created with an empty title and renamed a fraction of a second later. Linux window managers evaluate their rules when a window is mapped, so any rule matching on title (float/tile, size, workspace assignment) applies only when it happens to win the race.

The result is nondeterministic: the same dialog floats on one open and tiles on the next, with no config change in between.

The root cause is a difference between how the main window and every other window get their title:

  • HomeWindow.kt:57 seeds the controller up front:

    val windowController = rememberWindowController(AppInfo.displayName, windowIcon.rememberPainter())

    so Window(title = ...) receives a real title at creation.

  • Every other window uses the default controller, CustomWindow.kt:361:

    windowController: WindowController = remember { WindowController() }   // title = null

    CustomWindow.kt:373 then computes val title = windowController.title.orEmpty(), which is "", and that empty string is what Window(title = title) receives at CustomWindow.kt:392. The real title arrives later through WindowTitle() (CustomWindow.kt:540), which assigns controller.title from a LaunchedEffect — that is, after the first composition, by which point the window may already be mapped.

22 files call CustomWindow(...); only HomeWindow seeds a title. The other 21 map with an empty one.

App Version

1.10.1. The code paths above are unchanged on master as of 2026-09-22.

Platform

Arch Linux (aarch64), niri 26.04 Wayland session, app running under Xwayland.

This is not niri-specific. The same applies to sway for_window [title=...], Hyprland windowrulev2, i3 and KWin rules, all of which match at map time.

Installation Type

Installation script / tarball.

Steps to Reproduce

  1. On any Linux window manager, add a rule that floats the Add Download window by title. For niri:

    window-rule {
      match app-id="^com-abdownloadmanager-desktop-AppKt$"
      open-floating false
    }
    
    window-rule {
      match app-id="^com-abdownloadmanager-desktop-AppKt$" title="^Add Download$"
      open-floating true
    }
  2. Open the Add Download window several times in a row.

  3. Observe that it floats only sometimes.

Watching the compositor event stream shows why. The title at map time is ' ', and changes afterwards:

id=281  floating=False  title=' '
id=281  floating=False  title='Add Download'   <- too late, rule already evaluated
id=282  floating=True   title='Add Download'   <- won the race
id=283  floating=False  title=' '
id=283  floating=False  title='Add Download'
id=284  floating=False  title=' '
id=284  floating=False  title='Add Download'

Five of six opens missed the rule. By contrast, the main window is correct on its very first event:

id=287  floating=False  title='AB Download Manager'

Expected Behavior

Windows should be created with their final title, so that window-manager rules evaluated at map time behave deterministically, as the main window already does.

Additional Information

A secondary symptom: because the title is empty rather than merely wrong, window switchers and taskbars briefly show a blank entry for each dialog.

A scope note on translations. This fixes the timing, not portability. Dialog titles come from myStringResource(...) and stay localized, so a user's rule still has to name the title in their own app language. A fully language-independent handle is not available today: AWT sets WM_CLASS once per process, so all windows share a single app-id, and niri has no matcher for WM_WINDOW_ROLE. Deterministic-per-language is the achievable goal, and it is enough in practice: a rule keyed on the main window's title works in every language, because AppInfo.displayName resolves to the build constant BuildConfig.APP_DISPLAY_NAME rather than a translated resource.

Possible Solution

Let CustomWindow accept a title and seed the default controller with it:

// CustomWindow.kt
title: String? = null,
windowController: WindowController = remember { WindowController(title) },

This is backward compatible. Existing call sites compile unchanged and behave exactly as they do today, so the 21 call sites can be converted incrementally rather than in one sweep. Call sites then pass the title they already know, for example in SettingWindow.kt:

CustomWindow(
    state = windowState,
    onCloseRequest = { onRequestCloseWindow() },
    title = myStringResource(Res.string.settings),
) { ... }

WindowTitle() keeps working for titles that change at runtime, such as ones containing a filename. It simply stops being the only way a window ever gets a title.

I am happy to open a PR for the CustomWindow change plus the few windows that matter most for window-manager rules (Settings, Add Download, New Download), if you would like it in that shape.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions