river
and AII used dwl
until dwl was stated as stagnant. I tried hyprland, but it was not clear to me how to add custom layouts, and I like my grid based layout from dwl. Also i would have to set up a new bar, and would have to work with their way of showing things, which i felt was way too much effort. I wanted the flexibility of dwl. There is also a maximalist fork of dwl called mangowc, but I would have to either accept the maximalist view of it or maintain a stripped down fork of that, which seemed convoluted. niri was not tiled, and I don’t think I would like the scrolling layout.
riverwc was what I finally settled on. The main advantage that I saw was that its structured the same way as dwl, with rule based layouting. The custom layout protocol was great, especially river-luatile which allows you to write the layouts in lua. After some off by one errors I had implemented a gaplessgrid and monocle layout, and used the default master
layout from river-luatile. I setup keybinds to change between them: I really love that you can call any lua command from the keybinds, which made the layout changing so easy.
#! /usr/bin/lua
-- You can define your global state here
local agaps = 4
local master_ratio = 0.55
function iswidescreen(args)
return args.width > 2 * args.height
end
function master(args)
local retval = {}
local gaps = agaps
if args.count == 1 then
table.insert(retval, { 0, 0, args.width, args.height })
elseif args.count > 1 then
local main_w = (args.width - gaps * 3) * master_ratio
local side_w = (args.width - gaps * 3) - main_w
local main_h = args.height - gaps * 2
local side_h = (args.height - gaps) / (args.count - 1) - gaps
table.insert(retval, {
gaps,
gaps,
main_w,
main_h,
})
for i = 0, (args.count - 2) do
table.insert(retval, {
main_w + gaps * 2,
gaps + i * (side_h + gaps),
side_w,
side_h,
})
end
end
return retval
end
function grid(args)
local gaps = agaps
if args.count == 1 then
gaps = 0
end
local retval = {}
local ncols = 1
while ncols * ncols < args.count do
ncols = ncols + 1
end
if iswidescreen(args) and args.count == 3 then
ncols = 3 -- 1:1:1
elseif args.count == 5 then
ncols = 2
end
local nrows = math.ceil(args.count / ncols)
local cw = args.width / ncols
local ch = args.height / nrows
local fcnrows = args.count - (nrows * (ncols - 1))
for row = 0, fcnrows - 1 do
local fcch = args.height / fcnrows
local x = 2 * gaps
local y = row * fcch + ((row == 0) and 2 * gaps or gaps)
local w = cw - 3 * gaps
local h = fcch - (row == 0 and 2 * gaps or gaps) - ((row == fcnrows - 1) and 2 * gaps or gaps)
table.insert(retval, { x, y, w, h })
end
for i = fcnrows, args.count - 1 do
local cellid = (i - fcnrows) + nrows
local row = (cellid + fcnrows) % nrows
local col = math.floor(cellid / nrows)
local x = col * cw + gaps
local y = row * ch + ((row == 0) and 2 * gaps or gaps)
local w = cw - ((col == ncols) and 3 * gaps or 2 * gaps)
local h = ch - (row == 0 and 2 * gaps or gaps) - ((row == fcnrows - 1) and 2 * gaps or gaps)
table.insert(retval, { x, y, w, h })
end
return retval
end
function monocle(args)
local retval = {}
for _ = 0, args.count - 1 do
table.insert(retval, { 0, 0, args.width, args.height })
end
return retval
end
-- The most important function - the actual layout generator
--
-- The argument is a table with:
-- * Focused tags (`args.tags`)
-- * Window count (`args.count`)
-- * Output width (`args.width`)
-- * Output height (`args.height`)
-- * Output name (`args.output`)
--
-- The return value must be a table with exactly `count` entries. Each entry is a table with four
-- numbers:
-- * X coordinate
-- * Y coordinate
-- * Window width
-- * Window height
local active_layout = grid
function handle_layout(args)
return active_layout(args)
end
-- This optional function returns the metadata for the current layout.
-- Currently only `name` is supported, the name of the layout. It get's passed
-- the same `args` as handle_layout()
function handle_metadata(args)
if active_layout == grid then
return { name = "#" }
elseif active_layout == master then
return { name = "T" }
elseif active_layout == monocle then
return { name = "M" }
end
end
function set_layout(arg)
active_layout = arg
end
function incmr()
if master_ratio < 0.9 then
master_ratio = master_ratio + 0.05
end
end
function decmr()
if master_ratio > 0.1 then
master_ratio = master_ratio - 0.05
end
end
Migrating from dwl’s config.h to river’s init file was pretty easy, especially with the help of some regex replacements. Since river’s init can be written in anything, I just used bash scripts. I just had to remember to quote *
and stuff, and it was seriously simpler than the c
macros I was using in dwl.
I was using the systray fork of dwlb as my bar in dwl, which depended on the ipc patch. I was dreading having to set up a new bar system which also had a systray. It looked like waybar was the only one, but I would have to learn how to configure it. Then I saw sandbar which looked exactly like dwlb! I was pleasantly surprised by the similarities until I saw that they were both written by the same person, kolunmi! I realised that the systray and “bar without tags” patches were pretty easily transferable to sandbar, with only one major issue: dwlb used a socket based communication, whereas sandbar uses a pipe based communication. Since the systray requires the height of the rendered bar, it technically had to be a child process of sandbar. But since the width of systray would change, the communication had to happen from the child to parent. This meant piping the stdout of systray back into stdin of sandbar, reminiscent of the human caterpillar. Since this would prevent my status program from communicating sandbar, I used aistudio to tell me how to create another fd
in sandbar and pipe it to that instead. Learnt a good deal about pipes I guess.
But river still did not have two features that I used a lot in dwl: swallow
and dim-unfocused
.
I tried to use river without these features for a week or so. dim-unfocused
was something I missed but could live without. The lack of swallow
on the other hand was annoying. I searched if someone had already implemented it or if there was interest in this. There certainly was! But as people in this issue rightly pointed out, any implementation would be quite hacky. This was fine in the “patch and maintain it yourself” ideology of dwl, but not for the centrally maintained “install and run” ideology of river.
But nothing stopped me from patching it for my own personal use. Except that river is a pretty complex application, and written in zig, which I don’t know. As is the trend these days I used aistudio to help me.
Just asking it to implement swallow led to abysmal results, obviously. What I’ve learnt is to instead tell it to not make any assumptions, and instead ask me for information. It took a little coaxing, but with an initial tree of files, finally aistudio was able to ask accurately for the contents of the files it asked me to edit, and gave much better results.
It did get stuck on how it assumed a particular c
call (wl_client_get_credentials
) would be ported to zig, and kept suggesting different variations to try, until I looked for the source code of the library and asked it which file contents it would like to look at. Once I gave it the contents though, it was able to accurately implement the call. There was a similar issue with a file read (I imagine it has not seen a lot of zig code, since I assumed this would be elementary), but again manually providing it with the docs helped it correctly implement it.
And with this, swallow
was implemented with just one bug. When I tried to unswallow
, it crashed. When I informed aistudio of the error, it guessed an invalid null pointer exception. I again cautioned it against guessing and provided a backtrace. With this it accurately caught that the unswallow code was being run too late, after the child client was already in a half-destroy
ed state. Fixing this and some documentation and code completion changes, we were done.
With the harder swallow patch done, I started a new chat with a prompt that looked like
>tree of river source
This is the structure of river window manager. In the following requests, do not make any guesses, instead ask for information. Provide concise answers. Do not add text about feeling sorry from my frustration if something doesn't work. If we hit a snag, we will look for information, not make guesses.
I want to implement a dimming method which dims unfocussed windows. I already have an implementation for dwl. This is only a starting point, if you have a better idea, we will use that.
>my dim-unfocused patch from dwl
This resulted in far better results and the changes were very smooth, and stepwise.
I think this LLM business is pretty useful if you yourself know how to do it. Its a very good tool, but its just that, a tool, still. So I guess the conclusion of this post is our jobs are still somewhat kinda safe? In any case, I now have a system that will hopefully last for a year, before river 0.3.x gets dropped in favor of rwm
, and I’ll have to migrate all over again.