Skip to content

Add lua_eval command, lua IPC command, and scroll.context_container API - #347

Open
jbms wants to merge 5 commits into
dawsers:masterfrom
jbms:lua-eval-and-ipc
Open

Add lua_eval command, lua IPC command, and scroll.context_container API#347
jbms wants to merge 5 commits into
dawsers:masterfrom
jbms:lua-eval-and-ipc

Conversation

@jbms

@jbms jbms commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

lua_eval allows inline lua to be evaluated without needing to write it to a file.

Compared to just running lua or lua_eval via RUN_COMMAND, the new LUA_EXEC command allows a return value to be sent back to the client.

The scroll.context_container() API provides access to the context container when lua or lua_eval is run with criteria.

@jbms jbms changed the title Add lua_eval command and lua IPC command Add lua_eval command, lua IPC command, and scroll.context_container API Jun 30, 2026
@jbms
jbms force-pushed the lua-eval-and-ipc branch 4 times, most recently from 1761821 to 4c03191 Compare July 1, 2026 21:45
@jbms
jbms force-pushed the lua-eval-and-ipc branch 6 times, most recently from 5360edb to 157abe0 Compare July 9, 2026 21:59
jbms added 5 commits July 10, 2026 09:08
Exposes scroll.animating() and scroll.pending_transactions() to Lua
to allow external scripts and tests to query whether the compositor
has settled.

Also includes testing infrastructure updates (pytest.ini config, LSan
suppressions, and helper methods in test_utils.py).
When a new view is mapped, criteria rules may configure its
geometry (e.g. `resize set`). This configuration sends a new
configure event to the client.

However, during the map event handling, we are still processing
the initial commit from the client. After the map event is
handled and the rule-configured geometry is applied via a
transaction, the compositor's commit handler (`handle_commit`)
continues.

For floating containers, `handle_commit` resizes the container to
match the client's committed buffer size. If it processes the
initial commit (which has the initial client size, not the
configured one), it overrides the configured geometry back to the
initial size.

To fix this, we introduce `is_commit_stale` helper which checks
if there is a pending or scheduled configure event with a newer
serial than the serial acknowledged by the current commit. If the
commit is stale, we ignore its size for resizing floating
containers, waiting instead for the client to acknowledge and
commit the newly configured size.

How to reproduce:
1. Register the for_window rule:
   swaymsg 'for_window [title="Scratchpad Test"] move scratchpad, resize set 500 500'
2. Start the client:
   foot -T "Scratchpad Test"
3. Query the scratchpad window geometry:
   swaymsg -t get_tree | jq '.. | select(.name? == "__i3_scratch") | .floating_nodes[] | select(.name == "Scratchpad Test") | .rect'

   On buggy versions, this will output the client's default size
   instead of the configured 500x500.

Additionally:
*   Make `wait_for_client_map` test utility robust by searching
    all workspaces and the scratchpad for the mapped view, instead
    of just checking the focused view. This allows it to be used
    for windows that are moved to the scratchpad (and thus
    hidden/unfocused) immediately upon mapping.
*   Update `wayland-client.c` to support dynamic resizing in tests.
*   Add integration test `tests/test_scratchpad_geometry.py` to
    verify the fix.
Add a new `lua_eval` compositor command to allow executing a raw inline
Lua string as code rather than loading a file. Any optional arguments
passed are supplied to the inline Lua block as parameters (accessed
via `...`). The empty string is used as the script identifier.

Also update the `scroll` manual page and README.md to document the
new command.
Add a new `lua` IPC command type (represented by `IPC_LUA_EXEC` = 124)
to support evaluating either a Lua script file or inline Lua code.
The message accepts arguments as a list of JSON values which are parsed
and converted to Lua values, and serializes the return value(s) of the
script back to JSON before replying.

In the `scrollmsg` client, expose this through separate `lua` (for
running a file) and `lua_eval` (for running inline code) command
types.

Also:
- Export and reuse `sway_lua_value_to_json` and
  `sway_lua_table_to_json` from `sway/lua.c` for Lua-to-JSON
  serialization in the IPC reply.
- Export `sway_lua_push_json_to_lua` for JSON-to-Lua argument
  conversion.
- Fix relative stack index handling in JSON/Lua conversion functions to
  ensure they function correctly with negative stack indices.
- Commit dirty transactions immediately after the script execution on
  both success and error execution paths.
- Rewrite the `execute_lua` test helper to use the new IPC API directly
  (removing temporary files and the custom Lua runner).
- Update man pages (scrollmsg.1, scroll-ipc.7) and README.md.
Exposes the criteria-matched container context to running Lua scripts
via a new `scroll.context_container()` function.

If a script is run via criteria (e.g., `[class="XTerm"] lua_eval ...`),
the function returns the matched container's ID. If executed globally
without criteria, it returns `nil`.

To prevent nested executions of `scroll.command()` within a script
from permanently overwriting the context, the active Lua context is
backed up on entry to Lua command handlers (`lua` and `lua_eval`) and
restored on exit.

Also documents the new API in `scroll.lua` and the `scroll.5` man
page, and adds comprehensive pytest coverage.
@jbms
jbms force-pushed the lua-eval-and-ipc branch from 157abe0 to c91406b Compare July 10, 2026 16:14
@dawsers

dawsers commented Jul 27, 2026

Copy link
Copy Markdown
Owner

In a194801 I added a Lua REPL to scrollmsg, and an IPC command, LUA_EVAL. I don't know how it will affect this, and iwhetherit is enough for what you are trying to do.

@jbms

jbms commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

It is enough for what I need for the session manager program. However I do have a few comments about that commit:

  • A linux pipe has a fixed-size buffer and writes to it will hang once that buffer is filled. Therefore if too much gets written to redirected stdout, the compositor will hang. To solve, I think there are a few options: (1) have another thread reading from the pipe (kind of unfortunate to introduce another thread just for this purpose) (2) implement the redirection using lua APIs rather than actually redirecting stdout (3) set the pipe to non-blocking mode so that writes will fail if the buffer gets full --- it would be good to check how lua handles this error. You could also call fcntl F_SETPIPE_SZ to increase the buffer size in combination with that.
  • It would be nice to avoid having to duplicate the names of all of the lua functions again for use by the repl --- maybe it can instead be queried using the lua API?
  • It would still be nice to provide a lua_eval command so that you can e.g. bind a key to run a lua expression directly without needing a separate file. In particular this would allow defining a lua function as a global variable and then calling it from a key binding rather than having to load it from a file for each invocation.
  • Although not necessary for my session manager, since it uses the IPC API directly rather than via scrollmsg, it would be nice to expose lua_eval as a regular IPC command in addition to the lua repl.
  • Although not strictly necessary it might be nice to allow arguments to be passed to the lua code as JSON values, since that would be consistent with the return value being returned as JSON and avoid the need to implement "encode json-like value as a lua expression" in whatever language is being used with the IPC API. json encoding is much more readily available than lua literal encoding.

If you are open to any of these changes I'm happy to update this PR to make those changes on top of current scroll HEAD.

@dawsers

dawsers commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Thank you for your comments, they are all very insightful.

A linux pipe has a fixed-size buffer and writes to it will hang once that buffer is filled.

I should have been more thorough instead of supposing "nobody" would print more than 64 KB to stdout. In 7bca823 I set the reader in a different thread. It now supports any output size and also makes the code is cleaner.

It would be nice to avoid having to duplicate the names of all of the lua functions again for use by the repl --- maybe it can instead be queried using the lua API?

I also wanted to avoid this. The problem is scrollmsg is a different program and it doesn't have access to sway's library. The quick and dirty solution was what I did. An even dirtier one would have been to create a common lua.h header and put all the function names there, but note that currently, the Lua registry for scroll lib is not visible, it is a static array in sway/lua.c. So until there is a bigger refactor, I'd rather leave it as it is. The worst that can happen is I forget to add some function to the auto-complete for a while, but it is just a tool to help, not something critical.

It would still be nice to provide a lua_eval command so that you can e.g. bind a key to run a lua expression directly without needing a separate file. In particular this would allow defining a lua function as a global variable and then calling it from a key binding rather than having to load it from a file for each invocation.

I could do that, I just think the config file would become a mess if people abuse this, and then file issues here because "something doesn't work", leaving me to debug huge config files that also include Lua code. Scripts also allow using arguments and the state, so this facility would be a bit limited, probably only useful to run a couple of commands that can be run without Lua anyway. If you have something specific and useful in mind, please share.

Although not necessary for my session manager, since it uses the IPC API directly rather than via scrollmsg, it would be nice to expose lua_eval as a regular IPC command in addition to the lua repl.

I added this in cf9c88d. Now you can do `scrollmsg -t lua_eval 'print("Hello")'.

lthough not strictly necessary it might be nice to allow arguments to be passed to the lua code as JSON values, since that would be consistent with the return value being returned as JSON and avoid the need to implement "encode json-like value as a lua expression" in whatever language is being used with the IPC API. json encoding is much more readily available than lua literal encoding.

What do you mean? For scripts? Or you want to share data from Python? How would someone use this so I understand better?

@jbms

jbms commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Thank you for your comments, they are all very insightful.

A linux pipe has a fixed-size buffer and writes to it will hang once that buffer is filled.

I should have been more thorough instead of supposing "nobody" would print more than 64 KB to stdout. In 7bca823 I set the reader in a different thread. It now supports any output size and also makes the code is cleaner.

It would be nice to avoid having to duplicate the names of all of the lua functions again for use by the repl --- maybe it can instead be queried using the lua API?

I also wanted to avoid this. The problem is scrollmsg is a different program and it doesn't have access to sway's library. The quick and dirty solution was what I did. An even dirtier one would have been to create a common lua.h header and put all the function names there, but note that currently, the Lua registry for scroll lib is not visible, it is a static array in sway/lua.c. So until there is a bigger refactor, I'd rather leave it as it is. The worst that can happen is I forget to add some function to the auto-complete for a while, but it is just a tool to help, not something critical.

Maybe the repl can just use the LUA_EVAL command on startup or on first completion request to obtain the members.

It would still be nice to provide a lua_eval command so that you can e.g. bind a key to run a lua expression directly without needing a separate file. In particular this would allow defining a lua function as a global variable and then calling it from a key binding rather than having to load it from a file for each invocation.

I could do that, I just think the config file would become a mess if people abuse this, and then file issues here because "something doesn't work", leaving me to debug huge config files that also include Lua code. Scripts also allow using arguments and the state, so this facility would be a bit limited, probably only useful to run a couple of commands that can be run without Lua anyway. If you have something specific and useful in mind, please share.

In general it just makes it easier to do things without having to write extra files. Having the indirection of a file just to invoke some lua function makes it more complicated, in my opinion, and forcing the code to go through a separate file rather than inline doesn't reduce the complexity, it just adds an extra indirection.

One example is that you could define a global function in a file (or inline lua code) that gets evaluated, and then define a key binding that just calls the function. Without this an external shim file is needed to do anything.

By the way I was not clear on the purpose of the state mechanism in the first place. You can anyway just access lua globals or use the _G table, which also allows sharing state between multiple scripts and with lua_eval. Your state mechanism provides a separate namespace for each lua file, but just using a suitable prefix for any global variables would also seem to be sufficient. I also wasn't clear why state isn't just exposed directly as a lua table.

Although not necessary for my session manager, since it uses the IPC API directly rather than via scrollmsg, it would be nice to expose lua_eval as a regular IPC command in addition to the lua repl.

I added this in cf9c88d. Now you can do `scrollmsg -t lua_eval 'print("Hello")'.

lthough not strictly necessary it might be nice to allow arguments to be passed to the lua code as JSON values, since that would be consistent with the return value being returned as JSON and avoid the need to implement "encode json-like value as a lua expression" in whatever language is being used with the IPC API. json encoding is much more readily available than lua literal encoding.

What do you mean? For scripts? Or you want to share data from Python? How would someone use this so I understand better?

For my session manager I want to use lua_eval to restore one or more windows as an atomic operation (similar to the spaces restore), which means I need to pass certain information about the ids and target position of the set of windows that will be affected. Being able to just encode this as JSON and then have scroll translate to lua is convenient so that I don't have to either add a json parser to lua, or a lua encoder to my program. I think in general it would be convenient for users to be able to pass structured data directly to their lua code without having to encode it as a lua expression.

@dawsers

dawsers commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Maybe the repl can just use the LUA_EVAL command on startup or on first completion request to obtain the members.

Starting from 69e6e29, the scroll completions table is generated dynamically.

@dawsers

dawsers commented Jul 28, 2026

Copy link
Copy Markdown
Owner

For my session manager I want to use lua_eval to restore one or more windows as an atomic operation (similar to the spaces restore), which means I need to pass certain information about the ids and target position of the set of windows that will be affected. Being able to just encode this as JSON and then have scroll translate to lua is convenient so that I don't have to either add a json parser to lua, or a lua encoder to my program. I think in general it would be convenient for users to be able to pass structured data directly to their lua code without having to encode it as a lua expression.

Would this be covered if I added a function scroll.json_parse(string) that returns a Lua table after parsing the JSON data in string?

@jbms

jbms commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Yeah I suppose it would. Then I could encode it as json and then encode again as a json string, which I think would also be a valid lua string. Still given that the return value is encoded as json automatically it makes sense to me to offer a way to provide json values as input.

@dawsers

dawsers commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Yeah I suppose it would. Then I could encode it as json and then encode again as a json string, which I think would also be a valid lua string. Still given that the return value is encoded as json automatically it makes sense to me to offer a way to provide json values as input.

I think I don't understand what you mean. I think you are saying, optimally you would want to input non-encoded JSON? How would you do that? I mean, what kind of interface would there be? JSON is portable in string format, so to be able to interface with other languages you need to convert from the library's internal json binary format to a JSON string, don't you? That is why I proposed local table = scroll.json_parse(json_data_in_string_format), to convert a JSON object in string format to a Lua table. Maybe you are thinking of something different, please explain.

@dawsers

dawsers commented Jul 29, 2026

Copy link
Copy Markdown
Owner

I am OK with the changes to sway/commands/lua.c, including cmd_lua_eval() if you reaaally want to have a way to call Lua from the config file. But lua_eval needs to assign a name to the "fake" script it creates, so there are no problems with the local state for the script. Currently, you assign "" as the name, so a second script will use the state of the previous one. The command would then be something like:

bindsym $mod+q lua_eval fake_name 'local args, state = ...\n for i = 1, 2, 1 do scroll.command(nil, args[1]) end' 'cycle_size h next'

@jbms

jbms commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Yeah I suppose it would. Then I could encode it as json and then encode again as a json string, which I think would also be a valid lua string. Still given that the return value is encoded as json automatically it makes sense to me to offer a way to provide json values as input.

I think I don't understand what you mean. I think you are saying, optimally you would want to input non-encoded JSON? How would you do that? I mean, what kind of interface would there be? JSON is portable in string format, so to be able to interface with other languages you need to convert from the library's internal json binary format to a JSON string, don't you? That is why I proposed local table = scroll.json_parse(json_data_in_string_format), to convert a JSON object in string format to a Lua table. Maybe you are thinking of something different, please explain.

Most of the other IPC commands use json for the input, so in my PR I used json for the lua ipc command and that naturally provided an easy way to specify additional arguments as json. Indeed they still need to be encoded as json though.

Either way I think it would be useful to provide json encode and decode functions in the lua api for user convenience if they exist in the c source code anyway.

@jbms

jbms commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

I am OK with the changes to sway/commands/lua.c, including cmd_lua_eval() if you reaaally want to have a way to call Lua from the config file. But lua_eval needs to assign a name to the "fake" script it creates, so there are no problems with the local state for the script. Currently, you assign "" as the name, so a second script will use the state of the previous one. The command would then be something like:

bindsym $mod+q lua_eval fake_name 'local args, state = ...\n for i = 1, 2, 1 do scroll.command(nil, args[1]) end' 'cycle_size h next'

Maybe lua_eval just shouldn't get a state at all. Since users can just store state in global variables, and define global functions that reference file-local variables and then call those global functions from lua_eval, there are already a lot of options for state management that don't require the scroll state mechanism.

@dawsers

dawsers commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Yeah I suppose it would. Then I could encode it as json and then encode again as a json string, which I think would also be a valid lua string. Still given that the return value is encoded as json automatically it makes sense to me to offer a way to provide json values as input.

I think I don't understand what you mean. I think you are saying, optimally you would want to input non-encoded JSON? How would you do that? I mean, what kind of interface would there be? JSON is portable in string format, so to be able to interface with other languages you need to convert from the library's internal json binary format to a JSON string, don't you? That is why I proposed local table = scroll.json_parse(json_data_in_string_format), to convert a JSON object in string format to a Lua table. Maybe you are thinking of something different, please explain.

Most of the other IPC commands use json for the input, so in my PR I used json for the lua ipc command and that naturally provided an easy way to specify additional arguments as json. Indeed they still need to be encoded as json though.

Either way I think it would be useful to provide json encode and decode functions in the lua api for user convenience if they exist in the c source code anyway.

My question was about how someone can use those functions from outside of scroll itself. I can provide them, but nobody can actually input "binary" JSON from a config file, scrollmsg, the IPC interface or a Lua script, they can only use standard JSON in text format.

In your code you use the functions in IPC_LUA_EXEC, to pass arguments that have previously been passed in text JSON format, parsed into a json_object, and then sent to Lua converting them again into a Lua table. That is no different than choosing for a script what format of arguments you want to use for the script, and if it is JSON, use an internal parser, local lua_table = scroll.json_parse(json_txt_formatted), and if it is not, there is no need to do all that parsing. That is what I meant.

@dawsers

dawsers commented Jul 29, 2026

Copy link
Copy Markdown
Owner

I am OK with the changes to sway/commands/lua.c, including cmd_lua_eval() if you reaaally want to have a way to call Lua from the config file. But lua_eval needs to assign a name to the "fake" script it creates, so there are no problems with the local state for the script. Currently, you assign "" as the name, so a second script will use the state of the previous one. The command would then be something like:

bindsym $mod+q lua_eval fake_name 'local args, state = ...\n for i = 1, 2, 1 do scroll.command(nil, args[1]) end' 'cycle_size h next'

Maybe lua_eval just shouldn't get a state at all. Since users can just store state in global variables, and define global functions that reference file-local variables and then call those global functions from lua_eval, there are already a lot of options for state management that don't require the scroll state mechanism.

Sure, but currently there is a state, and that is useful for people who don't want to have to deal with global stuff. If they want to, they can, but it not, scroll already provides a state that allows them to use "script-local" memory that stays there between runs. Being able to use the same scripts and be compatible with standard scroll scripts should be a goal when it is so easy to attain.

@jbms

jbms commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

I am OK with the changes to sway/commands/lua.c, including cmd_lua_eval() if you reaaally want to have a way to call Lua from the config file. But lua_eval needs to assign a name to the "fake" script it creates, so there are no problems with the local state for the script. Currently, you assign "" as the name, so a second script will use the state of the previous one. The command would then be something like:

bindsym $mod+q lua_eval fake_name 'local args, state = ...\n for i = 1, 2, 1 do scroll.command(nil, args[1]) end' 'cycle_size h next'

Maybe lua_eval just shouldn't get a state at all. Since users can just store state in global variables, and define global functions that reference file-local variables and then call those global functions from lua_eval, there are already a lot of options for state management that don't require the scroll state mechanism.

Sure, but currently there is a state, and that is useful for people who don't want to have to deal with global stuff. If they want to, they can, but it not, scroll already provides a state that allows them to use "script-local" memory that stays there between runs. Being able to use the same scripts and be compatible with standard scroll scripts should be a goal when it is so easy to attain.

In my PR I just hard coded it to use the empty string as the state identifier because it is guaranteed not to conflict with any filename and because it seemed awkward to force users to specify a state identifier on every use even if they aren't actually using the state. The other issue with the state is that it isn't accessible via the lua_eval IPC command.

@dawsers

dawsers commented Jul 29, 2026

Copy link
Copy Markdown
Owner

I am OK with the changes to sway/commands/lua.c, including cmd_lua_eval() if you reaaally want to have a way to call Lua from the config file. But lua_eval needs to assign a name to the "fake" script it creates, so there are no problems with the local state for the script. Currently, you assign "" as the name, so a second script will use the state of the previous one. The command would then be something like:

bindsym $mod+q lua_eval fake_name 'local args, state = ...\n for i = 1, 2, 1 do scroll.command(nil, args[1]) end' 'cycle_size h next'

Maybe lua_eval just shouldn't get a state at all. Since users can just store state in global variables, and define global functions that reference file-local variables and then call those global functions from lua_eval, there are already a lot of options for state management that don't require the scroll state mechanism.

Sure, but currently there is a state, and that is useful for people who don't want to have to deal with global stuff. If they want to, they can, but it not, scroll already provides a state that allows them to use "script-local" memory that stays there between runs. Being able to use the same scripts and be compatible with standard scroll scripts should be a goal when it is so easy to attain.

In my PR I just hard coded it to use the empty string as the state identifier because it is guaranteed not to conflict with any filename and because it seemed awkward to force users to specify a state identifier on every use even if they aren't actually using the state. The other issue with the state is that it isn't accessible via the lua_eval IPC command.

Yes, but by using the empty string, you create a sway_lua_script with name "", and if then there is another call from another lua_eval for a different function/command/script, it will use the same state as the previous script.

The users wouldn't be specifying a "state identifier", they would be simply setting a name for the script, like any command or file would have one. It can be thought as something related to what the script does. And this way there is no state name clash.

Edit: I forgot to address this.

The Lua eval IPC command is to be able to interact with scroll in real-time, like the REPL, there is no need to arguments or state, everything goes in that particular interaction. That is why we are adding the lua_eval command, so there is a way to do things like in standard scroll, with state etc. And it can be called from scrollmsg too, like any other command. so we will have

scrollmsg -t lua_eval (IPC)

scrollmsg lua_eval ... (command).

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