Replies: 33 comments 76 replies
-
|
if you wanted to focus on every window that gets the urgent attribute, you could call this script from your config file: local function on_urgent(view, _)
local container = scroll.view_get_container(view)
local workspace = scroll.container_get_workspace(container)
scroll.command(nil, "workspace " .. scroll.workspace_get_name(workspace))
scroll.command(container, "focus")
end
scroll.add_callback("view_urgent", on_urgent, nil)Edit 2025-12-23: With the new API functions this can be simplified to: local function on_urgent(view, _)
local container = scroll.view_get_container(view)
scroll.container_set_focus(container)
end
scroll.add_callback("view_urgent", on_urgent, nil)If the current focused window is a neovim instance, when called, this script resizes neovim's window height to 0.66667, and opens a kitty terminal under it with height 0.33333. When you close the neovim window, the terminal is also automatically closed. local id_map
local id_unmap
local data = {}
local on_create = function (cbview, cbdata)
if scroll.view_get_app_id(cbview) == "kitty" then
cbdata.view = cbview
scroll.command(nil, "set_size v 0.33333333; move left nomode")
end
scroll.remove_callback(id_map)
end
local on_destroy = function (cbview, cbdata)
if scroll.view_get_pid(cbview) == cbdata.pid then
scroll.view_close(cbdata.view)
end
scroll.remove_callback(id_unmap)
end
local view = scroll.focused_view()
data.pid = scroll.view_get_pid(view)
id_map = scroll.add_callback("view_map", on_create, data)
id_unmap = scroll.add_callback("view_unmap", on_destroy, data)
if view then
if string.find(scroll.view_get_title(view), "^nvim") then
scroll.command(nil, 'set_size v 0.66666667; exec kitty')
end
endSelect and move every tiling kitty terminal from the focused workspace to workspace number 2. local workspace = scroll.focused_workspace()
local containers = scroll.workspace_get_tiling(workspace)
for _, container in ipairs(containers) do
local views = scroll.container_get_views(container)
for _, view in ipairs(views) do
local app_id = scroll.view_get_app_id(view)
if app_id == "kitty" then
local con = scroll.view_get_container(view)
scroll.command(con, "selection toggle")
end
end
end
scroll.command(nil, "workspace number 2; selection move") |
Beta Was this translation helpful? Give feedback.
-
|
Another example navigating the containers of a workspace. Toggle a master-type window layout using a key binding local args, state = ...
local function flatten(workspace)
local containers = scroll.workspace_get_tiling(workspace)
if #containers == 0 then
return false
end
local focused = scroll.focused_view()
for _, container in ipairs(containers) do
local views = scroll.container_get_views(container)
if #views > 1 then
for i = #views, 1, -1 do
if i > 1 then
local con = scroll.view_get_container(views[i])
scroll.command(con, "move right nomode")
end
end
end
end
local con_id = scroll.container_get_id(scroll.view_get_container(focused))
scroll.command(nil, "[con_id=" .. con_id .. "] focus")
return true
end
local function create_master(workspace)
-- Save the current configuration
scroll.command(nil, "space save " .. "master_" .. scroll.workspace_get_name(workspace["object"]))
-- Flatten
if flatten(workspace["object"]) == false then
return
end
scroll.command(nil, "move beginning")
-- Now create master layout
local mode = scroll.workspace_get_mode(workspace["object"])
scroll.workspace_set_mode(workspace, { insert = "before" })
local containers = scroll.workspace_get_tiling(workspace["object"])
for i = #containers, 2, -1 do
local container = containers[i]
local views = scroll.container_get_views(container)
for _, view in ipairs(views) do
local con = scroll.view_get_container(view)
scroll.command(con, "set_size h 0.5; move right nomode")
end
end
scroll.command(nil, "focus end; fit_size v all equal")
containers = scroll.workspace_get_tiling(workspace["object"])
local views = scroll.container_get_views(containers[1])
-- master container has only one view
scroll.command(scroll.view_get_container(views[1]), "set_size h 0.5; set_size v 1.0; focus")
scroll.workspace_set_mode(workspace["object"], { insert = mode.insert })
end
local function set_full(full)
if full then
scroll.command(nil, "fullscreen_application enable; fullscreen")
else
scroll.command(nil, "fullscreen_application reset; fullscreen")
end
end
local function master_swap(workspace)
if not workspace["master"] then
return
end
local containers = scroll.workspace_get_tiling(workspace["object"])
if #containers ~= 2 then
return
end
local views = scroll.container_get_views(containers[1])
-- The master container should have one view, if not, return
if #views ~= 1 then
return
end
local container = scroll.view_get_container(views[1])
local id = scroll.container_get_id(container)
scroll.command(nil, "swap container with con_id " .. id)
end
local function create_scrolling(workspace)
local view = scroll.focused_view()
scroll.command(nil, "space load " .. "master_" .. scroll.workspace_get_name(workspace["object"]))
local con_id = scroll.container_get_id(scroll.view_get_container(view))
scroll.command(nil, "[con_id=" .. con_id .. "] focus")
end
-- Set up workspace table
local workspaces = scroll.state_get_value(state, "workspaces")
if workspaces == nil then
scroll.state_set_value(state, "workspaces", {})
workspaces = scroll.state_get_value(state, "workspaces")
end
local focused_workspace = scroll.focused_workspace()
local workspace_name = scroll.workspace_get_name(focused_workspace)
if workspaces[workspace_name] == nil then
local ws = { object = focused_workspace, master = false, full = false }
workspaces[workspace_name] = ws
end
local workspace = workspaces[workspace_name]
if args[1] == 'toggle' then
workspace["master"] = not workspace["master"]
if workspace["master"] then
create_master(workspace)
else
create_scrolling(workspace)
end
elseif args[1] == 'full' then
workspace["full"] = not workspace["full"]
set_full(workspace["full"])
elseif args[1] == 'swap' then
master_swap(workspace)
end |
Beta Was this translation helpful? Give feedback.
-
|
This script is en example implementation of When you start mpv from a kitty terminal, the terminal window will be swallowed to the scratchpad. When you close the mpv window, the terminal will appear again. The script could be added to
local function candidate(view)
local app_id = scroll.view_get_app_id(view)
if app_id == "mpv" then
local pview = scroll.view_get_parent_view(view)
if pview ~= nil and pview ~= view then
local papp_id = scroll.view_get_app_id(pview)
if papp_id == "kitty" then
return scroll.view_get_container(pview)
end
end
end
return nil
end
local function on_create(view, _)
local parent = candidate(view)
if parent ~= nil then
scroll.command(parent, "move scratchpad")
end
end
local function on_destroy(view, _)
local parent = candidate(view)
if parent ~= nil then
scroll.command(nil, "scratchpad show; floating toggle")
end
end
scroll.add_callback("view_map", on_create, nil)
scroll.add_callback("view_unmap", on_destroy, nil)
|
Beta Was this translation helpful? Give feedback.
-
CallbacksSet a callback on The This script can be used as a workspace rule. Every time a workspace is created, you can do things. You can add this to your configuration: local map_id
local function on_create_view(view, _)
local container = scroll.view_get_container(view)
scroll.command(container, "set_size v 0.5")
scroll.remove_callback(map_id)
end
local function on_create_ws(workspace, _)
local name = scroll.workspace_get_name(workspace)
if name == "9" then
map_id = scroll.add_callback("view_map", on_create_view, nil)
scroll.command(workspace, "exec kitty")
end
end
scroll.add_callback("workspace_create", on_create_ws, nil) |
Beta Was this translation helpful? Give feedback.
-
Maximize ToggleThis script can be used to toggle the geometry of a window between its state and a fully maximized one Add a key binding to your configuration, like for example: And save this script to
local args, state = ...
-- Set up views table
local views = scroll.state_get_value(state, "views")
if views == nil then
scroll.state_set_value(state, "views", {})
views = scroll.state_get_value(state, "views")
end
local function find_view(view)
for _, v in ipairs(views) do
if v["object"] == view then
return v
end
end
return nil
end
if args[1] == 'toggle' then
local focused_view = scroll.focused_view()
local view = find_view(focused_view)
if view == nil then
view = {
object = focused_view,
maximized = false,
wf = 0,
hf = 0
}
table.insert(views, view)
end
view["maximized"] = not view["maximized"]
if view["maximized"] then
local container = scroll.view_get_container(focused_view)
view["wf"] = scroll.container_get_width_fraction(container)
view["hf"] = scroll.container_get_height_fraction(container)
scroll.command(nil, "set_size h 1.0")
scroll.command(nil, "set_size v 1.0")
else
scroll.command(nil, "set_size h " .. view["wf"])
scroll.command(nil, "set_size v " .. view["hf"])
end
end |
Beta Was this translation helpful? Give feedback.
-
|
Hi, is there a way to get lsp autocompletions, like we can get via setting the |
Beta Was this translation helpful? Give feedback.
-
|
You should get function auto-completion by simply adding local args, state = ...
local scroll = require("scroll")
-- Now you can write scroll.work and press your auto-completion key binding to get a list of functions in the scroll module that begin with `work`. |
Beta Was this translation helpful? Give feedback.
-
Yeah, this works, but how can I get completions for core api-functions? this only provides auto-completion for functions that are defined inside the file. It's not that viable, if that would be too much work, though. It just would be a nice addition to have so we don't have to read the manpages to know about the api functions. |
Beta Was this translation helpful? Give feedback.
-
|
Yes, sorry, I don't know how to do that. I think you need to use LuaJIT and FFI to be able to have that functionality, but I am using regular Lua. |
Beta Was this translation helpful? Give feedback.
-
Conditionally Centering ContainersThis script centers containers whose width is wider than half of the viewport. Run it from your config so the callback is always active.
local args, state = ...
local scroll = require("scroll")
local width = tonumber(args[1])
if not width then
width = 0.5
end
local function on_focus(view, _)
local workspace = scroll.focused_workspace()
local container = scroll.view_get_container(view)
local parent = scroll.container_get_parent(container)
if parent then
container = parent
end
if scroll.container_get_width_fraction(container) <= width then
scroll.workspace_set_mode(workspace, { center_horizontal = false })
else
scroll.workspace_set_mode(workspace, { center_horizontal = true })
end
end
scroll.add_callback("view_focus", on_focus, nil) |
Beta Was this translation helpful? Give feedback.
-
|
Let me see if I understood correctly before making changes: You want to center the container only if no other adjacent container fits completely in the viewport. |
Beta Was this translation helpful? Give feedback.
-
Niri's
|
Beta Was this translation helpful? Give feedback.
-
|
Firstly, I want to say thank you for the Lua API! It's really useful, I was able to make scratchpads work in the way I'm used to because of it. Very nice. Just one question, for the maximize script, how would one extend it so that it automatically maximizes windows when there is only one window on a workspace, and un-maximizes it when another window gets created/moved into it? I don't see a way to check how many containers/views are on a given workspace. |
Beta Was this translation helpful? Give feedback.
-
|
The Lua API is really great! I had one thought for something that might get included in future revisions. I was trying to implement a script to replicate the way niri cycles size. In niri, there is one cycle size command that increases width or height until the maximum size specified by the user; on the next invocation, the size cycles back to the smallest size specified. I have a script that does this, but it's very hacky because, as far as I can tell, there's no way to access things like In any case, scroll is fantastic and I'm really enjoying using it. Thank you for all your work on this! |
Beta Was this translation helpful? Give feedback.
-
Automatic Align to Center (#119 )Centers columns except when they are the first or last. local args, state = ...
local scroll = require("scroll")
local function on_focus(view, _)
local workspace = scroll.focused_workspace()
local container = scroll.view_get_container(view)
local parent = scroll.container_get_parent(container)
if parent then
container = parent
end
local containers = scroll.workspace_get_tiling(workspace)
for i, con in ipairs(containers) do
if con == container then
if i == 1 or i == #containers then
scroll.workspace_set_mode(workspace, { center_horizontal = false })
else
scroll.workspace_set_mode(workspace, { center_horizontal = true })
end
return
end
end
end
local data = scroll.state_get_value(state, "data")
if data == nil then
scroll.state_set_value(state, "data", { false, 0 })
data = scroll.state_get_value(state, "data")
end
if args[1] == 'toggle' then
data[1] = not data[1]
if data[1] then
local id = scroll.add_callback("view_focus", on_focus, nil)
scroll.state_set_value(state, "data", { true, id })
else
scroll.remove_callback(data[2])
scroll.state_set_value(state, "data", { false, 0 })
end
end |
Beta Was this translation helpful? Give feedback.
-
|
Trivially move a window to an output of a specific split-workspace position. If it is not split, it will split according to what split command you provide for arguments. -- Trivially move a window to an output at a specific split-workspace location
--
-- Use as follows:
-- bindsym <binding> lua to_out_split_pos.lua <target_output> <target_position> <split_command>
--
-- Example:
-- bindsym $mod+Alt+f lua $lua_scripts/to_out_split_pos.lua HDMI-A-1 top workspace split h 0.5
local args, state = ...
local scroll = require("scroll")
local to_output = args[1]
local to_split_pos = args[2]
local split_cmd = table.concat(args, " ", 3)
-- Find the monitor via provided monitor name
local outs = scroll.root_get_outputs()
local secondary
for _, out in ipairs(outs) do
if scroll.output_get_name(out) == to_output then
secondary = out
break
end
end
if not secondary then
scroll.log("Could not find secondary monitor.")
return
end
-- Prepare the move
local this = scroll.focused_container()
local target = scroll.output_get_active_workspace(secondary)
local target_name = scroll.workspace_get_name(target)
-- Get and ensure split
local target_split = scroll.workspace_get_split(target)
local split_pos = target_split['split']
if split_pos == "none" then
scroll.command(target, split_cmd)
end
-- Ensure the correct split position
if target_split['split'] ~= to_split_pos then
-- Not on the correct side, go to the other side
target = target_split['sibling']
target_split = scroll.workspace_get_split(target)
if target_split['split'] ~= to_split_pos then
-- User did the wrong split i.e. split v but request top or bottom
return
end
end
target_name = scroll.workspace_get_name(target)
scroll.command(this, 'move window to workspace ' .. target_name)
scroll.command(nil, 'workspace ' .. target_name)My workflow has a primary workspace on my primary monitor, and a secondary and sub-secondary workspace on my secondary monitor. I use this to quickly pin a window to my sub-secondary workspace. |
Beta Was this translation helpful? Give feedback.
-
|
Is there a way to get the cursor position via the Lua API currently? From my understanding in wayland the compositor needs to expose it, and I don't believe sway does so. I've seen that hyprctl has such a feature, for example. More specifically, I would like to implement an auto hide feature for my waybar. I already know I can bind it to a keybind to toggle it. Or use the 'scrollmsg bar mode toggle' command. Maybe more generally something like a hot-corner feature might be interesting, like Waycorner. Maybe monitoring zones and then have a callback whenever the cursor exits/leaves it? |
Beta Was this translation helpful? Give feedback.
-
|
hi, I have a question: |
Beta Was this translation helpful? Give feedback.
-
|
Simple MRU alt-tab script allowing focus change between the most-recently used windows. Transparently handles intermediate window closing; that is, opening a non-layer, transient window (e.g. a floating application launcher) will still let you alt-tab to the prior window. Code quality is probably not the most ideal, but it works for my usecase: local args, state = ...
local scroll = require("scroll")
local function value_idx(tab, val)
for index, value in ipairs(tab) do
if value == val then
return index
end
end
return -1
end
local function on_focus(view, _)
local view_container = scroll.view_get_container(view)
local con_id = scroll.container_get_id(view_container)
local focus_list = scroll.state_get_value(state, "focus_list")
if focus_list == nil then
focus_list = {}
end
local focus_idx = value_idx(focus_list, con_id)
if focus_idx > 0 then
table.remove(focus_list, focus_idx)
end
table.insert(focus_list, 1, con_id)
scroll.state_set_value(state, "focus_list", focus_list)
end
local function on_destroy(view, _)
local view_container = scroll.view_get_container(view)
local con_id = scroll.container_get_id(view_container)
local focus_list = scroll.state_get_value(state, "focus_list")
if focus_list == nil then
return
end
local focus_idx = value_idx(focus_list, con_id)
if focus_idx == -1 then
return
end
table.remove(focus_list, focus_idx)
scroll.state_set_value(state, "focus_list", focus_list)
end
local function switch_to_prev()
local focus_list = scroll.state_get_value(state, "focus_list")
if #focus_list < 2 then
return
end
local prev = focus_list[2]
scroll.command(nil, string.format("[con_id=%d] focus", prev))
end
if args[1] == "init" then
scroll.add_callback("view_focus", on_focus, nil)
scroll.add_callback("view_unmap", on_destroy, nil)
elseif args[1] == "prev" then
switch_to_prev()
endAdd the following to your Note that I'm positive the Lua API could be used to create a fully-featured MRU switcher akin to Windows without the thumbnail previews, but I'm not sure if it's possible to track the period where |
Beta Was this translation helpful? Give feedback.
-
Expand Column to Available Width (#169)Get the active column to exactly take the remaining width left by any others in the workspace if their widths add to less than the total output width: expand-column-to-available-width.lua local scroll = require("scroll")
local focused = scroll.focused_container()
local parent = scroll.container_get_parent(focused)
if parent == nil then
parent = focused
end
local workspace = scroll.focused_workspace()
local containers = scroll.workspace_get_tiling(workspace)
local total = 0.0
for _, container in ipairs(containers) do
if container ~= parent then
total = total + scroll.container_get_width_fraction(container)
end
end
if total < 1.0 then
local fraction = 1.0 - total
scroll.command(focused, "set_size h " .. fraction)
end |
Beta Was this translation helpful? Give feedback.
-
|
Hello, thank you for the project! I encountered a situation that I don't understand. There are two Lua scripts, both of which register callback functions. The first script is from #48 (comment) for focusing on urgent containers. The second script is for linking two containers, something like trailmark, for easier navigation between them. So, while the second script seems to work, the first one, which focuses on urgent containers, does not. More precisely, it starts working when you run it manually via scrollmsg. scrollmsg lua ./config/scroll/scripts/urgent_focus.lua |
Beta Was this translation helpful? Give feedback.
-
|
Hello, thank you for the project! |
Beta Was this translation helpful? Give feedback.
-
|
Thanks for the work. I have just stumbled upon this yesterday, and it's great. Also is there a way to replicate hyprland's exec command, which starts a process and opens the window on a particular workspace. exec, [workspace 2 silent] kitty |
Beta Was this translation helpful? Give feedback.
-
|
I wanted to focus a given workspace on current monitor. Like hyprland's |
Beta Was this translation helpful? Give feedback.
-
|
I am trying to implement focusing on an existing view, and I thought of doing the following:
And here it is not entirely clear what to do next. I thought about going through the array obtained in step 2, but in this case, it is not possible to get the array of views for a specific workspace or the array of containers. I also thought about simply executing a command such as 00:05:31.099 [sway/criteria.c:851] Found pair: app_id=‘vifm’
00:05:31.099 [sway/commands.c:302] Handling command ‘focus’But local result = scroll.command(nil, “[app_id=‘vifm’] focus”) |
Beta Was this translation helpful? Give feedback.
-
|
You can loop over root like this: local scroll = require("scroll")
local function set_focus(containers)
for _, container in ipairs(containers) do
local views = scroll.container_get_views(container)
for _, view in ipairs(views) do
if scroll.view_get_app_id(view) == "vifm" then
local con = scroll.view_get_container(view)
if con ~= nil then
scroll.container_set_focus(con)
return true
end
end
end
end
return false
end
local outputs = scroll.root_get_outputs()
for _, output in ipairs(outputs) do
local workspaces = scroll.output_get_workspaces(output)
for _, ws in ipairs(workspaces) do
local tiling = scroll.workspace_get_tiling(ws)
if set_focus(tiling) then
return
end
local floating = scroll.workspace_get_floating(ws)
if set_focus(floating) then
return
end
end
endYou can add |
Beta Was this translation helpful? Give feedback.
-
Sending Data from Your Scripts to the World Through IPCThere is a new Lua API function, You can subscribe to the "lua" event and receive data sent by Lua scripts. For example: local scroll = require("scroll")
local table = {
workspace = "2",
scroll = true,
num = 123,
{
x = 1200, y = 800,
},
{ "dog", "cat", "horse" },
}
scroll.ipc_send("test", table)Subscribe to the event: scrollmsg -t subscribe "[ \"lua\" ]"and call the script: scrollmsg lua /home/xxxx/.config/scroll/scripts/ipc.luaYou will see: {
"id": "test",
"data": {
"1": {
"x": 1200,
"y": 800
},
"2": [
"dog",
"cat",
"horse"
],
"workspace": "2",
"num": 123,
"scroll": true
}
}You can use this to export any type of information to your bar or external scrips |
Beta Was this translation helpful? Give feedback.
-
|
I am trying to implement a view count in the workspace. local args, state = ...
ViewsTable = { viewsCount = 0 }
local function countViewsInContainer(workspaceContainers)
for _, container in ipairs(workspaceContainers) do
local containerViews = scroll.container_get_views(container)
ViewsTable.viewsCount = #containerViews + ViewsTable.viewsCount
end
end
local function countViewsOnFocus(view, _)
ViewsTable.viewsCount = 0
local viewConainter = scroll.view_get_container(view)
local viewWorkspace = scroll.container_get_workspace(viewConainter)
local workspaceContainers = scroll.workspace_get_tiling(viewWorkspace)
countViewsInContainer(workspaceContainers)
scroll.ipc_send("views", ViewsTable)
end
local function countViewsOnDestroy(view, _)
ViewsTable.viewsCount = 0
local viewWorkspace = scroll.focused_workspace()
local workspaceContainers = scroll.workspace_get_tiling(viewWorkspace)
countViewsInContainer(workspaceContainers)
scroll.ipc_send("views", ViewsTable)
end
scroll.add_callback("view_focus", countViewsOnFocus, nil)
scroll.add_callback("view_unmap", countViewsOnDestroy, nil)Everything works, but when the window is closed, the recalculation does not occur. Could you please help me figure this out? |
Beta Was this translation helpful? Give feedback.
-
|
This question is off-topic, but is it possible to somehow trigger IPC before starting scroll? #!/usr/bin/env bash
declare date
coproc luaViewsIPC {
swaymsg -mt subscribe '["lua"]' | jq -r --unbuffered 'select(.id == "views").data.viewsCount'
}
while true
do
read views <&"${luaViewsIPC[0]}"
date=$(LC_TIME=ru_RU.UTF-8 date +'%a %d/%m/%y %R')
echo "$views |" "$date"
sleep 0.5
doneCurrently, I use |
Beta Was this translation helpful? Give feedback.
-
|
one question (sorry if thats out of place or has already been asked), but would there be some way to configure scroll entirely through a lua script, completely ditching the normal sway/i3 config? thats (at least for me) more for aesthetic reasons than anything else, so dont take it too seriously. |
Beta Was this translation helpful? Give feedback.

Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Since 6a97635 there is a Lua API and
luacommand to provide scripting abilities to scroll.Using the command
luayou can run Lua scripts that access window manager properties, execute commands or add callbacks to window events.You can assign scripts to keyboard bindings, or add them to your configuration for execution when the configuration loads.
I opened this issue for feedback, suggestions on how to improve the API, or cool scripts you created that are using the API. Once the API is mature, I will add it to scroll stable.
Feel welcome to share any ideas or missing APIs you'd like to have.
To read more about the API,
man 5 scrolland search for the LUA section.Below some scripts included in the README
There is now a module definition file for LSP auto-completion and documentation. Depending on the configuration of your LSP server, you may place
scroll.luain some subdirectory of your scroll scripts directory. For example, people using a Neovim configuration, may add it to theluasubdir, like this:Now you can use auto-completion and read the documentation while you write your scripts.
Note that a buggy script can crash scroll because it lets you manipulate C pointers. So it is better to do your development and testing using a "nested" instance of scroll. Wayland compositors can be run in a window, so you can run a new instance of the compositor in a window created in scroll. Write a simplified config file (
~/.config/scroll/config.debug) with a differentmodkey (maybe Alt instead of Super) so your main compositor doesn't trap key bindings you want to send to the nested one. In the new configuration, remove yourexeccommands (bar, startup apps, dbus etc.) that are not needed, and rename your monitors toWL-1,WL-2, etc. Then run scroll from a terminal:scroll -c ~/.config/scroll/config.debugYou can test your scripts in that environment, knowing that a crash won't bring your main session down.
Beta Was this translation helpful? Give feedback.
All reactions