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

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
lib/*.so
docs/*.3
docs/*.1
misc/

373
LICENSE Normal file
View file

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

26
README.md Normal file
View file

@ -0,0 +1,26 @@
# thornWM
A toy X11 WM I never got around to finishing. I first wanted a manuel tiler with configuration similar to awesomeWM, then I later decided I prefered auto, then after switching the tiling mechanics I decided it was saner to slim down awesomeWM and never finished this but it is kinda functional
![image](mynd.png.webp)
## Requirements
* [Lua](https://www.lua.org/)[[jit](https://luajit.org/)] 5.1+
* [Lua-ev](https://github.com/brimworks/lua-ev) ([LuaRocks](https://luarocks.org/modules/brimworks/lua-ev))
Additionally, the default config makes use of:
* a terminal emulator
* dmenu
* lemonbar
* xsetroot
## Installation
* `git clone https://git.sylvie.moe/tanya/thornWM && cd thornWM`
* Optionally edit the makefile
* `make install`
## Thanks
* i3
* dwm
* awesomeWM
* Shaver McCoy (RIP)

158
config.lua Normal file
View file

@ -0,0 +1,158 @@
-- This source code form is subject to the terms of the MPL-v2 https://mozilla.org/MPL/2.0/ --
local Async, Tree = require("async"), require("tree")
local X, C, Conf = require("x"), require("c"), require("conf")
local Util = require("util")
local tilers = require("tree.tilers")
-- {{{ Settings
os.setlocale("") -- Set the locale to $LANG
Conf.shell = "sh" -- Used by Async.spawnShell
local term = "konsole"
local theme = {
gap = 5,
margin = 5,
borderWidth = 2,
defaultBorderColour = 0x444444,
focusedBorderColour = 0xaaaaaa,
}
local tilerStack = {
tilers.masterLeft, tilers.masterRight,
tilers.masterTop, tilers.masterBottom,
getNext = Util.getNext -- Convenience to do tiler:getNext()
}
for t,tiler in ipairs(tilerStack) do
tiler:setGap(theme.gap)
end
-- }}}
-- {{{ Events
Conf.on("newMonitor", function(mon)
mon.tiler = tilerStack[1]
mon:setMargins({ top = theme.margin, bottom = theme.margin, left = theme.margin, right = theme.margin })
end)
Conf.on("newWindow", function(win)
if win.managed then
win:focus()
end
end)
Conf.on("delWindow", function(win)
if win.managed then
if win.focused and #Tree.focusedMonitor.stack > 1 then
win:getNext(-1):focus()
end
end
end)
-- }}}
-- {{{ Bar
local barStdin = Async.spawn("lemonbar", "-B", "#111111", "-f", "Source Code Pro 50", "-g", "1920x25+0+0")
Async.timer(0.0, 1.0, function() -- Start after 0 seconds, repeat after 1 second
local tagStr = "[1][2][3][4][5][6][7][8][9][0]"
barStdin:write(string.format(" %s%%{r}%s \n", tagStr, os.date("%a %b %d %H:%M:%S")))
end)
-- }}}
-- {{{ Keybindings
-- See https://wiki.linuxquestions.org/wiki/List_of_keysyms for all keysyms
local shift, ctrl, alt, super = X.Shift, X.Control, X.Mod1, X.Mod2
local master = alt
-- Meta --
Conf.onKeyPress("r", master + shift, function()
Conf.init()
end)
Conf.onKeyPress("q", master + shift, function()
os.exit(0)
end)
-- Spawn --
Conf.onKeyPress("Return", master, function()
Async.spawn(term)
end)
Conf.onKeyPress("p", master, function()
Async.spawn("dmenu_run", "-l", "20", "-p", "Run: ")
end)
-- Tiling --
local function focus(ev)
if #Tree.focusedMonitor.stack < 2 then return end
local d = ({ j = 1, k = -1 })[ev.key]
Tree.focusedWindow:getNext(d):focus()
end
Conf.onKeyPress("j", master, focus)
Conf.onKeyPress("k", master, focus)
local function swap(ev)
local stack = Tree.focusedMonitor.stack
if #stack < 2 then return end
local d = ({ j = 1, k = -1 })[ev.key]
local cur = Tree.focusedWindow
local curi = cur:getIndex()
local newi = cur:getNext(d):getIndex()
stack[curi], stack[newi] = stack[newi], stack[curi]
Tree.focusedMonitor:updateTiling()
end
Conf.onKeyPress("j", master + shift, swap)
Conf.onKeyPress("k", master + shift, swap)
local function layout(ev)
local d = ({ [master] = 1, [master+shift] = -1 })[ev.mask]
local fm = Tree.focusedMonitor
fm.tiler = tilerStack:getNext(fm.tiler, d)
fm:updateTiling()
end
Conf.onKeyPress("space", master, layout)
Conf.onKeyPress("space", master + shift, layout)
-- Tags --
-- for t,tag in pairs(tags) do
-- Conf.onKeyPress(tag.key, master, function()
-- -- print(tag.id)
-- Tree.tags.default:hide()
-- end)
-- end
-- Misc --
-- Conf.onKeyPress("space", master, function() -- Lists all windows in a dmenu prompt
-- local windows, rwindows = {}, {}
-- for w,win in pairs(Tree.windows) do
-- local name = win:getName()
-- if name and not rwindows[name] then
-- table.insert(windows, name)
-- rwindows[name] = win
-- end
-- end
-- Async.spawn("dmenu", function(out, err, status)
-- print(out)
-- end):write(table.concat(windows, "\n")):close()
-- end)
-- }}}
-- {{{ Autostart
Async.spawn("xcompmgr")
Async.timer(0.1, function()
Async.spawn("st")
end)
Async.spawn("xsetroot", "-cursor_name", "left_ptr")
Async.spawn("/usr/bin/feh", "--no-fehbg", "--bg-fill", '/home/eiko/Niðurhal/anime/veggfóður/cirnohehe.png')
-- Async.spawnShell([[
-- setxkbmap -model pc105 -layout is,ru-is -option caps:escape grp:rctrl_switch grp:shift_caps_toggle compose:sclk shift:both_capslock >/dev/null
-- xset r rate 300 35
-- ]])
-- }}}
-- Async.readFile("./thornWM", print)
-- vim: fdm=marker:noet

114
docs/genDocs Executable file
View file

@ -0,0 +1,114 @@
#!/bin/env lua
-- This source code form is subject to the terms of the MPL-v2 https://mozilla.org/MPL/2.0/ --
-- Any documentation generated is licensed under PDL-v1 https://www.openoffice.org/licenses/PDL.html
local sourceFileName = assert(arg[1])
local textFileName = assert(arg[2])
local sourceFile = assert(io.open(sourceFileName))
local tf = assert(io.open(textFileName, "w"))
local cmdArgs = {}
for i=1,#arg do
if skipNext then
skipNext = false
else
local a = arg[i]
if a == "-e" or a == "--ext" then
cmdArgs.ext = arg[i+1]
skipNext = true
end
end
end
local ext = cmdArgs.ext or sourceFileName:match("[^.]+$")
local marker
if ext == "c" then
marker = "//%*"
elseif ext == "lua" then
marker = "--%*"
else
error("Unkown extension", ext)
end
local name = sourceFileName:match("([^/]+)%.%w+$")
local functions = {}
local description = {}
function bold(str)
return "\\fB"..str.."\\fR"
end
function italic(str)
return "\\fI"..str.."\\fR"
end
for line in sourceFile:lines() do
local s, e = line:find("^%s*"..marker.."%s*")
if s then
local str = line:sub(e+1)
s, e = str:find("%w%([^)]*%)")
if s then
local proto = str:sub(1, e)
local returnProto = ""
local des = str:sub(e+2)
if proto:match("^[%w|,[%]:.]+ ") then
returnProto = proto:match("^([^(]+) ")--:gsub("%w+", italic("%1")):gsub("%|", " | "):gsub(",", ", ").." "
returnProto = returnProto:gsub("^(%w+)", bold("%1")):gsub("| (%w+)", "| "..bold("%1")):gsub(", (%w+)", ", "..bold("%1")):gsub("%[(%w+)", "["..bold("%1"))
returnProto = returnProto:gsub(" %w+", italic("%1"))
returnProto = returnProto.." "
proto = proto:gsub("^([^(]+) (%w+)", "%2")
end
proto = proto:gsub("%w+[,)%]]?", function(word)
if word:match("[,)%]]") then
return italic(word:sub(1, -2)) .. word:sub(-1)
else
return bold(word)
end
end)
des = des:gsub("%*%*([%w$.]+)%*%*", italic("%1"))
des = des:gsub("%*([%w$.]+)%*", bold("%1"))
table.insert(functions, {
proto = proto,
returnProto = returnProto,
des = des
})
else
s, e = str:find("^%.%w+")
if e then
local cmd = str:sub(2, e)
local txt = str:sub(e+1):gsub("^%s+", "")
if cmd == "name" then
name = name .. " - " .. txt
elseif cmd == "desc" then
txt = txt:gsub("%*%*([%w$.]+)%*%*", italic("%1"))
txt = txt:gsub("%*([%w$.]+)%*", bold("%1"))
table.insert(description, txt)
end
end
end
end
end
table.sort(functions, function(a, b)
return a.proto < b.proto
end)
for f,func in pairs(functions) do
func.proto = func.returnProto..func.proto
end
tf:write(".TH thornWM 3\n.SH NAME\n"..name.."\n")
tf:write(".SH SYNOPSIS\n")
for f,func in pairs(functions) do
tf:write(".TP\n")
tf:write(func.proto.."\n")
end
tf:write(".SH DESCRIPTION\n")
tf:write(table.concat(description, "\n").."\n")
for f,func in pairs(functions) do
tf:write(".TP\n"..func.proto.."\n")
tf:write(func.des.."\n")
end
tf:write(".SH COPYRIGHT\nThe contents of this Documentation are subject to the Public Documentation License Version 1.0\n")

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

86
makefile Normal file
View file

@ -0,0 +1,86 @@
CC = cc
CFLAGS = -Wall -pipe -Og -g -march=native
USEXRANDR = 1
SHAREPATH = /usr/local/share/thornWM
DOCPATH = /usr/local/share/man
PROGPATH = /usr/local/bin
CONFPATH = /etc/xdg/thornWM
ifeq ($(USEXRANDR), 1)
XFLAGS+=-lXrandr
CFLAGS+=-DL_XRANDR
else
XFLAGS+=-lXinerama
endif
LUA_VERSION = 5.1
ifeq ($(LUA_VERSION), jit)
CFLAGS += `pkg-config --cflags luajit || echo -I/usr/include/lua{,5.1}`
else ifeq ($(LUA_VERSION), 5.1)
CFLAGS += `pkg-config --cflags lua5.1 --silence-errors`
else ifeq ($(LUA_VERSION), 5.2)
CFLAGS += `pkg-config --cflags lua5.2 --silence-errors`
else ifeq ($(LUA_VERSION), 5.3)
CFLAGS += `pkg-config --cflags lua5.3 --silence-errors`
else ifeq ($(LUA_VERSION), 5.4)
CFLAGS += `pkg-config --cflags lua5.4 --silence-errors`
else
$(error Invalid Lua version $(LUA_VERSION))
endif
all: libs docs
libs: lib/x.so lib/c.so
docs: docs/thornWM.x.3 docs/thornWM.c.3 docs/thornWM.async.3 docs/thornWM.ewmh.3 docs/thornWM.tree.3
install: installProgram installLibs installDocs
uninstall: uninstallProgram uninstallLibs uninstallDocs
.PHONY: all libs docs install uninstall installProgram installLibs installDocs uninstallProgram uninstallLibs uninstallDocs
# PROGRAM
installProgram:
mkdir -p $(PROGPATH) $(CONFPATH)
cp thornWM $(PROGPATH)/
cp config.lua $(CONFPATH)/
uninstallProgram:
rm -f $(PROGPATH)/thornWM $(CONFPATH)/config.lua
rmdir --ignore-fail-on-non-empty $(PROGPATH) $(CONFPATH)
# LIBS
lib/x.so: src/x.c src/common.h makefile
@echo $(CC) $< $@
@$(CC) $< -o $@ -lX11 $(CFLAGS) -shared -fPIC $(XFLAGS)
lib/c.so: src/c.c src/common.h makefile
@echo $(CC) $< $@
@$(CC) $< -o $@ $(CFLAGS) -shared -fPIC
installLibs: libs
mkdir -p $(SHAREPATH)/lib
cp lib/* $(SHAREPATH)/lib/
uninstallLibs:
rm -f $(SHAREPATH)/*.lua $(SHAREPATH)/*.so
rmdir --ignore-fail-on-non-empty $(SHAREPATH) $(SHAREPATH)/lib
# DOCS
docs/thornWM.%.3: src/%.c docs/genDocs
docs/genDocs $< $@
docs/thornWM.%.3: lib/%.lua docs/genDocs
docs/genDocs $< $@
docs/thornWM.%.3: lib/%/init.lua docs/genDocs
docs/genDocs $< $@
installDocs: docs
mkdir -p $(DOCPATH)/man{1,3}
cp docs/thornWM.1 $(DOCPATH)/man1/
cp docs/*.3 $(DOCPATH)/man3/
uninstallDocs:
rm -f $(DOCPATH)/man3/thornWM.x.3 $(DOCPATH)/man3/thornWM.async.3 $(DOCPATH)/man3/thornWM.c.3 \
$(DOCPATH)/man3/thornWM.ewmh.3 $(DOCPATH)/man3/thornWM.tree.3 $(DOCPATH)/man1/thornWM.1
rmdir --ignore-fail-on-non-empty $(DOCPATH)
clean:
rm -fv -- lib/*.so docs/*.3
.PHONY: all libs docs intsall installProgram installDocs installLibs clean

BIN
mynd.png.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 KiB

25
scripts/test-x.lua Executable file
View file

@ -0,0 +1,25 @@
#!/bin/env luajit
package.cpath = package.cpath .. ";lib/?.so"
local X = require("x")
local root = assert(X.open())
local win = X.createWindow()
X.setErrorHandler(print)
atom, integer = X.getAtoms("ATOM", "INTEGER")
assert(atom == 4 and integer == 19)
assert(X.getAtomNames(atom) == "ATOM")
X.setProperty(win, X.getAtoms("test"), atom, 32, X.PropModeReplace, 69)
assert(X.getProperty(win, X.getAtoms("test"), 0, atom) == 69)
X.setProperty(win, X.getAtoms("test"), X.getAtoms("STRING"), 8, X.PropModeReplace, "hello")
assert(X.getProperty(win, X.getAtoms("test"), 0, X.getAtoms("STRING")) == "hello")
X.delProperty(win, X.getAtoms("test"))
assert(not X.delProperty(win, X.getAtoms("test"), 0, X.getAtoms("STRING")))
assert(X.getMonitors() and X.getWindowAttributes(win))
X.close()

18
scripts/xephyr.sh Executable file
View file

@ -0,0 +1,18 @@
#!/bin/env sh
# test/xephyr.sh ./thornWM -c config.lua
# test/xephyr.sh test/x.lua
Xephyr -br -ac -noreset -screen 1920x1080 :1 &
sleep 0.1
export DISPLAY=:1
export LUA_PATH="/usr/share/lua/5.1/?.lua;/usr/share/lua/5.1/?/init.lua;/usr/lib/lua/5.1/?.lua;/usr/lib/lua/5.1/?/init.lua;./?.lua;./?/init.lua;/root/.luarocks/share/lua/5.1/?.lua;/root/.luarocks/share/lua/5.1/?/init.lua"
export LUA_CPATH="/usr/lib/lua/5.1/?.so;/usr/lib/lua/5.1/loadall.so;./?.so;/root/.luarocks/lib/lua/5.1/?.so"
"$@" ; kill %1 &>/dev/null
# mkfifo /tmp/thornWM.fifo
# (while true; do cat /tmp/thornWM.fifo; done) | th ./thornWM -c config.lua ; kill %1
wait

145
src/c.c Normal file
View file

@ -0,0 +1,145 @@
/* This source code form is subject to the terms of the MPL-v2 https://mozilla.org/MPL/2.0/ */
//* .name Glorified UNIX function exporter
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <poll.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include "common.h"
typedef struct myluaL_Stream {
FILE *f;
lua_CFunction closef;
} myluaL_Stream;
//* int fd getFD(file file) Returns the file descriptor associated with a Lua file
static int l_getFD(lua_State* L){
lua_pushinteger(L, fileno(((myluaL_Stream*)luaL_checkudata(L, 1, "FILE*"))->f));
return 1;
}
//* int pid, int stdin, int stdout, int stderr spawn(str program, [str arg, ...]) Spawns an external program from the system path and pipes its output
static int l_spawn(lua_State* L){
#define R 0
#define W 1
if(lua_type(L, 1) != LUA_TTABLE)
luaL_argerror(L, 1, "Need a table");
int nargs = lua_objlen(L, 1);
if(nargs < 1)
luaL_argerror(L, 1, "Need a table with something in it");
char* args[nargs+1];
args[nargs] = NULL;
for(int i = 0; i < nargs; i++){
lua_rawgeti(L, 1, i+1);
args[i] = (char*)luaL_checkstring(L, -1);
}
int pid;
int pin[2];
int pout[2];
int perr[2];
if(pipe(pin) < 0 || pipe(pout) < 0 || pipe(perr) < 0)
L_RETURN_ERROR("Failed to create pipes");
if((pid = fork())){
// parent
if(pid < 0)
L_RETURN_ERROR("Failed to fork");
close(pin[R]);
close(pout[W]);
close(perr[W]);
lua_pushinteger(L, pid);
lua_pushinteger(L, pin[W]);
lua_pushinteger(L, pout[R]);
lua_pushinteger(L, perr[R]);
return 4;
}
// child
close(pin[W]);
close(pout[R]);
close(perr[R]);
dup2(pin[R], STDIN_FILENO);
dup2(pout[W], STDOUT_FILENO);
dup2(perr[W], STDERR_FILENO);
execvp(args[0], args); // should never return
int en = errno;
dprintf(STDERR_FILENO, "thornWM: Failed to run '%s': %s", args[0], strerror(en));
exit(en);
return 0;
#undef R
#undef W
}
//* str data read(int fd) Reads as much as it can from a file descriptor
static int l_read(lua_State* L){
int fd = luaL_checkinteger(L, 1);
if(fcntl(fd, F_GETFD) < 0)
L_RETURN_ERROR("Invalid FD");
luaL_Buffer buf;
luaL_buffinit(L, &buf);
int r;
char readBuf[4096];
while(1){
r = read(fd, readBuf, 4096);
if(r <= 0)
break;
luaL_addlstring(&buf, readBuf, r);
}
luaL_pushresult(&buf);
return 1;
}
//* close(int fd) Closes a file descriptor
static int l_close(lua_State* L){
int top = lua_gettop(L);
for(int i = 1; i <= top; i++){
int fd = luaL_checkinteger(L, i);
if(fcntl(fd, F_GETFD) < 0)
L_RETURN_ERROR("Invalid FD");
close(fd);
}
return 0;
}
//* write(int fd, str data) Writes **data** to a file descriptor
static int l_write(lua_State* L){
int fd = luaL_checkinteger(L, 1);
size_t len;
const char* str = luaL_checklstring(L, 2, &len);
if(fcntl(fd, F_GETFD) < 0)
L_RETURN_ERROR("Invalid FD");
lua_pushinteger(L, write(fd, str, len));
return 1;
}
//* setNonBlocking(int fd) Sets a file descriptor to be non-blocking
static int l_setNonBlocking(lua_State* L){
int fd = luaL_checkinteger(L, 1);
if(fcntl(fd, F_GETFD) < 0)
L_RETURN_ERROR("Invalid FD");
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
return 0;
}
static const struct luaL_Reg l_cutillib[] = {
{"getFD", l_getFD},
{"spawn", l_spawn},
{"read", l_read},
{"write", l_write},
{"close", l_close},
{"setNonBlocking", l_setNonBlocking},
{NULL, NULL}
};
int luaopen_c(lua_State* L){
lua_newtable(L);
for(int i = 0; l_cutillib[i].func; i++){
lua_pushcfunction(L, l_cutillib[i].func);
lua_setfield(L, -2, l_cutillib[i].name);
}
return 1;
}

35
src/common.h Normal file
View file

@ -0,0 +1,35 @@
/* This source code form is subject to the terms of the MPL-v2 https://mozilla.org/MPL/2.0/ */
#define L_SET_INT(s, i, t) { \
lua_pushstring(L, s); \
lua_pushinteger(L, i); \
lua_rawset(L, t); \
}
#define L_SET_BOOL(s, i, t) { \
lua_pushstring(L, s); \
lua_pushboolean(L, i); \
lua_rawset(L, t); \
}
#define L_SET_STR(s, i, t) { \
lua_pushstring(L, s); \
lua_pushstring(L, i); \
lua_rawset(L, t); \
}
#define L_SET_LSTRING(s, i, size, t) { \
lua_pushstring(L, s); \
lua_pushlstring(L, i, size); \
lua_rawset(L, t); \
}
#define L_RETURN_ERROR(string, ...) { \
lua_pushnil(L); \
lua_pushfstring(L, string" :(", ##__VA_ARGS__); \
return 2; \
}
#define L_THROW_ERROR(string) { \
lua_pushstring(L, string); \
lua_error(L); \
}
#if LUA_VERSION_NUM >= 502
#define lua_objlen lua_rawlen
#endif

543
src/x.c Normal file
View file

@ -0,0 +1,543 @@
/* This source code form is subject to the terms of the MPL-v2 https://mozilla.org/MPL/2.0/ */
//* .name Xlib bindings
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <string.h>
#include <setjmp.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#ifdef L_XRANDR
#include <X11/extensions/Xrandr.h>
#else
#include <X11/extensions/Xinerama.h>
#endif
#include "common.h"
static Display* display;
static Window root;
static lua_State* gL;
static int lastXErrorCode;
#define REGISTRY_ERROR_HANDLER "thornWM.xErrorHandler"
// --- ERROR ---
static int onXError(Display* display, XErrorEvent* err){
char buffer[1024];
XGetErrorText(display, err->error_code, buffer, 1023);
/* fprintf(stderr, "X error: %s\n\tcode: %u, request: %u.%u\n", buffer, err->error_code, err->request_code, err->minor_code); */
lastXErrorCode = err->error_code;
lua_pushstring(gL, REGISTRY_ERROR_HANDLER);
lua_rawget(gL, LUA_REGISTRYINDEX);
/* printf("type: %s\n", lua_typename(gL, lua_type(gL, -1))); */
if(lua_type(gL, -1) == LUA_TFUNCTION){
printf("pushing error handler args\n");
lua_pushfstring(gL, "X error: %s\n\tcode: %u, request: %u.%u\n", err->error_code, err->request_code, err->minor_code);
lua_pushinteger(gL, err->error_code);
lua_call(gL, 2, 0);
}
return 0;
}
//* setErrorHandler(function handler) Fires when Xlib reports an error
static int l_setErrorHandler(lua_State* L){
/* printf("setting error hnalder %p %p\n", L, gL); */
if(lua_type(L, 1) != LUA_TFUNCTION)
luaL_argerror(L, 1, "Function required");
lua_pushstring(L, REGISTRY_ERROR_HANDLER);
lua_rawset(L, LUA_REGISTRYINDEX);
return 0;
}
// --- ATOMS ---
//* atom atom, [atom atom, ...] getAtoms(str name, [str name, ...]) Fetches atom(s) by name. Atoms are ints with names for use in X
static int l_getAtoms(lua_State* L){
int top = lua_gettop(L);
bool onlyIfExists = false;
if(lua_type(L, top) == LUA_TBOOLEAN)
onlyIfExists = lua_toboolean(L, top--);
char* names[top];
for(int i = 0; i < top; i++)
names[i] = (char*)luaL_checkstring(L, i+1);
Atom atoms[top];
XInternAtoms(display, names, top, onlyIfExists, atoms);
for(int i = 0; i < top; i++){
if(atoms[i])
lua_pushinteger(L, atoms[i]);
else
lua_pushnil(L);
}
return top;
}
//* str name, [str name, ...] getAtomNames(atom atom, [atom atom, ...]) Fetches atom name(s)
static int l_getAtomNames(lua_State* L){
int top = lua_gettop(L);
Atom atoms[top];
for(int i = 0; i < top; i++)
atoms[i] = luaL_checkinteger(L, i+1);
char* names[top];
XGetAtomNames(display, atoms, top, (char**)(&names));
for(int i = 0; i < top; i++)
lua_pushstring(L, names[i]);
return top;
}
// --- PROPERTIES ---
//* str prop | int prop getProperty(int win, atom prop, int offset, atom type) Fetches a property. Returns one or more ints/strings. **type** can be **AnyPropertyType**
static int l_getProperty(lua_State* L){
Window win = luaL_checkinteger(L, 1);
Atom prop = luaL_checkinteger(L, 2);
long long offset = luaL_optnumber(L, 3, 0);
Atom type = luaL_optnumber(L, 4, 0);
Atom actualReturnType;
int actualReturnFormat;
unsigned long numberReturned;
unsigned long excessBytes;
unsigned char* propReturn;
XGetWindowProperty(display, win, prop, offset, 4096, false,
type, &actualReturnType, &actualReturnFormat, &numberReturned, &excessBytes, &propReturn);
if(!actualReturnType)
L_RETURN_ERROR("Property does not exist");
if(!numberReturned)
L_RETURN_ERROR("Wrong property type");
int strings = 0;
char* str = (char*)propReturn;
switch(actualReturnFormat){
case 32:
for(int i = 0; i < numberReturned; i++){
lua_pushinteger(L, *(((int32_t*)propReturn)+(i*2)));
}
return numberReturned;
case 8:
do{
lua_pushstring(L, (char*)str);
strings++;
} while((((str = strchr(str, '\0')+1) - (char*)propReturn) < numberReturned-1));
return strings;
case 16:
/* printf("got a 16 bit value!!!"); */
for(int i = 0; i < numberReturned; i++){
lua_pushinteger(L, *(((int16_t*)propReturn)+(i*2))); // untested. idk where a 16 bit value is
}
return numberReturned;
default:
return 0;
}
}
//* setProperty(int win, atom prop, atom type, int format, int mode, table|str|int value) Sets a property. Accepts multiple values in a table. **mode** is one of **PropModeAppend**, **PropModePrepend**, **PropModeReplace**
static int l_setProperty(lua_State* L){
Window win = luaL_checkinteger(L, 1);
Atom prop = luaL_checkinteger(L, 2);
Atom type = luaL_checkinteger(L, 3);
int format = luaL_checkinteger(L, 4);
int mode = luaL_checkinteger(L, 5);
unsigned char data[4096] = {'\0'}; // this can overflow FIXME
size_t dataTop = 0;
bool isTable = lua_istable(L, 6);
int len = isTable ? lua_objlen(L, 6) : 1;
int start = 6 + isTable;
for(int i = 0; i < len; i++){
if(isTable)
lua_rawgeti(L, 6, i+1);
const char* str; int num;
switch(lua_type(L, start+i)){
case LUA_TSTRING:
str = lua_tostring(L, start+i);
num = lua_objlen(L, start+i);
memcpy(data+dataTop, str, num);
dataTop += num;
data[dataTop++] = '\0';
break;
case LUA_TNUMBER:
if(format == 32){
*((int32_t*)data+i*2) = lua_tointeger(L, start+i);
dataTop += 4;
} else {
*((int16_t*)data+i*2) = lua_tointeger(L, start+i);
dataTop += 2;
}
}
}
/* lua_pushlstring(L, data, dataTop); */
XChangeProperty(display, win, prop, type, format, mode, data, dataTop/(format/8));
return 0;
}
//* delProperty(int win, atom prop) Deletes a property.
static int l_delProperty(lua_State* L){
XDeleteProperty(display, luaL_checkinteger(L, 1), luaL_checkinteger(L, 2));
return 0;
}
// --- WINDOW INFORMATION ---
//* table children getWindowChildren([int win]) Returns a table of child ids. Defaults to the root window
static int l_getWindowChildren(lua_State* L){
Window win = luaL_optnumber(L, 1, root);
unsigned wnum;
Window d1, d2;
Window* wins;
if(!XQueryTree(display, win, &d1, &d2, &wins, &wnum))
return 0;
lua_newtable(L);
for(int w = 0; w < wnum; w++){
lua_pushinteger(L, wins[w]);
}
if(wins)
XFree(wins);
return 1;
}
//* table attrs getWindowAttributes([int win]) Returns a window's **id**, **mapState**, **x**, **y**, **width**, **height**, **depth**, and **borderWidth** in a table. Defaults to the root window
static int l_getWindowAttributes(lua_State* L){
Window win = luaL_optnumber(L, 1, root);
XWindowAttributes wa;
if(!XGetWindowAttributes(display, win, &wa))
return 0;
lua_newtable(L);
L_SET_INT("id", win, -3);
L_SET_INT("mapState", wa.map_state, -3);
L_SET_INT("x", wa.x, -3);
L_SET_INT("y", wa.y, -3);
L_SET_INT("width", wa.width, -3);
L_SET_INT("height", wa.height, -3);
L_SET_INT("depth", wa.depth, -3);
L_SET_INT("borderWidth", wa.border_width, -3);
return 1;
}
// --- WINDOW APPEARANCE ---
//* setBorderWidth(int win, int width) Sets a window's border width in pixels
static int l_setBorderWidth(lua_State* L){
XSetWindowBorderWidth(display, luaL_checkinteger(L, 1), luaL_checkinteger(L, 2));
return 0;
}
//* setBorderColour(int win, int colour) Sets a window's border colour. **colour** is in the form of 0xRRGGBB
static int l_setBorderColour(lua_State* L){
XSetWindowBorder(display, luaL_checkinteger(L, 1), luaL_checkinteger(L, 2));
return 0;
}
// --- WINDOW GEOMETRY ---
//* mapWindow(int win) Maps a window to the screen
static int l_mapWindow(lua_State* L){
XMapWindow(display, luaL_checkinteger(L, 1));
return 0;
}
//* unmapWindow(int win) Unmaps a window from the screen
static int l_unmapWindow(lua_State* L){
printf("unmpaping window\n");
XUnmapWindow(display, luaL_checkinteger(L, 1));
return 0;
}
//* setWindowGeometry(int win, int x, int y, int width, int height) Sets a window's geometry
static int l_setWindowGeometry(lua_State* L){
XMoveResizeWindow(display, luaL_checkinteger(L, 1), luaL_checkinteger(L, 2),
luaL_checkinteger(L, 3), luaL_checkinteger(L, 4), luaL_checkinteger(L, 5));
return 0;
}
//* setWindowSize(int win, int width, int height) Sets a window's size
static int l_setWindowSize(lua_State* L){
XResizeWindow(display, luaL_checkinteger(L, 1), luaL_checkinteger(L, 2), luaL_checkinteger(L, 3));
return 0;
}
//* setWindowPosition(int win, int x, int y) Sets a window's coordinates
static int l_setWindowPosition(lua_State* L){
XMoveWindow(display, luaL_checkinteger(L, 1), luaL_checkinteger(L, 2), luaL_checkinteger(L, 3));
return 0;
}
// --- WINDOW OTHER ---
//* pingWindow(int win) Pings a window to see if it's still processing X events. See https://specifications.freedesktop.org/wm-spec/1.3/ar01s06.html
static int l_pingWindow(lua_State* L){
Window win = luaL_checkinteger(L, 1);
Atom atoms[2];
char* names[2] = {"WM_PROTOCOLS", "_NET_WM_PING"};
XInternAtoms(display, names, 2, true, atoms);
XEvent ev;
ev.type = ClientMessage;
ev.xclient.type = ClientMessage;
ev.xclient.window = win;
ev.xclient.message_type = atoms[0];
ev.xclient.data.l[0] = atoms[1];
/* ev.xclient.data.l[1] = CurrentTime(); */
ev.xclient.data.l[2] = win;
printf("XSendEvent %i\n", XSendEvent(display, win, false, 0, &ev));
return 0;
}
//* setInputFocus(int win) Sets the input focus for the keyboard
static int l_setInputFocus(lua_State* L){
Window win = luaL_checkinteger(L, 1);
XSetInputFocus(display, win, RevertToParent, CurrentTime);
return 0;
}
// --- MONITORS ---
//* table monitors getMonitors() Returns a list of monitors with their geometry
static int l_getMonitors(lua_State* L){
lua_newtable(L);
#ifdef L_XRANDR
XRRScreenResources *res = XRRGetScreenResources(display, root); // this is the most fucking cursed fuck fucking bullshit
for(int i = 0; i < res->ncrtc; i++){
XRRCrtcInfo* info = XRRGetCrtcInfo(display, res, res->crtcs[i]);
if(!info->width){
XRRFreeCrtcInfo(info);
break;
}
lua_newtable(L);
L_SET_INT("width", info->width, -3);
L_SET_INT("height", info->height, -3);
L_SET_INT("x", info->x, -3);
L_SET_INT("y", info->y, -3);
XRRFreeCrtcInfo(info);
lua_rawseti(L, -2, i+1);
}
XRRFreeScreenResources(res);
#else
int num;
XineramaScreenInfo* info = XineramaQueryScreens(display, &num);
for(int i = 0; i < num; i++){
lua_newtable(L);
L_SET_INT("width", info[i].width, -3);
L_SET_INT("height", info[i].height, -3);
L_SET_INT("x", info[i].x_org, -3);
L_SET_INT("y", info[i].y_org, -3);
lua_rawseti(L, -2, i+1);
}
XFree(info);
#endif
return 1;
}
// --- INPUT ---
//* grabKey(str key, int mask) Grabs a key combo to receive events from it. **key** is a keysym, see https://wiki.linuxquestions.org/wiki/List_of_keysyms for a list. **mask** is a list of modifiers added together
static int l_grabKey(lua_State* L){
const char* key = luaL_checkstring(L, 1);
int mask = luaL_checkinteger(L, 2);
KeySym keysim = XStringToKeysym(key);
if(!keysim)
L_RETURN_ERROR("KeySim not found for '%s'", key)
KeyCode keycode = XKeysymToKeycode(display, keysim);
XGrabKey(display, keycode, mask, root, true, GrabModeAsync, GrabModeAsync);
return 0;
}
//* releaseKey(str key, int mask) Stop receiving key events
static int l_releaseKey(lua_State* L){
return 0;
}
//* grabMouseButton(int button, int mask) Start receiving events about one of the three mouse buttons. **button** is one of the three mouse buttons, and **mask** is the same as with *grabKey*
static int l_grabMouseButton(lua_State* L){
int button = luaL_checkinteger(L, 1);
int mask = luaL_checkinteger(L, 2);
XGrabButton(display, button, mask, root, true, ButtonPressMask, GrabModeAsync, GrabModeAsync, None, None);
return 0;
}
//* releaseMouseButton(int button, int mask) Stop receiving mouse button events
static int l_releaseMouseButton(lua_State* L){
return 0;
}
//* grabMousePointer() Start receiving events from mouse movement
static int l_grabMousePointer(lua_State* L){
XGrabPointer(display, root, true, PointerMotionMask|ButtonReleaseMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
return 0;
}
//* releaseMousePointer() Stop receiving events from mouse movement
static int l_releaseMousePointer(lua_State* L){
XUngrabPointer(display, CurrentTime);
return 0;
}
// --- EVENTS ---
//* table event nextEvent() Read an event from the queue
int l_nextEvent(lua_State* L){
XEvent ev;
XNextEvent(display, &ev);
lua_newtable(L);
L_SET_INT("type", ev.type, -3);
switch(ev.type){
case MapRequest:
L_SET_INT("id", ev.xmaprequest.window, -3);
L_SET_INT("parent", ev.xmaprequest.parent, -3);
return 1;
case DestroyNotify:
L_SET_INT("id", ev.xdestroywindow.window, -3);
return 1;
case KeyPress:
case KeyRelease:
L_SET_STR("key", XKeysymToString(XLookupKeysym(&ev.xkey, 0)), -3);
L_SET_INT("mask", ev.xkey.state, -3);
return 1;
case ClientMessage:
printf("got a client message");
return 1;
}
return 1;
}
// --- META ---
//* int root open([str display]) Opens the connection to the X server. This needs to be called before any other function, otherwise the program will segfault. **display** is which display should be connected to, and defaults to **$DISPLAY**. Returns the root window id
static int l_open(lua_State* L){
display = XOpenDisplay(lua_isnoneornil(L, 1) ? NULL : luaL_checkstring(L, 1));
if(!display)
L_RETURN_ERROR("Failed to connect to X");
root = DefaultRootWindow(display);
XSetErrorHandler(onXError);
lastXErrorCode = 0;
XSelectInput(display, root, SubstructureRedirectMask | SubstructureNotifyMask);
XSync(display, root);
if(lastXErrorCode)
L_RETURN_ERROR("Failed to get SubstructureRedirectMask. Is a WM already running?");
#if L_XRANDR
XRRSelectInput(display, root, RRScreenChangeNotifyMask);
#endif
lua_pushinteger(L, root);
return 1;
}
//* close() Closes the X server connection
static int l_close(lua_State* L){
XCloseDisplay(display);
return 0;
}
//* sync([bool discard]) Flushes the output buffer and waits until all requests have been received and processed by the X server. **discard** is whether it should discard all events on the event queue and defaults to false
static int l_sync(lua_State* L){
XSync(display, lua_toboolean(L, 1));
return 0;
}
//* int number pending() Returns the number of events enqueued
static int l_pending(lua_State* L){
lua_pushinteger(L, XPending(display));
return 1;
}
//* str version getVersion() Returns a string with X component versions
static int l_getVersion(lua_State* L){
int xrmajor, xrminor;
#ifdef L_XRANDR
#define MMEXT "Xrandr"
XRRQueryVersion(display, &xrmajor, &xrminor);
#else
#define MMEXT "Xinerama"
XineramaQueryVersion(display, &xrmajor, &xrminor);
#endif
lua_pushfstring(L, "%s %d %d.%d - %s - "MMEXT" %d.%d",
ServerVendor(display), VendorRelease(display), ProtocolVersion(display), ProtocolRevision(display),
DisplayString(display),
xrmajor, xrminor);
#undef MMEXT
return 1;
}
//* int id createWindow() Creates a simple window
static int l_createWindow(lua_State* L){
Window win = XCreateSimpleWindow(display, root, 0, 0, 1, 1, 0, 0, 0);
lua_pushinteger(L, win);
return 1;
}
//* int fd getFD() Returns the file descriptor referencing the X socket
static int l_getFD(lua_State* L){
lua_pushinteger(L, XConnectionNumber(display));
return 1;
}
static const struct luaL_Reg l_xlib[] = {
// ERROR
{"setErrorHandler", l_setErrorHandler},
// ATOMS
{"getAtoms", l_getAtoms},
{"getAtomNames", l_getAtomNames},
// PROPERTIES
{"getProperty", l_getProperty},
{"setProperty", l_setProperty},
{"delProperty", l_delProperty},
// WINDOW INFORMATION
{"getWindowChildren", l_getWindowChildren},
{"getWindowAttributes", l_getWindowAttributes},
// WINDOW APPEARANCE
{"setBorderWidth", l_setBorderWidth},
{"setBorderColour", l_setBorderColour},
// WINDOW MANIPULATION
{"mapWindow", l_mapWindow},
{"unmapWindow", l_unmapWindow},
{"setWindowGeometry", l_setWindowGeometry},
{"setWindowSize", l_setWindowSize},
{"setWindowPosition", l_setWindowPosition},
// WINDOW OTHER
{"pingWindow", l_pingWindow},
{"setInputFocus", l_setInputFocus},
// MONITORS
{"getMonitors", l_getMonitors},
// INPUT
{"grabKey", l_grabKey},
{"releaseKey", l_releaseKey},
{"grabMouseButton", l_grabMouseButton},
{"releaseMouseButton", l_releaseMouseButton},
{"grabMousePointer", l_grabMousePointer},
{"releaseMousePointer", l_releaseMousePointer},
// EVENTS
{"nextEvent", l_nextEvent},
// META
{"open", l_open},
{"close", l_close},
{"sync", l_sync},
{"pending", l_pending},
{"getVersion", l_getVersion},
{"createWindow", l_createWindow},
{"getFD", l_getFD},
{NULL, NULL}
};
int luaopen_x(lua_State* L){
gL = L;
lua_newtable(L);
for(int i = 0; l_xlib[i].func; i++){
lua_pushcfunction(L, l_xlib[i].func);
lua_setfield(L, -2, l_xlib[i].name);
}
// KEY MASKS
//* .desc .TP
//* .desc Key modifiers:
//* .desc AnyMod, Control, Lock, Mod1, Mod2, Mod3, Mod4, Mod5, Shift
L_SET_INT("AnyMod", AnyModifier, -3);
L_SET_INT("Control", ControlMask, -3);
L_SET_INT("Lock", LockMask, -3);
L_SET_INT("Mod1", Mod1Mask, -3);
L_SET_INT("Mod2", Mod2Mask, -3);
L_SET_INT("Mod3", Mod3Mask, -3);
L_SET_INT("Mod4", Mod4Mask, -3);
L_SET_INT("Mod5", Mod5Mask, -3);
L_SET_INT("Shift", ShiftMask, -3);
// EVENTS
//* .desc .TP
//* .desc Event types:
//* .desc ButtonPress, ButtonRelease, ClientMessage, ClientMessage, ConfigureRequest, CreateNotify, DestroyNotify, EnterNotify, FocusIn, FocusOut, KeyPress, KeyRelease, LastEvent, LeaveNotify, MapNotify, MapRequest, MappingNotify, MotionNotify, PropertyNotify, ResizeRequest, UnmapNotify, RRScreenChangeNotify
L_SET_INT("ButtonPress", ButtonPress, -3);
L_SET_INT("ButtonRelease", ButtonRelease, -3);
L_SET_INT("ClientMessage", ClientMessage, -3);
L_SET_INT("ConfigureRequest", ConfigureRequest, -3);
L_SET_INT("CreateNotify", CreateNotify, -3);
L_SET_INT("DestroyNotify", DestroyNotify, -3);
L_SET_INT("EnterNotify", EnterNotify, -3);
L_SET_INT("FocusIn", FocusIn, -3);
L_SET_INT("FocusOut", FocusOut, -3);
L_SET_INT("KeyPress", KeyPress, -3);
L_SET_INT("KeyRelease", KeyRelease, -3);
L_SET_INT("LastEvent", LASTEvent, -3);
L_SET_INT("LeaveNotify", LeaveNotify, -3);
L_SET_INT("MapNotify", MapNotify, -3);
L_SET_INT("MapRequest", MapRequest, -3);
L_SET_INT("MappingNotify", MappingNotify, -3);
L_SET_INT("MotionNotify", MotionNotify, -3);
L_SET_INT("PropertyNotify", PropertyNotify, -3);
L_SET_INT("ResizeRequest", ResizeRequest, -3);
L_SET_INT("UnmapNotify", UnmapNotify, -3);
#if L_XRANDR
L_SET_INT("RRScreenChangeNotify", RRScreenChangeNotify, -3);
#endif
// PROPERTY ENUMS
//* .desc .TP
//* .desc Property enums:
//* .desc AnyPropertyType, PropModeAppend, PropModePrepend, PropModeReplace
L_SET_INT("AnyPropertyType", AnyPropertyType, -3);
L_SET_INT("PropModeAppend", PropModeAppend, -3);
L_SET_INT("PropModePrepend", PropModePrepend, -3);
L_SET_INT("PropModeReplace", PropModeReplace, -3);
return 1;
}

106
thornWM Executable file
View file

@ -0,0 +1,106 @@
#!/bin/env luajit
-- This source code form is subject to the terms of the MPL-v2 https://mozilla.org/MPL/2.0/ --
-- Entry point. Handles cli arguments and imports and sets up other modules
local thornWMVersion = "0.1.0"
do
-- local p = arg[0]:match("(.-)[^\\/]+$").."lib/" -- add pathOfScript/lib to path
-- package.path = p .. "?.lua;" .. package.path .. ";./lib/?.lua;./lib/?/init.lua;/usr/share/thornWM/lib/?.lua;/usr/local/share/thornWM/lib/?.lua"
-- package.cpath = p .. "?.so;" .. package.cpath .. ";./lib/?.so;./lib/?/init.so;/usr/share/thornWM/lib/?.so;/usr/local/share/thornWM/lib/?.so"
-- local version = _VERSION:match("%d%.%d") -- add luarocks to path
-- package.cpath = package.cpath .. string.format(";%s/.luarocks/lib/lua/%s/?.so;/usr/local/lib/lua/%s/?.so", os.getenv("HOME"), version, version)
for t,x in pairs({ path = "lua", cpath = "so" }) do
package[t] = table.concat({
package[t],
"./lib/?."..x, "./lib/?/init."..x,
"/usr/share/thornWM/lib/?."..x, "/usr/share/thornWM/lib/?/init.lua."..x,
"/usr/locale/share/thornWM/lib/?."..x, "/usr/local/share/thornWM/lib/?/init.lua."..x,
}, ";")
end
-- package.path = package.path .. "/usr/share/lua/5.1/?.lua;;/home/eiko/.luarocks/share/lua/5.1/?.lua;/home/eiko/.luarocks/share/lua/5.1/?/init.lua;/usr/share/lua/5.1/?/init.lua"
-- package.cpath = package.cpath .. "/usr/lib/lua/5.1/?.so;/usr/lib/lua/5.1/loadall.so;./?.so;/root/.luarocks/lib/lua/5.1/?.so;/home/eiko/.luarocks/lib/lua/5.1/?.so"
end
local Async = require("async")
local cmdArgs = {}
local skipNext
for i=1,#arg do
if skipNext then
skipNext = false
else
local a = arg[i]
if a == "-c" or a == "--config" then
cmdArgs.config = arg[i+1]
skipNext = true
elseif a == "-d" or a == "--display" then
cmdArgs.display = arg[i+1]
skipNext = true
elseif a == "-v" or a == "--version" then -- TODO might want to add a git hash here too
local ok, X = pcall(require, "x")
local xVersion
if ok then
X.open(cmdArgs.display)
X.setErrorHandler(print)
X.setWindowGeometry(213, 0, 0,0, 0)
xVersion = X.getVersion()
else
xVersion = "[Failed to load X]"
end
io.stderr:write(string.format(
"thornWM v%s#%s\n%s %s\n%s\nlua-ev v%d.%d\nLicense: Mozilla Public License 2.0\n",
thornWMVersion, "8a00ff", _VERSION, arg[-1], xVersion, Async.ev.version()
))
os.exit(0)
elseif a == "-h" or a == "--help" then
io.stderr:write((
"Usage: %s [-c configPath] [-d displayNumber] [-h | -v]\n"..
" -h --help\n"..
" -v --version\n"..
" -d --display displayNumber\n"..
" -c --config configPath\n"
):format(arg[0]:match("[^/]+$")))
os.exit(0)
else
io.stderr:write(string.format("thornWM: Unkown argument '%s'\n", a))
os.exit(1)
end
end
end
local X = require("x")
local C = require("c")
local Tree = require("tree")
local Conf = require("conf")
local EWMH = require("ewmh")
local Event = require("event")
_G.inspect = require("inspect")
_G.p = function(...) print(inspect(...)) end
-- Debug = require("thdebug") -- Not generally useful except for development
X.root = assert(X.open(cmdArgs.display))
X.lastError = 0
EWMH.init("thornWM")
Conf.cmdArgs = cmdArgs
Conf.init()
Tree.init()
X.setErrorHandler(function(str, code)
print(str, code)
io.stderr:write(string.format("thornWM: Got X Error: %s\n", str))
X.lastError = code
end)
Async.fd(X.getFD(), Async.ev.READ, Event.handleXEvents)
Async.fd(0, Async.ev.READ, function()
local mes = C.read(0)
local f, err = loadstring(mes)
if not f then return io.stderr:write(err.."\n") end
f()
end)
-- Async.idle(function()
-- end)
C.setNonBlocking(0)
Event.handleXEvents()
Async.loop()