This commit is contained in:
nazrin 2025-05-23 15:41:23 +00:00
commit d2743fe41d
22 changed files with 2290 additions and 0 deletions

136
lib/async.lua Normal file
View file

@ -0,0 +1,136 @@
-- This source code form is subject to the terms of the MPL-v2 https://mozilla.org/MPL/2.0/ --
--* .name Asynchronous callbacks for anything that needs it
local C = require("c")
local X = require("x")
local Conf = require("conf")
local ev = require("ev") -- https://github.com/brimworks/lua-ev
local Async = {ev = ev}
--* fd(int fd, int events, function callback) Raw file descriptor callbacks
function Async.fd(fd, evs, cb)
ev.IO.new(cb, fd, evs):start(ev.Loop.default)
end
function Async.idle(cb)
local id
id = ev.Idle.new(function()
id:stop(ev.Loop.default)
cb()
X.sync()
end)
id:start(ev.Loop.default)
end
--* timer(float after, [float repeat], function callback) Calls **callback** after **after** seconds, and then repeats on a **repeat** second loop if it's provided
function Async.timer(after, rep, cb)
if not cb then rep, cb = cb, rep end
if after <= 0 then after = 0.00000001 end
ev.Timer.new(cb, after, rep):start(ev.Loop.default)
end
--* .desc The *spawn** functions return a table with the **pid**, as well as *write*(*str* **data**) and *close*() for stdin
local StdinWrapper = {
write = function(self, text)
C.write(self.stdin, text)
return self
end,
close = function(self)
C.close(self.stdin)
end
}
StdinWrapper.__index = StdinWrapper
--* table stdin spawn(str program, [str arg, ...], [function callback]) Spawns a program and calls **callback** once it exits. Parameters passed to **callback** are (*str* **stdout**, *str* **stderr**, *int* **exitStatus**)
function Async.spawn(...)
local args = {...}
local cb
if #args == 0 then error("Need a program") end
if type(args[#args]) == "function" then cb = table.remove(args, #args) end
local pid, stdin, stdout, stderr
if cb then
pid, stdin, stdout, stderr = C.spawn(args)
if not pid then return nil, "Failed to spawn" end
local childWatcher
childWatcher = ev.Child.new(function(loop, child)
childWatcher:stop(loop)
local out = C.read(stdout)
local err = C.read(stderr)
C.close(stdin, stdout, stderr)
cb(out, err, child:getstatus().exit_status)
end, pid, false)
childWatcher:start(ev.Loop.default)
else
pid, stdin, stdout, stderr = C.spawn(args)
if not pid then return nil, "Failed to spawn" end
end
return setmetatable({ pid = pid, stdin = stdin }, StdinWrapper)
end
--* table stdin spawnShell(str command, [str shell], [function callback]) Runs **command** in a shell instace. The shell is the first of **shell**, **Conf.shell**, **$SHELL**, "/bin/sh". Parameters passed to **callback** are the same as with *spawn*
function Async.spawnShell(str, shell, cb)
if not cb then cb, shell = shell, cb end
return Async.spawn(shell or Conf.shell or os.getenv("SHELL") or "/bin/sh", "-c", str, cb)
end
--* table stdin spawnRead(str program, [str arg, ...], function dataCallback, [function exitCallback]) Spawns a program and calls **dataCallback** when the program writes to stdout or stderr, then **exitCallback** once it exits. Parameters to **dataCallback** are (*str* **stdout**, *str* **stderr**). Paramaters to **exitCallback** are (*int* **exitStatus**)
function Async.spawnRead(...)
local args = {...}
if #args < 2 then error("Need a program and callback") end
local outcb = table.remove(args, #args)
if type(outcb) ~= "function" then error("Need a callback") end
local exitcb
if type(args[#args]) == "function" then
if #args < 2 then error("Need a program") end
outcb, exitcb = table.remove(args, #args), outcb
if type(outcb) ~= "function" then error("Need a callback") end
end
local pid, stdin, stdout, stderr = C.spawn(args)
C.setNonBlocking(stdout)
C.setNonBlocking(stderr)
local stdoutWatcher = ev.IO.new(function(loop, io, revents)
local d = C.read(stdout)
print(d, #d)
if d and #d > 0 then
outcb(d, nil)
end
end, stdout, ev.READ)
stdoutWatcher:start(ev.Loop.default)
local exitWatcher
exitWatcher = ev.Child.new(function(loop, child, revents)
local out = C.read(stdout)
local err = C.read(stderr)
C.close(stdout, stderr, stdin)
stdoutWatcher:stop(loop)
exitWatcher:stop(loop)
outcb(out, err)
if exitcb then exitcb(child:getstatus().exit_status) end
end, pid, false)
exitWatcher:start(ev.Loop.default)
return setmetatable({pid = pid, stdin = stdin}, StdinWrapper)
end
--* table stdin spawnReadShell(str command, [str shell], function dataCallback, [function exitCallback]) Combination of *spawnRead* and *spawnShell*
function Async.spawnReadShell(str, shell, outcb, exitcb)
if type(shell) == "function" then
shell, outcb, exitcb = nil, shell, outcb
end
return Async.spawnRead(shell or Conf.shell or os.getenv("SHELL") or "/bin/sh", "-c", str, outcb, exitcb)
end
function Async.readFile(path, cb)
local stdout = C.getFD(assert(io.open(path, "r")))
C.setNonBlocking(stdout)
local chunks = {}
local stdoutWatcher
stdoutWatcher = ev.IO.new(function()
local c = C.read(stdout)
if c and #c > 0 then
table.insert(chunks, c)
else
C.close(stdout)
stdoutWatcher:stop(ev.Loop.default)
cb(table.concat(chunks))
end
end, stdout, ev.READ)
stdoutWatcher:start(ev.Loop.default)
end
function Async.loop()
ev.Loop.default:loop()
end
return Async

46
lib/conf.lua Normal file
View file

@ -0,0 +1,46 @@
-- This source code form is subject to the terms of the MPL-v2 https://mozilla.org/MPL/2.0/ --
-- (re)loads the config file and stores settings
local X = require("x")
local Conf = {}
function Conf.onKeyPress(key, mod, cb)
if type(key) ~= "string" or type(mod) ~= "number" or type(cb) ~= "function" then
error("Parameters are wrong")
end
local n, err = X.grabKey(key, mod)
assert(not err, err)
local id = key..mod
if Conf.keyPressHandlers[id] then
error(string.format("Key %s with mod %i already bound", key, mod))
end
Conf.keyPressHandlers[id] = cb
end
function Conf.on(t, cb)
Conf.eventHandlers[t] = cb
end
function Conf.init(path)
Conf.keyPressHandlers = {}
Conf.keyReleaseHandlers = {}
Conf.eventHandlers = {}
path = path or Conf.cmdArgs.config or (os.getenv("XDG_CONFIG_HOME") or (os.getenv("HOME").."/.config")).."/thornWM/config.lua"
local func = loadfile(path)
if func then
local s, err = pcall(func)
if s then
return
end
print(string.format("Config %s failed:%s", path, err))
os.exit(1)
else
print(string.format("Config %s not found", path))
end
local s, err = pcall((os.getenv("XDG_CONFIG_DIRS") or "/etc/xdg") .. "thornWM/config.lua")
if not s then
print(string.format("Config %s failed:%s", path, err))
os.exit(1)
end
end
return Conf

40
lib/event.lua Normal file
View file

@ -0,0 +1,40 @@
-- This source code form is subject to the terms of the MPL-v2 https://mozilla.org/MPL/2.0/ --
-- Event handling
local X = require("x")
local Conf = require("conf")
local Tree = require("tree")
local Event = {}
local xEventHandlers = {}
xEventHandlers[X.MapRequest] = function(ev)
if Tree.windows[ev.id] then return print("got duplicate map request") end
local win = Tree.newWindow(ev)
local handler = Conf.eventHandlers.newWindow
if handler and win then handler(win) end
end
xEventHandlers[X.DestroyNotify] = function(ev)
local win = Tree.windows[ev.id]
local handler = Conf.eventHandlers.delWindow
if handler and win then handler(win) end
if win then
win:unmanage()
end
end
xEventHandlers[X.KeyPress] = function(ev)
local handler = Conf.keyPressHandlers[ev.key..ev.mask]
if handler then handler(ev) end
end
function Event.handleXEvents()
while X.pending() > 0 do
local ev = X.nextEvent()
if xEventHandlers[ev.type] then
xEventHandlers[ev.type](ev)
end
if Conf.eventHandlers[ev.type] then
pcall(Conf.eventHandlers[ev.type], ev)
end
end
end
return Event

61
lib/ewmh.lua Normal file
View file

@ -0,0 +1,61 @@
-- This source code form is subject to the terms of the MPL-v2 https://mozilla.org/MPL/2.0/ --
--* Handles Extended Window Manager Hint (https://specifications.freedesktop.org/wm-spec/latest/) stuff that doesn't really fit in Tree
local Tree
local X = require("x")
local EWMH = {}
local pid = tonumber(io.open("/proc/self/stat"):read(8):match("%d+"))
--* init(str name) Sets up properties to the root and support check windows
function EWMH.init(name)
Tree = require("tree")
local supported = {
X.getAtoms(
"_NET_SUPPORTED", "_NET_SUPPORTING_WM_CHECK", "_NET_WM_NAME", "_NET_WM_PID", "_NET_WM_WINDOW_TYPE",
"_NET_WM_WINDOW_TYPE_DESKTOP", "_NET_WM_WINDOW_TYPE_DOCK", "_NET_WM_WINDOW_TYPE_TOOLBAR", "_NET_WM_WINDOW_TYPE_MENU",
"_NET_WM_WINDOW_TYPE_UTILITY", "_NET_WM_WINDOW_TYPE_SPLASH", "_NET_WM_WINDOW_TYPE_DIALOG", "_NET_WM_WINDOW_TYPE_NORMAL"
)
}
local supportWin = X.createWindow()
X.setProperty(supportWin, X.getAtoms("WM_CLASS"), X.getAtoms("STRING"), 8, X.PropModeReplace, { name, name })
X.setProperty(supportWin, X.getAtoms("_NET_WM_NAME"), X.getAtoms("UTF8_STRING"), 8, X.PropModeReplace, name)
X.setProperty(supportWin, X.getAtoms("_NET_SUPPORTING_WM_CHECK"), X.getAtoms("WINDOW"), 32, X.PropModeReplace, supportWin)
X.setProperty(supportWin, X.getAtoms("_NET_WM_PID"), X.getAtoms("CARDINAL"), 32, X.PropModeReplace, pid)
X.setProperty(X.root, X.getAtoms("_NET_SUPPORTING_WM_CHECK"), X.getAtoms("WINDOW"), 32, X.PropModeReplace, supportWin)
X.setProperty(X.root, X.getAtoms("_NET_SUPPORTED"), X.getAtoms("ATOM"), 32, X.PropModeReplace, supported)
end
--* bool should shouldManage(table win) Returns a boolean on whether the WM should manage the window
function EWMH.shouldManage(win)
local winTypeAtom = X.getProperty(win.id, X.getAtoms("_NET_WM_WINDOW_TYPE"), 0, X.getAtoms("ATOM"))
if not winTypeAtom then return true end
local winType = X.getAtomNames(winTypeAtom)
if winType == "_NET_WM_WINDOW_TYPE_DOCK" then
local attrs = X.getWindowAttributes(win.id)
win.winType = "dock"
if attrs.y == 0 then
-- print(inspect(Tree))
local m = Tree.focusedMonitor.margins
-- print("HHIHIHII", inspect(m))
-- print("m", inspect(attrs))
Tree.focusedMonitor.dockMargins = { top = attrs.height, bottom = 0, left = 0, right = 0 }
Tree.focusedMonitor:setMargins(Tree.focusedMonitor.margins)
end
return false
elseif winType == "_NET_WM_WINDOW_TYPE_SPLASH" then
return false
else
win.winType = winType or "normal"
end
return true
end
--* bool should shouldTile(table win) Returns a boolean on whether the WM should tile the window by default
function EWMH.shouldTile(win)
if win.winType == "_NET_WM_WINDOW_TYPE_DIALOG" then
return false
end
return true
end
return EWMH

85
lib/tree/init.lua Normal file
View file

@ -0,0 +1,85 @@
-- This source code form is subject to the terms of the MPL-v2 https://mozilla.org/MPL/2.0/ --
--* .name Manages the tree structure that holds all monitors, containers, windows, and tags
local X = require("x")
local EWMH = require("ewmh")
local Conf = require("conf")
local Tree = {
windows = {},
monitors = {},
focusedWindow = nil,
focusedMonitor = nil,
-- tags = {},
}
-- local Tag = require("tree.tag")
local Window = require("tree.window")
function Tree.newWindow(ev)
local win = Window.new(ev.id)
if EWMH.shouldManage(win) then
Tree.windows[win.id] = win
win.managed = true
if EWMH.shouldTile(win) then
table.insert(Tree.focusedMonitor.stack, win)
win.monitor = Tree.focusedMonitor
-- Tree.focusedMonitor:updateTiling()
else
win:float()
end
win:show()
else
win.managed = false
win:show()
return
end
return win
end
----* table tag Tree.newTag(any id, [table preset]) Creates a new tag that can then be added to tables. New tags are also initialised automatically with *Window*:*addTag*()
--function Tree.newTag(id, tbl)
-- if Tree.tags[id] then return Tree.tags[id] end
-- local tag = Tag.new(tbl)
-- tag.id = id
-- Tree.tags[id] = tag
-- return tag
--end
--* Tree.init() Sets up the tree
function Tree.init()
Window.init()
for m,monitor in pairs(X.getMonitors()) do
Tree.monitors[m] = monitor
monitor.number = m
monitor.stack = {}
monitor.dockMargins = { top = 0, bottom = 0, left = 0, right = 0 }
monitor.margins = { top = 5, bottom = 5, left = 5, right = 5 }
function monitor:updateTiling()
if self.tiler then
self.tiler:tile(self.stack, self)
end
end
function monitor:setMargins(m)
print("привет", inspect(m))
self.margins.top = m.top + self.dockMargins.top
self.margins.bottom = m.bottom + self.dockMargins.bottom
self.margins.right = m.right + self.dockMargins.right
self.margins.left = m.left + self.dockMargins.left
end
if Conf.eventHandlers.newMonitor then
Conf.eventHandlers.newMonitor(monitor)
end
end
Tree.focusedMonitor = Tree.monitors[1]
-- Tree.newTag("focused")
end
-- function Tree.updateDirty()
-- if Tree.dirtyGeometry then
-- for w,win in pairs(Tree.windows) do
-- if win.dirtyGeometry then
-- win:applyGeometry()
-- end
-- end
-- end
-- end
return Tree

56
lib/tree/tag.lua Normal file
View file

@ -0,0 +1,56 @@
---- TAG --
--local Tag = {}
--Tag.__index = Tag
--function Tag.new(tag)
-- tag = tag or {}
-- tag.windows = {}
-- return setmetatable(tag, Tag)
--end
----* Tag:setBorderColour(int colour) Sets the tag's window's border colour. **colour** is in the form of 0xRRGGBB
--function Tag:setBorderColour(col)
-- self.borderColour = col
-- for w,win in pairs(self.windows) do win:updateTag(self) end
--end
--function Tag:setBorderWidth(width)
-- self.borderWidth = width
-- for w,win in pairs(self.windows) do win:updateTag(self) end
--end
--function Tag:setMargin(top, right, bottom, left)
-- if type(top) == "table" then
-- self.margin = top
-- else
-- self.margin = {top = top, right = right, bottom = bottom, left = left}
-- end
-- for w,win in pairs(self.windows) do win:updateTag(self) end
--end
----* Tag:set(table props) Sets a table worth of properties
--function Tag:set(tbl)
-- for prop,value in pairs(tbl) do
-- if prop == "borderColour" then
-- self:setBorderColour(value)
-- elseif prop == "borderWidth" then
-- self:setBorderWidth(value)
-- elseif prop == "visible" then
-- if value then self:show() else self:hide() end
-- elseif prop == "margin" then
-- self:setMargin(value)
-- end
-- end
--end
----* Tag:show() Unhides windows from the screen
--function Tag:show()
-- self.visible = true
-- for w,win in pairs(self.windows) do
-- win:updateTag(self)
-- end
--end
----* Tag:hide() Hides windows from the screen
--function Tag:hide()
-- self.visible = false
-- for w,win in pairs(self.windows) do
-- win:updateTag(self)
-- end
--end
--return Tag

91
lib/tree/tilers.lua Normal file
View file

@ -0,0 +1,91 @@
local Tilers = {}
local inspect = require("inspect")
local dirs = {
left = function(m, w, h, stack, r, gap)
local masterBase = {
w = w * r - gap/2,
h = h,
x = m.left,
y = m.top
}
local childBase = {
w = (w * (1-r)) - gap/2,
h = (masterBase.h / (#stack-1)) - gap + gap / (#stack-1),
x = masterBase.x + masterBase.w + gap,
y = m.top
}
local childOffset = {
x = 0, y = childBase.h + gap
}
return masterBase, childBase, childOffset
end,
top = function(m, w, h, stack, r, gap)
local masterBase = {
w = w,
h = h * r - gap,
x = m.left,
y = m.top
}
local childBase = {
w = (masterBase.w / (#stack-1)) - gap + gap / (#stack-1),
h = (h * (1-r)) - gap,
x = m.left,
y = masterBase.y + masterBase.h + gap,
}
local childOffset = {
x = childBase.w + gap, y = 0
}
return masterBase, childBase, childOffset
end
}
dirs.right = function(...)
local masterBase, childBase, childOffset = dirs.left(...)
masterBase.x, childBase.x = childBase.x, masterBase.x
masterBase.w, childBase.w = childBase.w, masterBase.w
return masterBase, childBase, childOffset
end
dirs.bottom = function(...)
local masterBase, childBase, childOffset = dirs.top(...)
masterBase.y, childBase.y = childBase.y, masterBase.y
masterBase.h, childBase.h = childBase.h, masterBase.h
return masterBase, childBase, childOffset
end
local function makeTiler(getOff)
return {
r = 0.5,
tile = function(self, stack, monitor)
local m = monitor.margins
local w = monitor.width - m.left - m.right
local h = monitor.height - m.top - m.bottom
if #stack == 1 then
stack[1]:setGeometry(m.left, m.top, w, h)
elseif #stack >= 2 then
local masterBase, childBase, childOffset = getOff(m, w, h, stack, self.r, self.gap)
stack[1]:setGeometry(masterBase.x, masterBase.y, masterBase.w, masterBase.h)
for i=2,#stack do
stack[i]:setGeometry(childBase.x, childBase.y, childBase.w, childBase.h)
childBase.x = childBase.x + childOffset.x
childBase.y = childBase.y + childOffset.y
end
end
end,
setGap = function(self, gap)
assert(type(gap) == "number")
self.gap = gap
end,
gap = 0
}
end
Tilers = {
masterTop = makeTiler(dirs.top),
masterLeft = makeTiler(dirs.left),
masterRight = makeTiler(dirs.right),
masterBottom = makeTiler(dirs.bottom),
}
return Tilers

122
lib/tree/window.lua Normal file
View file

@ -0,0 +1,122 @@
local Tree
local X = require("x")
local Util = require("util")
local Async = require("async")
local Tag = require("tree.tag")
local function round(n)
return math.floor(n+0.5)
end
local Window = {
x = 0, y = 0,
w = 0, h = 0,
bx = 0, by = 0,
bw = 0, bh = 0,
margin = {top = 0, right = 0, bottom = 0, left = 0},
borderWidth = 0
}
Window.__index = Window
function Window.new(id)
local window = {
id = id,
monitor = Tree.focusedMonitor,
-- tags = {
-- main = Tag.new()
-- }
}
setmetatable(window, Window)
return window
end
function Window:getClass()
if self.class == nil then
local c1, c2 = X.getProperty(self.id, X.getAtoms("WM_CLASS"), 0, X.AnyPropertyType)
if c1 then
self.class = {c1, c2}
else
self.class = false
end
end
return self.class
end
function Window:getName()
self.name = X.getProperty(self.id, X.getAtoms("_NET_WM_NAME"), 0, X.getAtoms("UTF8_STRING"))
if not self.name then
self.name = X.getProperty(self.id, X.getAtoms("WM_NAME"), 0, X.AnyPropertyType)
end
return self.name
end
function Window:float()
end
function Window:setGeometry(x, y, w, h)
if self.x ~= x or self.y ~= y or self.width ~= w or self.h ~= h then
self.x, self.y, self.width, self.height = x, y, w, h
self:applyGeometry()
end
-- self.monitor.dirtyGeometry = true
-- self.dirtyGeometry = true
-- Tree.dirtyGeometry = true
-- self.dirtyGeometry = true
-- print("set")
-- Async.idle(function()
-- if self.dirtyGeometry then
-- self:applyGeometry()
-- end
-- print("ran")
-- self.dirtyGeometry = false
-- end)
end
function Window:applyGeometry()
X.setWindowGeometry(self.id, self.x, self.y, self.width, self.height)
end
function Window:show()
self.visible = true
X.mapWindow(self.id)
Tree.focusedMonitor:updateTiling()
end
function Window:hide()
self.visible = false
X.unmapWindow(self.id)
Tree.focusedMonitor:updateTiling()
end
--* Window:focus() Moves keyboard focus to window, appends it to the focus history (**Tree**.**focusHistory**), and adds the "focused" tag
function Window:unmanage()
local i = self:getIndex()
table.remove(self.monitor.stack, i)
if self.focused then
Tree.focusedWindow = nil
self.focused = nil
end
Tree.windows[self.id] = nil
Tree.focusedMonitor:updateTiling()
end
function Window:getIndex()
return Util.indexOf(self.monitor.stack, self)
end
function Window:getNext(d)
d = d or 1
local stack = Tree.focusedMonitor.stack
local w = self:getIndex()
local i = (((w-1) + d) % #stack) + 1
return stack[i]
end
function Window:focus()
self.focused = true
if Tree.focusedWindow then
Tree.focusedWindow.focused = false
end
Tree.focusedWindow = self
Tree.focusedContainer = self.parent
Tree.focusedMonitor.focusedContainer = self.parent
X.setInputFocus(self.id)
end
function Window:getStackIndex()
return Util.indexOf(self, self.monitor.stack)
end
function Window.init()
Tree = require("tree")
end
return Window

17
lib/util.lua Normal file
View file

@ -0,0 +1,17 @@
local Util = {}
function Util.indexOf(haystack, needle)
for b,blade in pairs(haystack) do
if blade == needle then return b end
end
end
function Util.getNext(stack, item, d)
local w = Util.indexOf(stack, item)
if not w then return nil end
local i = (((w-1) + d) % #stack) + 1
return stack[i]
end
return Util