Replies: 5 comments
-
Beta Was this translation helpful? Give feedback.
-
|
Thank you, @CRAG666 ! I am going to move this issue to Discussions, so other people can also add their ideas and contributions. I will also add a reference to newm in the title of the issue so people interested will find it more easily. I hope you don't mind. When I looked at newm, it looked cool, but to me grids have problems in the sense that if you want to have total freedom when resizing, you leave holes, and I couldn't figure out a good way to solve that problem. So, for people who only use a few window sizes, and especially when they are multiples of the same size, it works beautifully, but the drawback is resizing freedom. As for navigation, you may end up having a very big surface, and that is where So I am really glad you were able to get almost to the point where your workflow is more or less what you want. Let me know if I can help with small additions that could help you get closer to your goal. There are plans to add other layout types, but I have had to delay them to work on more pressing bugs. As soon as those are fixed, I will release another stable version and go back to adding new, bigger features. |
Beta Was this translation helpful? Give feedback.
-
|
I was thinking of starting an 'appreciation topic' after having used Scroll after some time. Well, I might as well post a reply here now. I've been using Scroll for about two weeks on a daily basis after migrating from Sway. As much as I love Sway, right now, I feel hard to go back. At first, the scrolling layout and the general workflow of managing windows needed some getting used to, but I no longer have the feeling of 'boxed in' when trying to open more than three windows on my 1080p monitor. As for other features not found in Sway (at least to my knowledge), I think my favourite one is I might try writing Lua scripts for Scroll later, as that's another topic I'm interested in. Thank you very much for your and all contributors' work! |
Beta Was this translation helpful? Give feedback.
-
|
I also love the grid mode in hyprscroller! I worked on a similar grid layout for scroll with some help from the author, though the logic is fairly simple — it scans existing columns from left to right and moves the new window into the first one that isn't full yet, rather than always opening a new column. Happy to share if anyone's interested! local args, state = ...
local scroll = require("scroll")
local config_path = "/tmp/grid_config.json"
local debug_notify = function(msg)
scroll.command(nil, 'exec notify-send "' .. msg .. '"')
end
local function save_grid_config(data)
local f = io.open(config_path, "w")
if f then
f:write("return {\n")
f:write(" active_workspaces = {\n")
for ws, cfg in pairs(data.active_workspaces) do
f:write(string.format(" ['%s'] = {%s},\n", ws, table.concat(cfg, ",")))
end
f:write(" }\n")
f:write("}\n")
f:close()
end
scroll.ipc_send("grid", data)
end
local function load_grid_config()
local f = io.open(config_path, "r")
if not f then
return nil
end
f:close()
local ok, data = pcall(dofile, config_path)
return ok and data or nil
end
local column_limit = tonumber(args[2]) or 3
local fit_size = tonumber(args[3]) or 0
local grid_rows = tonumber(args[4]) or column_limit
local grid_columns = tonumber(args[5]) or grid_rows
local grid = scroll.state_get_value(state, "grid_state")
if grid == nil then
local saved_data = load_grid_config()
if saved_data then
grid = {
map_id = nil,
unmap_id = nil,
last_view = nil,
active_workspaces = saved_data.active_workspaces or {},
}
else
grid = {
map_id = nil,
unmap_id = nil,
last_view = nil,
active_workspaces = {},
}
end
scroll.state_set_value(state, "grid_state", grid)
end
local function on_destroy(view, _)
local workspace = scroll.focused_workspace()
if not workspace then
return
end
local ws_name = scroll.workspace_get_name(workspace)
local tiling = scroll.workspace_get_tiling(workspace)
if not grid["active_workspaces"][ws_name] then
return
end
local children = scroll.container_get_children(tiling[1])
if #tiling == 1 and #children == 1 then
grid["active_workspaces"][ws_name] = nil
scroll.workspace_set_mode(workspace, { insert = "after", focus = true })
save_grid_config(grid)
end
end
local function on_create_view(view, _)
local focused_view = scroll.focused_view()
local workspace = scroll.focused_workspace()
if not workspace then
return
end
local tiling = scroll.workspace_get_tiling(workspace)
local current_ws_name = scroll.workspace_get_name(workspace)
grid["last_view"] = focused_view
-- Check if Grid Mode is active for this workspace
local ws_config = grid["active_workspaces"][current_ws_name]
if not ws_config then
return
end
scroll.workspace_set_mode(workspace, { insert = "end" })
local container = scroll.view_get_container(view)
if ws_config[4] == 1 then
scroll.command(container, "set_size h " .. (1 / ws_config[3]))
scroll.command(container, "set_size v " .. (1 / ws_config[2]))
end
if #tiling > 1 then
local target_tiling = nil
-- Iterate through existing columns to find a gap based on column_limit
for i = 1, #tiling - 1 do
local prev_container = tiling[i]
local children = scroll.container_get_children(prev_container)
if children and #children < ws_config[1] then
target_tiling = i
break
end
end
if target_tiling then
local steps = (#tiling - target_tiling - 1) * 2 + 1
for i = 1, steps do
scroll.command(container, "move left nomode")
end
end
end
if focused_view then
local focused_con = scroll.view_get_container(focused_view)
scroll.container_set_focus(focused_con)
end
end
local workspace = scroll.focused_workspace()
if not workspace then
return
end
local ws_name = scroll.workspace_get_name(workspace)
local function ensure_callback()
if not grid["map_id"] then
grid["map_id"] = scroll.add_callback("view_map", on_create_view, nil)
end
if not grid["unmap_id"] then
grid["unmap_id"] = scroll.add_callback("view_unmap", on_destroy, nil)
end
save_grid_config(grid)
end
if args[1] == "toggle" then
if grid["active_workspaces"][ws_name] then
grid["active_workspaces"][ws_name] = nil
scroll.workspace_set_mode(workspace, { insert = "after", focus = true })
debug_notify("Grid Mode: DISABLED for [" .. ws_name .. "]")
else
grid["active_workspaces"][ws_name] = { column_limit, grid_rows, grid_columns, fit_size }
scroll.workspace_set_mode(workspace, { insert = "end", focus = true })
debug_notify(
string.format(
"Grid Mode: ENABLED for %s (Lim:%d, H:%d, V:%d)",
ws_name,
column_limit,
grid_rows,
grid_columns
)
)
end
elseif args[1] == "disable" then
grid["active_workspaces"][ws_name] = nil
scroll.workspace_set_mode(workspace, { insert = "after", focus = true })
debug_notify("Grid Mode: DISABLED for [" .. ws_name .. "]")
elseif args[1] == "enable" then
grid["active_workspaces"][ws_name] = { column_limit, grid_rows, grid_columns, fit_size }
scroll.workspace_set_mode(workspace, { insert = "end", focus = true })
debug_notify(
string.format("Grid Mode: ENABLED for %s (Lim:%d, H:%d, V:%d)", ws_name, column_limit, grid_rows, grid_columns)
)
end
ensure_callback()
My main use case is playing multiple videos simultaneously in a grid (e.g. 3x3), so backfilling is important — when one video ends early, the next one should fill the empty slot to keep the grid intact rather than opening a new column. ^^ #eg
ls | xargs -I {} -P 9 mpv {} |
Beta Was this translation helpful? Give feedback.
-
|
update: local args, state = ...
local scroll = require("scroll")
local arg1 = args and args[1]
local OFF = (arg1 == "off")
local REPACK = (arg1 == "repack")
local MAX_PER_COL = tonumber(arg1) or 2
-- Flip to true and run scroll with `-d` to trace repack decisions.
local DEBUG = false
local function dbg(msg)
scroll.log("[grid] " .. msg)
end
-- Compact column-occupancy snapshot, e.g. "[2,2,1]". Debug only.
local function cols_str(ws)
local parts = {}
for ci, c in ipairs(scroll.workspace_get_tiling(ws)) do
parts[ci] = tostring(#scroll.container_get_views(c))
end
return "[" .. table.concat(parts, ",") .. "]"
end
-- Short-lived dialogs/pickers must not drive grid placement.
local IGNORE_APPS = {
["dialog"] = true,
["pwvucontrol"] = true,
["nm-connection-editor"] = true,
["xdg-desktop-portal-gtk"] = true,
["imv"] = true,
["org.kde.polkit-kde-authentication-agent-1"] = true,
["polkit-gnome-authentication-agent-1"] = true,
}
local function is_ignored(view)
local app = scroll.view_get_app_id(view)
return app ~= nil and IGNORE_APPS[app] == true
end
-- In a horizontal-layout workspace, "horizontal" mode opens a new column and
-- "vertical" stacks inside it; mirrored for vertical layouts. Returns
-- (new_column_mode, same_column_mode).
local function modes_for(ws)
local layout = scroll.workspace_get_layout_type(ws) or "horizontal"
if layout == "vertical" then
return "vertical", "horizontal"
end
return "horizontal", "vertical"
end
local function locate_view(view, ws)
local cols = scroll.workspace_get_tiling(ws)
for ci, col in ipairs(cols) do
for _, v in ipairs(scroll.container_get_views(col)) do
if v == view then
return ci, col, cols
end
end
end
end
-- New-window placement: keep filling the current column until the cap, then
-- switch the mode so the next window opens a fresh column.
local function on_view_map(view, _)
if not scroll.view_mapped(view) then
return
end
if is_ignored(view) then
return
end
local con = scroll.view_get_container(view)
if not con or scroll.container_get_floating(con) then
return
end
local ws = scroll.container_get_workspace(con)
if not ws then
return
end
local _, col = locate_view(view, ws)
if not col then
return
end
local count = #scroll.container_get_views(col)
local new_col, same_col = modes_for(ws)
scroll.workspace_set_mode(ws, { mode = (count >= MAX_PER_COL) and new_col or same_col })
end
-- Pull `source_view` (the first view of the column just right of the target)
-- into the target column with `move left nomode`. A lone window merges in one
-- move; a window with siblings is expelled into its own column first, then
-- merged -- exactly two. `source_col_count` (how many views shared source's
-- column) tells us the move count up front, so there's no need to rescan the
-- tree to detect when it landed -- O(1) instead of O(W). Window order within
-- the target column may change, but the grid shape is preserved.
local function pull_left(source_view, source_col_count)
local moves = (source_col_count > 1) and 2 or 1
for _ = 1, moves do
local con = scroll.view_get_container(source_view)
if not con then
return false
end
scroll.command(con, "move left nomode")
end
return true
end
-- Normalise the workspace into left-packed columns of exactly MAX_PER_COL
-- (last column may hold fewer). Walks columns left-to-right: a column under the
-- cap pulls the first view of the next column in; a column over the cap (e.g. a
-- window opened into an already-full column) expels its last view into its own
-- column on the right, which a later iteration packs. The tree is re-read each
-- step because moves invalidate container pointers. On a packed grid it makes
-- no moves.
local function repack(ws)
if DEBUG then
dbg("repack: start cols=" .. cols_str(ws))
end
-- The column list is fetched once and reused while we only walk (idx++);
-- it is re-fetched ONLY after a move, since moves invalidate container
-- pointers. A packed grid is therefore a single O(C) walk with no re-fetch.
local cols = scroll.workspace_get_tiling(ws)
local idx = 1
local guard = 0
while idx <= #cols do
guard = guard + 1
if guard > 1024 then
break
end
local cur_views = scroll.container_get_views(cols[idx])
local n = #cur_views
if n == MAX_PER_COL then
idx = idx + 1
elseif n > MAX_PER_COL then
-- Over-full: expel the last view into its own column on the right;
-- a later iteration packs it. Self-heals 3-stacks.
local con = scroll.view_get_container(cur_views[n])
if not con then
break
end
scroll.command(con, "move right nomode")
cols = scroll.workspace_get_tiling(ws)
else
-- Under-full: pull the first view of the next column in.
local nxt = cols[idx + 1]
if not nxt then
break -- last column, below the cap: the grid is packed
end
local nxt_views = scroll.container_get_views(nxt)
if #nxt_views == 0 then
break
end
if not pull_left(nxt_views[1], #nxt_views) then
break
end
cols = scroll.workspace_get_tiling(ws)
end
end
if DEBUG then
dbg("repack: done cols=" .. cols_str(ws))
end
end
-- Repack on focus changes. `busy` guards against the focus events our own moves
-- (and the focus restore) generate, so we never re-enter.
local function on_view_focus(view, _)
if scroll.state_get_value(state, "busy") then
return
end
local ws = scroll.focused_workspace()
if not ws then
return
end
scroll.state_set_value(state, "busy", true)
repack(ws)
-- Moves may have shifted focus; put it back where the user left it.
if view and scroll.view_mapped(view) then
local c = scroll.view_get_container(view)
if c then
scroll.container_set_focus(c)
end
end
scroll.state_set_value(state, "busy", nil)
end
-- One-shot manual repack (key binding). Guarded the same way.
local function manual_repack()
local ws = scroll.focused_workspace()
if not ws then
return
end
scroll.state_set_value(state, "busy", true)
repack(ws)
scroll.state_set_value(state, "busy", nil)
end
if REPACK then
manual_repack()
return
end
-- Drop callbacks/state from a previous run so reloads don't stack handlers.
for _, key in ipairs({ "map_id", "focus_id" }) do
local id = scroll.state_get_value(state, key)
if id then
scroll.remove_callback(id)
scroll.state_set_value(state, key, nil)
end
end
scroll.state_set_value(state, "busy", nil)
if OFF or MAX_PER_COL < 1 then
return
end
scroll.state_set_value(state, "map_id", scroll.add_callback("view_map", on_view_map, nil))
scroll.state_set_value(state, "focus_id", scroll.add_callback("view_focus", on_view_focus, nil)) |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I couldn't find any other way to express my gratitude for such an incredible WM.
I was one of the first users of Newm, and it was an incredible project with an amazing workflow. However, the project was discontinued. Then I switched to Hyprland, and I was never able to replicate my workflow until I found your plugin. It gave me back hope of replicating the flow; I recovered about 70% of it, but it was functional for me. Then you archived it and created this incredible project. At first, I was happy with what it offered (which is a lot), but I still remembered how great Newm was, so I decided to try the Lua scripts, and wow, it was wonderful! I recovered 90% of the workflow I had with Newm, and that made me so happy. I regained my motivation to work for everything. Thank you so much. I hope you can see what the Newm workflow was like and maybe give me some tips. For now, and for anyone who finds it useful, I'm sharing my Lua scripts to achieve something similar. Thanks again.
Beta Was this translation helpful? Give feedback.
All reactions