Resize a tiled window on startup?

Hi ! I have some apps that I start when hyprland starts up:

hl.on("hyprland.start", function()
    <...>
    hl.exec_cmd("obsidian", { workspace = "1" })
    hl.exec_cmd(
        "kitty --directory ~/Sync/Obsidian -- ~/.local/share/bob/nvim-bin/nvim",
        { workspace = "1" }
    )

    -- What i'm trying to do:
    hl.dsp.focus({ window = "class:kitty" })
    hl.dsp.layout("splitratio -0.5")
end)

Obsidian and nvim start on my other monitor. I am trying to re-size nvim (which is a tiled
window) to take up more space on startup. Splitratio works only on the active window, so I
need to focus it.

However this doesn’t seem to work, probably because there’s no kitty window yet when focus
/ layout runs?

Is there a way to do this or do I need to add a delay to focus / layout and hope for the
best?

Maybe I missed something? At first I found the size static effect but it only applies to floating windows.

Tried this:

 hl.dsp.exec_cmd(
         "sleep 5 && hyprctl dispatch 'hl.dsp.focus({window = \"class:kitty\"})' && sleep 1 && hyprctl dispatch 'hl.dsp.layout(\"splitratio -0.5\")'"
   )

Works if I bind it to a key. But if I put it inside the hyprland.start event listener it doesn’t

I’ll try adding an event listener that wait for kitty window to open and then runs the command…

There was a similar thread here - How to focus main monitor & main workspace on startup - #2 by ajgringo619 - that was solved by @vito.

Yep same problem I think.
hl.dsp is a table containing dispatchers
You need to actually dispatch them with either hl.bind or hl.dispatch

Thanks a lot, I think that was it. But I also needed an event listener and to make obsidian startup AFTER Kitty (Nvim).

I was testing in terminal with hyprctl dispatch, completely forgetting that it’s a shorthand for eval 'hl.dispatch(...)'

This is what I did :

hl.on("hyprland.start", function()
    hl.exec_cmd(
        "sleep 1 && kitty --directory ~/Sync/Obsidian -- ~/.local/share/bob/nvim-bin/nvim",
        { workspace = "1" }
    )
    hl.exec_cmd("obsidian", { workspace = "1" })
end)

local done = false
hl.on("window.open", function(win)
    if done or win.class ~= "kitty" then
        return
    end
    done = true
    hl.dispatch(hl.dsp.focus({ window = "class:kitty" }))
    hl.dispatch(hl.dsp.layout("splitratio 0.5"))
end)

Messy but works consistently from my testing 5/5 times. Will break if obsidian loads faster than nvim, though.

Is there a way to remove an event listener in this scenario after it has been ran 1 time?

@commute0498, Here is my attempt at a clean-ish method for this…

  1. Create file ~/.config/hypr/hlx.lua with the following content:
local M = {}
M.dsp = {}

--- @class exec_with_callback_opts
--- @field timeout number? Timeout in milliseconds to wait for window to open before giving up. Default is 5000ms.
--- @field tag string? Tag to use for identifying the window. Default is `layout_<random>`.

--- @class dispatch_return
--- @field ok boolean Whether the command was successfully dispatched
--- @field pass_event boolean Whether the event should be passed to other handlers

--- @alias exec_callback fun(window: table): nil

--- Execute a command with optional callback when window opens
--- Supports multiple signatures:
--- - exec_cmd(cmd, rules, callback, opts)
--- - exec_cmd(cmd, rules, callback)
--- - exec_cmd(cmd, callback, opts)
--- - exec_cmd(cmd, callback)
--- @param cmd string
--- @param rules table|function?
--- @param callback exec_callback|table?
--- @param opts exec_with_callback_opts?
--- @return HL.Dispatcher
function M.dsp.exec_cmd(cmd, rules, callback, opts)
  if type(rules) == 'function' and type(callback) == 'table' then -- Called as exec_cmd(cmd, callback, opts)
    opts = callback
    callback = rules
    rules = nil
  elseif type(callback) == 'table' and callback ~= nil then -- Called as exec_cmd(cmd, rules, opts) with no callback
    opts = callback
    callback = nil
  end

  opts = opts or {}
  rules = rules or {}

  -- If no callback, just dispatch normally
  if not callback then
    return hl.dsp.exec_cmd(cmd, rules)
  end

  -- With callback: use tags to id window
  local timeout_ms = opts.timeout or 5000
  local tag = opts.tag or ('launch_' .. tostring(math.random(1000000)))

  local found = false
  local subscription

  subscription = hl.on('window.open', function(window)
    if found or not subscription:is_active() then
      return
    end

    local tags = window.tags
    for _, window_tag in ipairs(tags) do
      if window_tag == tag or window_tag == tag .. '*' then
        found = true
        subscription:remove()
        callback(window)
        return
      end
    end
  end)

  hl.timer(function()
    if not found and subscription:is_active() then
      subscription:remove()
    end
  end, {
    timeout = timeout_ms,
    type = 'oneshot',
  })

  -- Merge tag into rules
  rules.tag = tag
  return hl.dsp.exec_cmd(cmd, rules)
end

--- Convenience function like the `hl.exec_cmd` to allow for direct calling without having to use hl.dispatch
--- @param cmd string
--- @param rules table|function?
--- @param callback exec_callback|table?
--- @param opts exec_with_callback_opts?
--- @return dispatch_return
function M.exec_cmd(cmd, rules, callback, opts)
  return hl.dispatch(M.dsp.exec_cmd(cmd, rules, callback, opts))
end

return M
  1. use like this:
local hlx = require('hlx')

local function kitty_window_layout(kitty_window)
  hl.dispatch(hl.dsp.focus({ window = kitty_window }))
  hl.dispatch(hl.dsp.window.swap({ window = kitty_window, direction = 'right' }))
  hl.dispatch(hl.dsp.layout('splitratio 0.5'))
end

local function launch_kitty()
  hlx.exec_cmd('kitty --directory ~/Sync/Obsidian -- ~/.local/share/bob/nvim-bin/nvim', { workspace = 1 }, kitty_window_layout)
end

hl.on('hyprland.start', function()
  hlx.exec_cmd('obsidian', { workspace = 1 }, launch_kitty)
end)

The extended version of exec_cmd will cleanup its event listener for you.
Read the typedef comments for usage if its not quite what your after.

Hope this is helpful. It has earned a place in my utils folder for my hl config :stuck_out_tongue:

Edit: changed to OP’s workspace setting of 1. (I was testing on ws 5)
Another edit: Fixed the neovim path back to OP’s stated one in the kitty cmd

Looks great ! I will test it out when I have the time. Thanks for your work :heart: