Help needed to fix inconsistent behavior between .lua config and hyprcrl

Hyprland version
PASTE YOUR HYPRLAND VERSION HERE (0.55.2), BETWEEN THE BACKTICKS. DO NOT REMOVE ANY FORMATTING.

I’m attempting to automate the process of switching between my main monitor and a connected TV on my PC using a shell script. However, I’d like to leverage hyprctl to automate this process. I have been modifying the config file manually to set it previously and now I’m using the lua configuration. Example:

-- Default Configuration
hl.monitor({ output = "DP-1", mode = "[email protected]", position = "0x0", scale = "1" })
hl.monitor({ output = "HDMI-A-1", disabled = true })

-- TV configuration
-- hl.monitor({ output = "HDMI-A-1", mode = "[email protected]", position = "0x0", scale = "1", bitdepth = 10 })
-- hl.monitor({ output = "DP-1", disabled = true })

These configurations work as expected, but I’d like to automate the process using a shell script.

game_tv() {
    hyprctl --batch "
        eval hl.monitor({ output = 'HDMI-A-1', mode = '[email protected]', position = '0x0', scale = 1, bitdepth = 10 }) ;
        eval hl.monitor({ output = 'DP-1', disabled = true })
         "
}

However, when I run the script using hyprctl --batch, it doesn’t apply the changes as expected.

Troubleshooting Attempts

I’ve tried running the command manually and without the --batch flag to see if it was a batch mode issue. Unfortunately, none of these attempts have resolved the problem.

hyprctl --batch "
           eval hl.monitor({ output = 'HDMI-A-1', mode = '[email protected]', position = '0x0', scale = 1, bitdepth = 10 }) ;
            eval hl.monitor({ output = 'DP-1', disabled = true })
            "
ok


ok

hyprctl eval 'hl.monitor({ output = "HDMI-A-1", mode = "[email protected]", position = "0x0", scale = 1, bitdepth = 10 })'
ok

I’d appreciate any assistance in resolving this issue.

There are two things with .55 that I have been struggling.

  1. Switching between monitors in a predefined fashion.
  2. Zoom in-out

With regards to the first one (which is your thread as well).
The most puzzling thing so far fas been to end up to the second monitor(TV) with the exact same active workspaces and clients without the appearance of a new workspace.

Since we are in .lua configs, I scripted a function for that. It is not perfect but the basics work.
That is enable HDMI-A-1, disable DP-1 and vv.

local function toggleMonitors()
    local monitors = hl.get_monitors() or {}
    local dp1_enabled = false

    for _, m in ipairs(monitors) do
        if m.name == "DP-1" and not m.disabled then
            dp1_enabled = true
            break
        end
    end

    if dp1_enabled then
        -- Switch to TV
        hl.monitor({ output = "DP-1", disabled = true })
        os.execute("sleep 1.0")
        hl.monitor({ output = "HDMI-A-1", mode = "3840x2160@30", position = "0x0", scale = 1.2, disabled = false })
        os.execute("sleep 1.0")
        hl.dispatch(hl.dsp.focus({ workspace = "1" }))
    else
        -- Switch to monitor
        hl.monitor({ output = "HDMI-A-1", disabled = true })
        os.execute("sleep 1.0")
        hl.monitor({ output = "DP-1", mode = "2560x1440@75", position = "0x0", scale = 1, disabled = false })
        os.execute("sleep 1.0")
        hl.dispatch(hl.dsp.focus({ workspace = "1" }))
    end
end

_G.toggleMonitors = toggleMonitors

You need to create a .lua module for that and add it to the “require”.
Then bind the function toggleMonitors() to any key combo that suits you best.

There are folks here that are familiar with lua/hyprland.
Maybe they will enlighten us both.

If you can better this, I am all eyes mate.

I think you need to pass the config through a dispatch (if that make sense), instead of raw eval (that only tell you if the code is correct, idk sum like that.):

game_tv() {
    hyprctl dispatch "
        hl.monitor({ output = 'HDMI-A-1', mode = '[email protected]', position = '0x0', scale = 1, bitdepth = 10 }) ;
        hl.monitor({ output = 'DP-1', disabled = true })
         "
}

Something like that

Thanks man. I totally forgot that lua have a lot more functionality than the old .config files. Taking your idea I tried this adding to the exec-once lua function but it didn’t work in the end.

-- Monitor switch function
local function toggleMonitors()
    local monitors = hl.get_monitors() or {}
    local dp1_enabled = false

    for _, m in ipairs(monitors) do
        if m.name == "DP-1" and not m.disabled then
            dp1_enabled = true
            break
        end
    end

    if dp1_enabled then
        -- Switch to TV
        hl.monitor({ output = "DP-1", disabled = true })
        os.execute("sleep 1.0")
        hl.monitor({ output = "HDMI-A-1", mode = "[email protected]", position = "0x0", scale = "1", bitdepth = 10, disabled = false })
        os.execute("sleep 1.0")
        hl.dispatch(hl.dsp.focus({ workspace = "1" }))
    else
        -- Switch to monitor
        hl.monitor({ output = "HDMI-A-1", disabled = true })
        os.execute("sleep 1.0")
        hl.monitor({ output = "DP-1", mode = "[email protected]", position = "0x0", scale = "1", disabled = false })
        os.execute("sleep 1.0")
        hl.dispatch(hl.dsp.focus({ workspace = "1" }))
    end
end

_G.toggleMonitors = toggleMonitors
--

-- Process Detector
local function is_process_running(process_name)
    local handle = io.popen("pgrep " .. process_name)
    local result = handle:read("*a"):match("%d+") -- Check for any PID output
    handle:close()
    if result ~= nil then
        return 0
    else
        return 1
    end
end

_G.is_process_running = is_process_running
--

-- USB controller detector
local function controller_status()
    local handle = io.popen("lsusb | grep '2dc8:310b'")
    local result = handle:read("*a")
    handle:close()
    if result ~= nil and result ~= "" then
        return 0
    else
        return 1
    end
end

_G.controller_status = controller_status
--

-- Run workaround script.
local function lua_workaround()
    local step1_timer = hl.timer(function()
        if controller_status() == 0 then
            step1_timer:destroy() -- Stop this timer
            local step2_timer = hl.timer(function()
                if is_process_running('pegasus-fe') == 0 then
                    step2_timer:destroy()
                    toggleMonitors()
                    local step3_timer = hl.timer(function()
                        if is_process_running('pegasus-fe') == 1 then
                            step3_timer:destroy()
                            toggleMonitors()
                        end
                    end, { timeout = 500, type = "repeat" })
                end
            end, { timeout = 500, type = "repeat" })
        end
    end, { timeout = 500, type = "repeat" })
end

_G.lua_workaround = lua_workaround
--

Thanks for the suggestion. Syntax error with this one as monitor is not a dispatcher. I previously tried to call the toggleMonitors function that @hyprm3 recommended with hyprctl but it also didn’t do anything again.

hyprctl dispatch 'hl.dsp.exec_cmd("toggleMonitors")'
ok

Kept trying and I remembered that the toggleMonitors function was declared globally. So I tried with hyprctl eval “toggleMonitors()”. Now it works as I intended.