Skip to content

Releases: itsdezen/tili

v0.5.3

Choose a tag to compare

@github-actions github-actions released this 23 Jul 06:39

Recommended install: brew install itsdezen/tap/tili
Homebrew downloads via curl, which doesn't set macOS's
quarantine flag, so Gatekeeper never enters the picture.

The tarballs below are codesigned with tili's self-signed
certificate (hardened runtime) — see
CONTRIBUTING.md.
That's for a stable signing identity across releases (so
your Accessibility grant survives an upgrade), not for
passing Gatekeeper — a self-signed cert isn't notarized. If
you download tili.app directly from this page instead of
using Homebrew, Gatekeeper will still prompt on first launch:
right-click → Open, or xattr -d com.apple.quarantine tili.app.

Fixed

  • Finder's Quick Look preview panel (Space) and "Get Info" panel
    (Cmd+I) were floating and centering instead of being left alone
    , even
    when com.apple.finder wasn't configured as a floating app at all.
    Quick Look is a WindowKind::Popup, which already defaults to Ignore,
    but a floating-rules entry for com.apple.finder (written for its real
    browser windows) has no way to exclude this one auxiliary panel and an
    explicit rule always wins over the kind-based default. Get Info reaches
    WindowKind::Dialog via the zoom-but-no-fullscreen heuristic, whose own
    kind-based default is Float — wrong regardless of any rule, since the
    window should never be touched at all. Both are now force-Ignored
    unconditionally, the same mechanism already used for Finder's "Copy" and
    "Connect to Server" dialogs and System Settings' suggestion popup.

Full changelog: CHANGELOG.md

v0.5.2

Choose a tag to compare

@github-actions github-actions released this 22 Jul 18:00

Recommended install: brew install itsdezen/tap/tili
Homebrew downloads via curl, which doesn't set macOS's
quarantine flag, so Gatekeeper never enters the picture.

The tarballs below are codesigned with tili's self-signed
certificate (hardened runtime) — see
CONTRIBUTING.md.
That's for a stable signing identity across releases (so
your Accessibility grant survives an upgrade), not for
passing Gatekeeper — a self-signed cert isn't notarized. If
you download tili.app directly from this page instead of
using Homebrew, Gatekeeper will still prompt on first launch:
right-click → Open, or xattr -d com.apple.quarantine tili.app.

Fixed

  • A Preferences/Settings-style window (e.g. System Settings' own main
    window) could be force-Ignored instead of floating/centering
    , even
    with an explicit floating-rules entry asking for it to float.
    is_transient_empty_dialog (added to catch the input-source-switch HUD
    glyph) matched any WindowKind::Dialog with an empty title, but a
    Preferences/Settings-style window also reaches Dialog via
    classify_window_kind's zoom-but-no-fullscreen heuristic — and its
    AXTitle routinely reads empty at the exact moment tili scans it,
    before AX has populated it. That race got it misidentified as the
    transient HUD glyph and force-Ignored, overriding the user's own
    config. is_transient_empty_dialog now also requires the window to have
    no zoom button, which the HUD glyph never has but a Settings-style
    window always does.
  • System Settings' search-suggestions dropdown (shown while typing in its
    search field) was floating and centering instead of being left alone.

    Confirmed via diagnostic logging it's a borderless, chrome-less,
    empty-titled WindowKind::Popup — the same ambiguous shape as an
    ordinary tooltip/menu, which normally defaults to Ignore. The user's own
    rule app-id="com.apple.systempreferences" floating-rules entry (meant
    for the app's real Preferences windows) has no way to exclude just this
    one auxiliary window, and an explicit rule always wins over the
    kind-based default. Now force-Ignored unconditionally, regardless of
    floating-rules config, the same way Finder's protected dialogs are.

Full changelog: CHANGELOG.md

v0.5.1

Choose a tag to compare

@github-actions github-actions released this 22 Jul 17:05

Recommended install: brew install itsdezen/tap/tili
Homebrew downloads via curl, which doesn't set macOS's
quarantine flag, so Gatekeeper never enters the picture.

The tarballs below are codesigned with tili's self-signed
certificate (hardened runtime) — see
CONTRIBUTING.md.
That's for a stable signing identity across releases (so
your Accessibility grant survives an upgrade), not for
passing Gatekeeper — a self-signed cert isn't notarized. If
you download tili.app directly from this page instead of
using Homebrew, Gatekeeper will still prompt on first launch:
right-click → Open, or xattr -d com.apple.quarantine tili.app.

Fixed

  • Moving "the focused window" to another workspace could act on a stale,
    previously-focused window instead of whatever the user actually had
    focused
    — most reliably reproduced with a floating window (e.g. Note)
    sharing a workspace with a tiled one (e.g. Ghostty). Root-caused to two
    compounding gaps: (1) tili-daemon's event loop only applied a pending
    new-window registration on the next 30ms maintenance tick, so a hotkey
    fired right after opening a window could dispatch before that window was
    even known about — now drained before dispatching a hotkey- or
    socket-triggered command instead. (2) The system-wide focus check
    (sync_focus_from_frontmost, run before every command) read
    kAXFocusedWindowAttribute, an attribute macOS's system-wide
    accessibility element never actually populates — confirmed on real
    hardware it silently failed on every call, every time, regardless of
    what was focused. Now reads kAXFocusedUIElementAttribute (the one
    attribute the system-wide element reliably supports) and resolves the
    owning window from whatever control that returns.
  • tili uninstall could still delete the real config file when a
    dotfiles tool symlinked the whole ~/.config/tili directory
    , rather
    than just tili.kdl itself — the file-level symlink case was already
    handled, but a symlinked ancestor directory wasn't. Now walks every
    ancestor of the config path and leaves it in place if any of them is a
    symlink.
  • brew upgrade tili didn't actually restart onto the new binary.
    post_install's restart works by signaling the running process and
    relying on the LaunchAgent's KeepAlive to relaunch it — but the plist's
    cached path was fully resolved to a specific, version-pinned Cellar path,
    which never changes on its own, so every upgrade kept relaunching the
    exact same old binary until a manual tili stop && tili start. tili start now resolves that path through Homebrew's opt/tili symlink
    instead, which Homebrew itself relinks to the current keg on every
    upgrade. If you're upgrading from an affected version (v0.4.3 or
    later), run tili stop && tili start once after this upgrade
    to
    regenerate the LaunchAgent plist with the corrected path — this fix only
    changes what a future tili start writes, not an already-installed
    plist.

Full changelog: CHANGELOG.md

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 22 Jul 15:25

Recommended install: brew install itsdezen/tap/tili
Homebrew downloads via curl, which doesn't set macOS's
quarantine flag, so Gatekeeper never enters the picture.

The tarballs below are codesigned with tili's self-signed
certificate (hardened runtime) — see
CONTRIBUTING.md.
That's for a stable signing identity across releases (so
your Accessibility grant survives an upgrade), not for
passing Gatekeeper — a self-signed cert isn't notarized. If
you download tili.app directly from this page instead of
using Homebrew, Gatekeeper will still prompt on first launch:
right-click → Open, or xattr -d com.apple.quarantine tili.app.

Added

  • tili doctor [--fix] — checks config syntax, daemon/menu-bar
    LaunchAgent state, the IPC socket, Accessibility/Input Monitoring
    permission grants, and the last config load's warnings. Offers
    confirmed auto-fix for whatever's safely automatable (a stale socket, an
    unloaded LaunchAgent, a missing half of the daemon/menu-bar pair); a bad
    config file or an ungranted permission is reported only, never guessed
    at. --fix skips the confirmation prompt for scripting.

Changed

  • tili-daemon and tili-menubar now run as a synchronized pair,
    never one without the other.
    tili start requires both LaunchAgents
    to install, rolling the daemon back if the menu bar badge's install
    fails; tili-menubar gives up and stops itself after a sustained run of
    daemon-unreachable retries; tili-daemon's shutdown paths
    (Command::Shutdown, stop_self) tear the menu bar's LaunchAgent down
    too.

Fixed

  • A hotkey-triggered command that failed (no window focused, an
    undeclared workspace, ...) left no trace anywhere
    — unlike the
    socket path, which already reported the error back to tili-cli. Now
    logged.
  • The daemon's Unix socket had no cap on an incoming command's declared
    length
    , so a stray or malformed local client could drive an
    unbounded allocation (up to ~4 GiB) just from its 4-byte length prefix.
    Capped at 1 MiB.
  • tili start could print a spurious Load failed: 5: Input/output error from launchctl before its own "LaunchAgent installed" message.
    install_launch_agent called launchctl load -w unconditionally, even
    when the label was already loaded (e.g. the menu bar badge's LaunchAgent
    surviving from an earlier tili start) — a well-known legacy
    launchctl quirk on an already-loaded label, harmless but noisy, and it
    also meant a rewritten plist (e.g. a changed binary path after an
    upgrade) silently didn't take effect. Now unloads first if already
    loaded.
  • tili uninstall deleted ~/.config/tili/tili.kdl even when it was a
    symlink
    , breaking a dotfiles manager's (stow, chezmoi, ...) managed
    arrangement. Now left in place if it's a symlink.
  • Toggling input sources (Globe/Ctrl-Space) showed the "A" HUD glyph as
    a floating window tili re-centered
    , instead of leaving it alone like
    other transient system UI. Unlike the volume/brightness HUD
    (com.apple.OSDUIHelper, also added to the system-UI bundle-id
    denylist while investigating this), this glyph isn't owned by a
    dedicated system process at all — it's attributed to whichever app is
    frontmost at the moment, confirmed via diagnostic logging. Now matched
    by shape instead: any brand-new Dialog-kind window with an empty title
    is ignored, regardless of which app it's attributed to.
  • Moving a focused floating window to another workspace could move a
    different, unrelated tiled window instead
    , when the floating window
    belonged to a different app than whatever macOS still considered
    frontmost. sync_focus_from_frontmost resolved "what's focused" via
    frontmost_app_pid() (which app is frontmost) then that app's own
    focused window — a two-hop lookup that misses a floating panel/utility
    window that can hold real AX focus without its owning app ever becoming
    NSWorkspace.frontmostApplication. Now queries the system-wide focused
    window directly (AxWindow::system_focused_id), sidestepping the
    "which app" hop entirely.

Full changelog: CHANGELOG.md

v0.4.4

Choose a tag to compare

@github-actions github-actions released this 22 Jul 13:21

Recommended install: brew install itsdezen/tap/tili
Homebrew downloads via curl, which doesn't set macOS's
quarantine flag, so Gatekeeper never enters the picture.

The tarballs below are codesigned with tili's self-signed
certificate (hardened runtime) — see
CONTRIBUTING.md.
That's for a stable signing identity across releases (so
your Accessibility grant survives an upgrade), not for
passing Gatekeeper — a self-signed cert isn't notarized. If
you download tili.app directly from this page instead of
using Homebrew, Gatekeeper will still prompt on first launch:
right-click → Open, or xattr -d com.apple.quarantine tili.app.

Fixed

  • tili uninstall could print a spurious Unload failed: 5: Input/output error from launchctl before its own "uninstalled"
    message.
    stop_daemon called launchctl unload on a LaunchAgent
    plist whenever the file existed on disk, even if launchd no longer had
    that job loaded (e.g. the daemon had already unloaded itself via
    stop_self). Now skips the unload when the job isn't actually loaded,
    still removing the stale plist file either way.
  • A fresh install's first tili start could show no tili item to
    toggle in System Settings' Accessibility list.
    Right after
    triggering the Accessibility prompt, tili-daemon ran tccutil reset
    on its own TCC entry — since the prompt's row is created
    asynchronously by macOS, this reset raced it and deleted the row
    before it was ever visible to toggle, on every single attempt. That
    reset is gone.
  • tili status could report the daemon "running" while hotkeys
    silently didn't work.
    Input Monitoring was treated as a soft
    condition — the daemon kept running in a degraded state if it wasn't
    granted. It's now a hard gate like Accessibility: if Input Monitoring
    isn't granted, the daemon stops itself and requires an explicit tili start once both permissions are granted, instead of half-running.
  • brew install/brew upgrade could skip restarting the menu bar
    badge (or the daemon) after an asymmetric LaunchAgent state.

    post_install's restart gate checked only com.tili.daemon.plist's
    existence before restarting both processes; each LaunchAgent is now
    checked independently.

Full changelog: CHANGELOG.md

v0.4.3

Choose a tag to compare

@github-actions github-actions released this 22 Jul 10:17

Recommended install: brew install itsdezen/tap/tili
Homebrew downloads via curl, which doesn't set macOS's
quarantine flag, so Gatekeeper never enters the picture.

The tarballs below are codesigned with tili's self-signed
certificate (hardened runtime) — see
CONTRIBUTING.md.
That's for a stable signing identity across releases (so
your Accessibility grant survives an upgrade), not for
passing Gatekeeper — a self-signed cert isn't notarized. If
you download tili.app directly from this page instead of
using Homebrew, Gatekeeper will still prompt on first launch:
right-click → Open, or xattr -d com.apple.quarantine tili.app.

Fixed

  • The menu bar badge could show up in System Settings' Menu Bar list as
    tili-menubar with no icon instead of tili with its logo.

    sibling_binary_path derived the badge's LaunchAgent path from
    current_exe()'s parent directory without canonicalizing first — since
    Homebrew's bin/ prefix holds a tili-menubar symlink right next to
    tili's own, an unresolved current_exe() found it there and baked
    that symlink path into the plist instead of the real path inside
    tili.app, which is what actually carries the bundle's name/icon.
    sibling_binary_path now canonicalizes first. Existing installs pick
    this up after tili stop && tili start (or a fresh brew upgrade per
    its caveats).

Full changelog: CHANGELOG.md

v0.4.2

Choose a tag to compare

@github-actions github-actions released this 22 Jul 09:52

Recommended install: brew install itsdezen/tap/tili
Homebrew downloads via curl, which doesn't set macOS's
quarantine flag, so Gatekeeper never enters the picture.

The tarballs below are codesigned with tili's self-signed
certificate (hardened runtime) — see
CONTRIBUTING.md.
That's for a stable signing identity across releases (so
your Accessibility grant survives an upgrade), not for
passing Gatekeeper — a self-signed cert isn't notarized. If
you download tili.app directly from this page instead of
using Homebrew, Gatekeeper will still prompt on first launch:
right-click → Open, or xattr -d com.apple.quarantine tili.app.

Fixed

  • brew install/brew upgrade could report a false "post-install step
    did not complete successfully" warning.
    post_install used system
    to run pkill -x tili-daemon/pkill -x tili-menubar, which treats
    pkill's exit 1 (no matching process — the normal case whenever the
    LaunchAgent plist exists but the process isn't currently running) as a
    build failure. Switched to quiet_system, which doesn't raise on a
    non-zero exit.

Full changelog: CHANGELOG.md

v0.4.1

Choose a tag to compare

@github-actions github-actions released this 22 Jul 04:37

Recommended install: brew install itsdezen/tap/tili
Homebrew downloads via curl, which doesn't set macOS's
quarantine flag, so Gatekeeper never enters the picture.

The tarballs below are codesigned with tili's self-signed
certificate (hardened runtime) — see
CONTRIBUTING.md.
That's for a stable signing identity across releases (so
your Accessibility grant survives an upgrade), not for
passing Gatekeeper — a self-signed cert isn't notarized. If
you download tili.app directly from this page instead of
using Homebrew, Gatekeeper will still prompt on first launch:
right-click → Open, or xattr -d com.apple.quarantine tili.app.

Fixed

  • Preferences/Settings-style windows no longer get tiled. Some apps
    (e.g. Safari's own Settings window) present their preferences panel as
    an ordinary AXStandardWindow, indistinguishable by subrole from a real
    content window, so it fell through to WindowKind::Standard and got
    tiled like any other window. classify_window_kind now also treats a
    standard-subrole window as Dialog (tili's default float/center
    treatment) when it has a zoom button but no full-screen button — AppKit
    gives fullscreen-capable content windows a full-screen button and
    non-fullscreenable utility panels a classic zoom button instead, so this
    generalizes to any app with the same window shape rather than a
    per-bundle-id fix.

Full changelog: CHANGELOG.md

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 17:46

Recommended install: brew install itsdezen/tap/tili
Homebrew downloads via curl, which doesn't set macOS's
quarantine flag, so Gatekeeper never enters the picture.

The tarballs below are codesigned with tili's self-signed
certificate (hardened runtime) — see
CONTRIBUTING.md.
That's for a stable signing identity across releases (so
your Accessibility grant survives an upgrade), not for
passing Gatekeeper — a self-signed cert isn't notarized. If
you download tili.app directly from this page instead of
using Homebrew, Gatekeeper will still prompt on first launch:
right-click → Open, or xattr -d com.apple.quarantine tili.app.

Added

  • Animated window movement (opt-in). New animate setting (#false,
    #true, "medium", or "high" — default #false; #true is
    shorthand for "medium") eases tiled relayout and floating-window
    placement — including a newly-centered floating window's placement —
    into their new frame over a short duration (90ms) instead of jumping
    straight there, fixing the visible "flick" on relayout, post-drag
    mouse-resize snap, and floating centering. "medium"/"high" pick the
    animation's tick rate (~60fps/~120fps) — tili-daemon reconstructs its
    dedicated animation timer to match on every config change, and that
    timer only runs at all while something is actually animating.
    Performance tradeoff: each animation step is a real AX write, not a free
    interpolation, so "medium" costs roughly 6x the AX round-trips of a
    plain instant move per relayout, and "high" roughly 11x (double
    "medium") — negligible on responsive native apps, more noticeable as
    extra per-relayout latency on a slow-to-respond one. Implemented as
    TweenedFrameSetter, a second WindowFrameSetter alongside the
    existing InstantFrameSetter. Doesn't affect a user's own native
    drag/resize of a floating window (tili never writes during that);
    parking a window off-screen and the size-discovery step of centering a
    floating window stay instant on purpose, and so does revealing a
    workspace's windows on switch (they're showing at wherever they were
    parked, which was never meant to be seen, so there's no start point to
    meaningfully ease from).

Full changelog: CHANGELOG.md

v0.3.1

Choose a tag to compare

@github-actions github-actions released this 21 Jul 16:29

Recommended install: brew install itsdezen/tap/tili
Homebrew downloads via curl, which doesn't set macOS's
quarantine flag, so Gatekeeper never enters the picture.

The tarballs below are codesigned with tili's self-signed
certificate (hardened runtime) — see
CONTRIBUTING.md.
That's for a stable signing identity across releases (so
your Accessibility grant survives an upgrade), not for
passing Gatekeeper — a self-signed cert isn't notarized. If
you download tili.app directly from this page instead of
using Homebrew, Gatekeeper will still prompt on first launch:
right-click → Open, or xattr -d com.apple.quarantine tili.app.

Fixed

  • Reopening a single centered floating window no longer drifts
    off-center.
    The cascade nudge added in 0.3.0 (to keep several
    same-sized centered windows from stacking exactly on top of each other)
    used to advance a per-workspace counter on every placement and never
    reset it, so repeatedly opening and closing one floating window — with
    no other centered window ever present — still cycled through the
    cascade sequence on every reopen. The nudge now only applies when
    another centered floating window is actually on screen at the same
    time, so a lone window always reopens dead-center.
  • Siri AI's background panel no longer steals a floating window's
    dead-center placement.
    Its bundle id (com.apple.campo) wasn't
    recognized as system UI, so its transient panel got floated and
    centered like a real app window — landing on the same dead-center spot
    a genuine floating window would otherwise get, and pushing that real
    window into the cascade-offset sequence instead. Added to the same
    ignore-list as the Dock/Spotlight/Notification Center's own transient
    chrome.

Full changelog: CHANGELOG.md