commit d2743fe41d8d2a4cc463ce79849ccf9fb579e1e9 Author: nazrin Date: Fri May 23 15:41:23 2025 +0000 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad3799b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +lib/*.so + +docs/*.3 +docs/*.1 + +misc/ + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a612ad9 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..de70d07 --- /dev/null +++ b/README.md @@ -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) + diff --git a/config.lua b/config.lua new file mode 100644 index 0000000..45c60ca --- /dev/null +++ b/config.lua @@ -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 diff --git a/docs/genDocs b/docs/genDocs new file mode 100755 index 0000000..7f252ad --- /dev/null +++ b/docs/genDocs @@ -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") diff --git a/lib/async.lua b/lib/async.lua new file mode 100644 index 0000000..e147d12 --- /dev/null +++ b/lib/async.lua @@ -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 diff --git a/lib/conf.lua b/lib/conf.lua new file mode 100644 index 0000000..8b455df --- /dev/null +++ b/lib/conf.lua @@ -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 diff --git a/lib/event.lua b/lib/event.lua new file mode 100644 index 0000000..74ff41a --- /dev/null +++ b/lib/event.lua @@ -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 diff --git a/lib/ewmh.lua b/lib/ewmh.lua new file mode 100644 index 0000000..05e7930 --- /dev/null +++ b/lib/ewmh.lua @@ -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 diff --git a/lib/tree/init.lua b/lib/tree/init.lua new file mode 100644 index 0000000..d97539d --- /dev/null +++ b/lib/tree/init.lua @@ -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 + diff --git a/lib/tree/tag.lua b/lib/tree/tag.lua new file mode 100644 index 0000000..1841395 --- /dev/null +++ b/lib/tree/tag.lua @@ -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 + diff --git a/lib/tree/tilers.lua b/lib/tree/tilers.lua new file mode 100644 index 0000000..d457f93 --- /dev/null +++ b/lib/tree/tilers.lua @@ -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 + + diff --git a/lib/tree/window.lua b/lib/tree/window.lua new file mode 100644 index 0000000..4a38a6e --- /dev/null +++ b/lib/tree/window.lua @@ -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 + diff --git a/lib/util.lua b/lib/util.lua new file mode 100644 index 0000000..8e24879 --- /dev/null +++ b/lib/util.lua @@ -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 + diff --git a/makefile b/makefile new file mode 100644 index 0000000..0a8386e --- /dev/null +++ b/makefile @@ -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 diff --git a/mynd.png.webp b/mynd.png.webp new file mode 100644 index 0000000..658a218 Binary files /dev/null and b/mynd.png.webp differ diff --git a/scripts/test-x.lua b/scripts/test-x.lua new file mode 100755 index 0000000..3b31e18 --- /dev/null +++ b/scripts/test-x.lua @@ -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() diff --git a/scripts/xephyr.sh b/scripts/xephyr.sh new file mode 100755 index 0000000..c0a888a --- /dev/null +++ b/scripts/xephyr.sh @@ -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 + diff --git a/src/c.c b/src/c.c new file mode 100644 index 0000000..9a0f3ab --- /dev/null +++ b/src/c.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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; +} diff --git a/src/common.h b/src/common.h new file mode 100644 index 0000000..f3aa682 --- /dev/null +++ b/src/common.h @@ -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 + diff --git a/src/x.c b/src/x.c new file mode 100644 index 0000000..4b5a3d5 --- /dev/null +++ b/src/x.c @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef L_XRANDR +#include +#else +#include +#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; +} diff --git a/thornWM b/thornWM new file mode 100755 index 0000000..4dc06c5 --- /dev/null +++ b/thornWM @@ -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()