Skip to content

Lua API and scripts #35

Closed
Closed

Description

@dawsers

Since 6a97635 there is a Lua API and lua command to provide scripting abilities to scroll.

Using the command lua you 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 scroll and search for the LUA section.

Below some scripts included in the README

Activity

  1. dawsers commented on Jul 7, 2025

    @dawsers
    OwnerAuthor

    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)

    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
    end

    Select 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")
  2. pinned this issue on Jul 7, 2025
  3. dawsers commented on Jul 8, 2025

    @dawsers
    OwnerAuthor

    Another example navigating the containers of a workspace.

    Toggle a master-type window layout using a key binding

    bindsym $mod+Shift+q lua $lua_scripts/master.lua toggle
    bindsym $mod+q lua $lua_scripts/master.lua swap
    bindsym $mod+Ctrl+q lua $lua_scripts/master.lua full
    
    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
  4. dawsers commented on Jul 8, 2025

    @dawsers
    OwnerAuthor

    This script is en example implementation of swallowing using the Lua API.

    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 ~/.config/scroll/config so it is always active

    ....
    lua /home/.../.config/scroll/scripts/swallow.lua
    ....
    

    swallow.lua

    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)
    
    
  5. dawsers commented on Jul 13, 2025

    @dawsers
    OwnerAuthor

    Callbacks

    Set a callback on workspace_create (on_create_ws), so each time a workspace with name 9 gets created, it opens and resizes a kitty terminal.

    The workspace_create callback sets a callback on view_map and then execs kitty. The view_map callback gets executed when a new application (kitty) is ready. It resizes the container for the application and then removes the view_map callback so it doesn't run again until reset by a new workspace 9 creation.

    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:

    lua $lua_scripts/workspace.lua
    
    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)
  6. dawsers commented on Jul 16, 2025

    @dawsers
    OwnerAuthor

    Maximize Toggle

    This 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:

    bindsym $mod+q lua $lua_scripts/maximize.lua toggle
    

    And save this script to $lua_scripts/maximize.lua

    maximize.lua

    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
  7. voidstar-null commented on Jul 17, 2025

    @voidstar-null

    Hi, is there a way to get lsp autocompletions, like we can get via setting the vim global variable inside neovim in the lsp spec ?

  8. dawsers commented on Jul 17, 2025

    @dawsers
    OwnerAuthor

    You should get function auto-completion by simply adding local scroll = require("scroll") near the top of the file, something like:

    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`.
  9. voidstar-null commented on Jul 17, 2025

    @voidstar-null

    You should get function auto-completion by simply adding local scroll = require("scroll") near the top of the file, something like:

    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.

    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.

  10. dawsers commented on Jul 17, 2025

    @dawsers
    OwnerAuthor

    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.

  11. Repository owner locked and limited conversation to collaborators on Jul 18, 2025
  12. converted this issue into a discussion #48 on Jul 18, 2025
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

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions