Skip to content

Add for_exec_window command and activation_token criteria#339

Open
jbms wants to merge 1 commit into
dawsers:masterfrom
jbms:for-exec-window
Open

Add for_exec_window command and activation_token criteria#339
jbms wants to merge 1 commit into
dawsers:masterfrom
jbms:for-exec-window

Conversation

@jbms

@jbms jbms commented Jun 26, 2026

Copy link
Copy Markdown
Contributor
  • for_exec_window allows executing a program and applying a list of sway commands to the window created by that program.
    Syntax: for_exec_window ""
    Matches via XDG activation token, PID (fallback), or X11 startup ID (Xwayland).

  • Expose activation_token in window's JSON representation (IPC). Added activation_token to sway_view and serialized it in get_tree.

  • Support for_window [activation_token=...] criteria. This is a one-shot criteria that binds commands to the corresponding launcher_ctx immediately, and cannot be combined with other criteria.

Changes:

  • include/sway/desktop/launcher.h & sway/desktop/launcher.c:
    • Add cmdlist to launcher_ctx.
    • Implement launcher_ctx_find_token to look up ctx by activation token.
    • Fix crash in view_assign_ctx by obtaining token name before consuming.
  • include/sway/tree/view.h & sway/tree/view.c:
    • Add exec_cmdlist and activation_token to sway_view.
    • Populate them in view_assign_ctx.
  • include/sway/criteria.h & sway/criteria.c:
    • Add activation_token to criteria struct and implement parsing/matching/destruction.
  • sway/commands/for_exec_window.c: Implement for_exec_window command.
  • sway/commands/for_window.c: Intercept activation_token criteria and bind to launcher_ctx.
  • sway/commands/exec_always.c & exec.c: Refactor cmd_exec_process to support passing cmdlist.
  • sway/ipc-json.c: Expose activation_token in view JSON.
  • sway/scroll.5.scd: Document new command and criteria, noting that activation_token rules automatically expire.
  • sway/scroll-ipc.7.scd: Document activation_token property in window JSON representation.
  • protocols/meson.build: Add xdg-activation-v1 protocol.
  • tests/clients/wayland-client.c: Implement XDG activation support in test client.
  • tests/clients/x11-client.c: Implement _NET_STARTUP_ID support.
  • tests/test_for_exec_window.py: Add tests for for_exec_window (wait for Xserver using socket check).
  • tests/test_activation_token.py: Add tests for activation_token criteria (avoiding sleep by launching client from python) and IPC exposure, and verification of invalid criteria combinations.

@jbms

jbms commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

This is related to this old feature request for sway: swaywm/sway#4045

@jbms jbms force-pushed the for-exec-window branch from 92ae196 to 1555569 Compare June 26, 2026 07:05
- Implement `for_exec_window <commands> <command_to_exec>` to run
  commands on the next window opened by the executed process.
- Add `activation_token` criteria to match windows by their XDG
  activation token.
- Implement `IPC_MINT_ACTIVATION_TOKEN` (124) IPC command to allow
  clients to obtain a freshly generated XDG activation token.
- Expose `activation_token` in the window JSON representation
  (get_tree).
- Add documentation for the new IPC command in `scroll-ipc.7.scd`
  and `README.md`.
- Add tests for `for_exec_window` (including PID, XDG activation,
  and X11 startup ID matching) and `IPC_MINT_ACTIVATION_TOKEN`.
@jbms jbms force-pushed the for-exec-window branch from 1555569 to 819f013 Compare June 26, 2026 07:08
@dawsers

dawsers commented Jun 26, 2026

Copy link
Copy Markdown
Owner

I prefer to use Lua for these things instead of cluttering the code more. The Lua environment is richer and more powerful than some parsed config rule.

In general for_window and workspace rules should use Lua long term. I even removed some rudimentary workspace rule I implemented in the beginning when I added the Lua API.

The view_map callback should work for this.

local scroll = require("scroll")

local function on_create(view, data)
  if scroll.view_get_app_id(view) == "kitty" then
    local container = scroll.view_get_container(view)
    if container then
      scroll.container_set_focus(container)
      scroll.command(scroll.view_get_container(view), "set_size h 0.2")
      scroll.command(scroll.view_get_container(view), "align left")
    end
  end
end

scroll.add_callback("view_map", on_create, nil)

If you think something is missing from the Lua API to implement this feature, maybe it would be better to add it there. Currently, there are functions to retrieve information about the view and application running in it, maybe something else is missing there. Running commands is already supported.

@jbms

jbms commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

The key thing this adds is the ability to match based on an xdg activation token, which was not previously exposed either via IPC or LUA, in order to say: launch a new window, and perform certain actions on just that exact window associated with my launch action. I can certainly add a LUA API for getting the activation token of a window (which this PR also exposes via get_tree now) and for minting a new activation token (which this PR also exposes as a new IPC request), but I think the added for_window[activation_token="..."] and for_exec_window commands are quite ergonomic. Maybe sway would also upstream them.

@dawsers

dawsers commented Jun 27, 2026

Copy link
Copy Markdown
Owner

You can already do what for_exec_window does. For example:

for_exec_window.lua "exec firefox" "floating enable"

local args, state = ...

local scroll = require("scroll")

local id_cmd = nil
local id_app = nil

local function on_view_map(view, data)
  if id_app then
    scroll.remove_callback(id_app)
  end
  if #args > 1 then
    local container = scroll.view_get_container(view)
    scroll.command(container, args[2])
  end
end

local function on_command_end(cmd_data, data)
  if id_cmd then
    scroll.remove_callback(id_cmd)
  end
  id_app = scroll.add_callback("view_map", on_view_map, nil)
end

if #args > 0 then
  id_cmd = scroll.add_callback("command_end", on_command_end, nil)
  scroll.command(nil, args[1])
end

and with the command_end callback and lua_command_data_create() you can do many more things.

@jbms

jbms commented Jun 27, 2026

Copy link
Copy Markdown
Contributor Author

The problem with this approach of just matching the next window to map is that if there are multiple windows opening concurrently you might match the wrong one. For my specific use case of the session manager I want to be able to restore multiple windows at the same time in some cases (e.g. after restart) and also be robust to the user opening windows manually at the same time. I also can't rely on app_id because there may be multiple windows with the same app_id, e.g. multiple terminals or multiple browser windows.

There are other heuristics that can be used for matching, like title, but activation token provides the most robust and easiest way by far.

@dawsers

dawsers commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Sure, what I meant is we could add Lua functions to retrieve the view's launch context activation token, or whatever is necessary. There is already one to get the view's parent PID too.

My main idea when I added Lua was to get rid of IPC as much as possible, because it is usually abused, slow, and needs a lot of parsing of the content tree, which is fragile and prone to errors and questions by users.

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.

2 participants