diff options
author | Aylur <[email protected]> | 2024-05-19 02:39:53 +0200 |
---|---|---|
committer | Aylur <[email protected]> | 2024-05-19 02:39:53 +0200 |
commit | 1425b396b08f0e91d45bbd0f92b1309115c7c870 (patch) | |
tree | 8af1a899a14d8a01a9ef50e248c077b48aed25bc /lua/astal |
init 0.1.0
Diffstat (limited to 'lua/astal')
-rw-r--r-- | lua/astal/application.lua | 76 | ||||
-rw-r--r-- | lua/astal/binding.lua | 65 | ||||
-rw-r--r-- | lua/astal/init.lua | 30 | ||||
-rw-r--r-- | lua/astal/process.lua | 93 | ||||
-rw-r--r-- | lua/astal/time.lua | 27 | ||||
-rw-r--r-- | lua/astal/variable.lua | 270 | ||||
-rw-r--r-- | lua/astal/widget.lua | 123 |
7 files changed, 684 insertions, 0 deletions
diff --git a/lua/astal/application.lua b/lua/astal/application.lua new file mode 100644 index 0000000..6146e33 --- /dev/null +++ b/lua/astal/application.lua @@ -0,0 +1,76 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "0.1") + +local AstalLua = Astal.Application:derive("AstalLua") +local request_handler + +function AstalLua:do_request(msg, conn) + if type(request_handler) == "function" then + request_handler(msg, function(request) + Astal.write_sock(conn, request, function(_, res) + Astal.write_sock_finish(res) + end) + end) + else + Astal.Application.do_request(self, msg, conn) + end +end + +local app = AstalLua() + +---@class StartConfig +---@field instance_name? string +---@field gtk_theme? string +---@field icon_theme? string +---@field cursor_theme? string +---@field css? string +---@field hold? boolean +---@field request_handler? fun(msg: string, response: fun(res: string)) + +---@param config StartConfig | nil +---@param callback function | nil +function Astal.Application:start(config, callback) + if config == nil then + config = {} + end + + if config.hold == nil then + config.hold = true + end + + request_handler = config.request_handler + + if config.css then + self:apply_css(config.css) + end + if config.instance_name then + self.instance_name = config.instance_name + end + if config.gtk_theme then + self.gtk_theme = config.gtk_theme + end + if config.icon_theme then + self.icon_theme = config.icon_theme + end + if config.cursor_theme then + self.cursor_theme = config.cursor_theme + end + + app.on_activate = function() + if config.hold then + self:hold() + end + if type(callback) == "function" then + callback() + end + end + + if not app:acquire_socket() then + print('Astal instance "' .. app.instance_name .. '" is already running') + os.exit(1) + end + + self:run(nil) +end + +return app diff --git a/lua/astal/binding.lua b/lua/astal/binding.lua new file mode 100644 index 0000000..264986b --- /dev/null +++ b/lua/astal/binding.lua @@ -0,0 +1,65 @@ +local lgi = require("lgi") +local GObject = lgi.require("GObject", "2.0") + +---@class Binding +---@field emitter object +---@field property? string +---@field transformFn function +local Binding = {} + +---@param emitter object +---@param property? string +---@return Binding +function Binding.new(emitter, property) + return setmetatable({ + emitter = emitter, + property = property, + transformFn = function(v) + return v + end, + }, Binding) +end + +function Binding:__tostring() + local str = "Binding<" .. tostring(self:get()) + if self.property ~= nil then + str = str .. ", " .. self.property + end + return str .. ">" +end + +function Binding:get() + if type(self.emitter.get) == "function" then + return self.transformFn(self.emitter:get()) + end + return self.transformFn(self.emitter[self.property]) +end + +---@param transform fun(value: any): any +---@return Binding +function Binding:as(transform) + local b = Binding.new(self.emitter, self.property) + b.transformFn = function(v) + return transform(self.transformFn(v)) + end + return b +end + +---@param callback fun(value: any) +---@return function +function Binding:subscribe(callback) + if type(self.emitter.subscribe) == "function" then + return self.emitter:subscribe(function() + callback(self:get()) + end) + end + local id = self.emitter.on_notify:connect(function() + callback(self:get()) + end, self.property, false) + return function() + GObject.signal_handler_disconnect(self.emitter, id) + end +end + +Binding.__index = Binding +return Binding diff --git a/lua/astal/init.lua b/lua/astal/init.lua new file mode 100644 index 0000000..c247533 --- /dev/null +++ b/lua/astal/init.lua @@ -0,0 +1,30 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "0.1") +local Gtk = lgi.require("Gtk", "3.0") +local GObject = lgi.require("GObject", "2.0") +local Widget = require("astal.widget") +local Variable = require("astal.variable") +local Binding = require("astal.binding") +local App = require("astal.application") +local Process = require("astal.process") +local Time = require("astal.time") + +return { + App = App, + Variable = Variable, + Widget = Widget, + bind = Binding.new, + interval = Time.interval, + timeout = Time.timeout, + idle = Time.idle, + subprocess = Process.subprocess, + exec = Process.exec, + exec_async = Process.exec_async, + + Astal = Astal, + Gtk = Gtk, + GObject = GObject, + GLib = lgi.require("GLib", "2.0"), + Gio = lgi.require("Gio", "2.0"), + require = lgi.require, +} diff --git a/lua/astal/process.lua b/lua/astal/process.lua new file mode 100644 index 0000000..804fd0f --- /dev/null +++ b/lua/astal/process.lua @@ -0,0 +1,93 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "0.1") + +local M = {} + +local defualt_proc_args = function(on_stdout, on_stderr) + if on_stdout == nil then + on_stdout = function(out) + io.stdout:write(tostring(out) .. "\n") + return tostring(out) + end + end + + if on_stderr == nil then + on_stderr = function(err) + io.stderr:write(tostring(err) .. "\n") + return tostring(err) + end + end + + return on_stdout, on_stderr +end + +---@param commandline string | string[] +---@param on_stdout? fun(out: string): nil +---@param on_stderr? fun(err: string): nil +---@return { kill: function } | nil proc +function M.subprocess(commandline, on_stdout, on_stderr) + local out, err = defualt_proc_args(on_stdout, on_stderr) + local proc, fail + if type(commandline) == "table" then + proc, fail = Astal.Process.subprocessv(commandline) + else + proc, fail = Astal.Process.subprocess(commandline) + end + if fail ~= nil then + err(fail) + return nil + end + proc.on_stdout = function(_, str) + out(str) + end + proc.on_stderr = function(_, str) + err(str) + end + return proc +end + +---@generic T +---@param commandline string | string[] +---@param on_stdout? fun(out: string): T +---@param on_stderr? fun(err: string): T +---@return T +function M.exec(commandline, on_stdout, on_stderr) + local out, err = defualt_proc_args(on_stdout, on_stderr) + local stdout, stderr + if type(commandline) == "table" then + stdout, stderr = Astal.Process.execv(commandline) + else + stdout, stderr = Astal.Process.exec(commandline) + end + if stderr then + return err(stderr) + end + return out(stdout) +end + +---@param commandline string | string[] +---@param on_stdout? fun(out: string): nil +---@param on_stderr? fun(err: string): nil +---@return { kill: function } | nil proc +function M.exec_async(commandline, on_stdout, on_stderr) + local out, err = defualt_proc_args(on_stdout, on_stderr) + local proc, fail + if type(commandline) == "table" then + proc, fail = Astal.Process.exec_asyncv(commandline) + else + proc, fail = Astal.Process.exec_async(commandline) + end + if fail ~= nil then + err(fail) + return nil + end + proc.on_stdout = function(_, str) + out(str) + end + proc.on_stderr = function(_, str) + err(str) + end + return proc +end + +return M diff --git a/lua/astal/time.lua b/lua/astal/time.lua new file mode 100644 index 0000000..f4e2b81 --- /dev/null +++ b/lua/astal/time.lua @@ -0,0 +1,27 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "0.1") +local GObject = lgi.require("GObject", "2.0") + +local M = {} + +---@param interval number +---@param fn function +---@return { cancel: function, on_now: function } +function M.interval(interval, fn) + return Astal.Time.interval(interval, GObject.Closure(fn)) +end + +---@param timeout number +---@param fn function +---@return { cancel: function, on_now: function } +function M.timeout(timeout, fn) + return Astal.Time.timeout(timeout, GObject.Closure(fn)) +end + +---@param fn function +---@return { cancel: function, on_now: function } +function M.idle(fn) + return Astal.Time.idle(GObject.Closure(fn)) +end + +return M diff --git a/lua/astal/variable.lua b/lua/astal/variable.lua new file mode 100644 index 0000000..baa2d69 --- /dev/null +++ b/lua/astal/variable.lua @@ -0,0 +1,270 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "0.1") +local GObject = lgi.require("GObject", "2.0") +local Binding = require("astal.binding") +local Time = require("astal.time") +local Process = require("astal.process") + +---@class Variable +---@field private variable object +---@field private err_handler? function +---@field private _value any +---@field private _poll? object +---@field private _watch? object +---@field private poll_interval number +---@field private poll_exec? string[] | string +---@field private poll_transform? fun(next: any, prev: any): any +---@field private poll_fn? function +---@field private watch_transform? fun(next: any, prev: any): any +---@field private watch_exec? string[] | string +local Variable = {} +Variable.__index = Variable + +---@param value any +---@return Variable +function Variable.new(value) + local v = Astal.VariableBase() + local variable = setmetatable({ + variable = v, + _value = value, + }, Variable) + v.on_dropped = function() + variable:stop_watch() + variable:stop_watch() + end + v.on_error = function(_, err) + if variable.err_handler then + variable.err_handler(err) + end + end + return variable +end + +---@param transform function +---@return Binding +function Variable:__call(transform) + if transform == nil then + transform = function(v) + return v + end + return Binding.new(self) + end + return Binding.new(self):as(transform) +end + +function Variable:__tostring() + return "Variable<" .. tostring(self:get()) .. ">" +end + +function Variable:get() + return self._value or nil +end + +function Variable:set(value) + if value ~= self:get() then + self._value = value + self.variable:emit_changed() + end +end + +function Variable:start_poll() + if self._poll ~= nil then + return + end + + if self.poll_fn then + self._poll = Time.interval(self.poll_interval, function() + self:set(self.poll_fn(self:get())) + end) + elseif self.poll_exec then + self._poll = Time.interval(self.poll_interval, function() + Process.exec_async(self.poll_exec, function(out) + self:set(self.poll_transform(out, self:get())) + end, function(err) + self.variable.emit_error(err) + end) + end) + end +end + +function Variable:start_watch() + if self._watch then + return + end + + self._watch = Process.subprocess(self.watch_exec, function(out) + self:set(self.watch_transform(out, self:get())) + end, function(err) + self.variable.emit_error(err) + end) +end + +function Variable:stop_poll() + if self._poll then + self._poll.cancel() + end + self._poll = nil +end + +function Variable:stop_watch() + if self._watch then + self._watch.kill() + end + self._watch = nil +end + +function Variable:is_polling() + return self._poll ~= nil +end + +function Variable:is_watching() + return self._watch ~= nil +end + +function Variable:drop() + self.variable.emit_dropped() + self.variable.run_dispose() +end + +---@param callback function +---@return Variable +function Variable:on_dropped(callback) + self.variable.on_dropped = callback + return self +end + +---@param callback function +---@return Variable +function Variable:on_error(callback) + self.err_handler = nil + self.variable.on_eror = function(_, err) + callback(err) + end + return self +end + +---@param callback fun(value: any) +---@return function +function Variable:subscribe(callback) + local id = self.variable.on_changed:connect(function() + callback(self:get()) + end) + return function() + GObject.signal_handler_disconnect(self.variable, id) + end +end + +---@param interval number +---@param exec string | string[] | function +---@param transform? fun(next: any, prev: any): any +function Variable:poll(interval, exec, transform) + if transform == nil then + transform = function(next) + return next + end + end + self:stop_poll() + self.poll_interval = interval + self.poll_transform = transform + + if type(exec) == "function" then + self.poll_fn = exec + self.poll_exec = nil + else + self.poll_exec = exec + self.poll_fn = nil + end + self:start_poll() + return self +end + +---@param exec string | string[] +---@param transform? fun(next: any, prev: any): any +function Variable:watch(exec, transform) + if transform == nil then + transform = function(next) + return next + end + end + self:stop_poll() + self.watch_exec = exec + self.watch_transform = transform + self:start_watch() + return self +end + +---@param object object | table[] +---@param sigOrFn string | fun(...): any +---@param callback fun(...): any +---@return Variable +function Variable:observe(object, sigOrFn, callback) + local f + if type(sigOrFn) == "function" then + f = sigOrFn + elseif type(callback) == "function" then + f = callback + else + f = function() + return self:get() + end + end + local set = function(_, ...) + self:set(f(...)) + end + + if type(sigOrFn) == "string" then + object["on_" .. sigOrFn]:connect(set) + else + for _, obj in ipairs(object) do + obj[1]["on_" .. obj[2]]:connect(set) + end + end + return self +end + +---@param deps Variable | (Binding | Variable)[] +---@param transform fun(...): any +---@return Variable +function Variable.derive(deps, transform) + if getmetatable(deps) == Variable then + local var = Variable.new(transform(deps:get())) + deps:subscribe(function(v) + var:set(transform(v)) + end) + return var + end + + for i, var in ipairs(deps) do + if getmetatable(var) == Variable then + deps[i] = Binding.new(var) + end + end + + local update = function() + local params = {} + for _, binding in ipairs(deps) do + table.insert(params, binding:get()) + end + return transform(table.unpack(params)) + end + + local var = Variable.new(update()) + + local unsubs = {} + for _, b in ipairs(deps) do + table.insert(unsubs, b:subscribe(update)) + end + + var.variable.on_dropped = function() + for _, unsub in ipairs(unsubs) do + var:set(unsub()) + end + end + return var +end + +return setmetatable(Variable, { + __call = function(_, v) + return Variable.new(v) + end, +}) diff --git a/lua/astal/widget.lua b/lua/astal/widget.lua new file mode 100644 index 0000000..ade000e --- /dev/null +++ b/lua/astal/widget.lua @@ -0,0 +1,123 @@ +local lgi = require("lgi") +local Astal = lgi.require("Astal", "0.1") +local Gtk = lgi.require("Gtk", "3.0") +local GObject = lgi.require("GObject", "2.0") +local Binding = require("astal.binding") + +local function filter(tbl, fn) + local copy = {} + for key, value in pairs(tbl) do + if fn(value, key) then + copy[key] = value + end + end + return copy +end + +Gtk.Widget._attribute.css = { + get = Astal.widget_get_css, + set = Astal.widget_set_css, +} + +Gtk.Widget._attribute.class_name = { + get = function(self) + local result = "" + local strings = Astal.widget_set_class_names(self) + for i, str in ipairs(strings) do + result = result .. str + if i < #strings then + result = result .. " " + end + end + return result + end, + set = function(self, class_name) + local names = {} + for word in class_name:gmatch("%S+") do + table.insert(names, word) + end + Astal.widget_set_class_names(self, names) + end, +} + +Gtk.Widget._attribute.cursor = { + get = Astal.widget_get_cursor, + set = Astal.widget_set_cursor, +} + +Astal.Box._attribute.children = { + get = Astal.Box.get_children, + set = Astal.Box.set_children, +} + +local function astalify(ctor) + function ctor:hook(object, signalOrCallback, callback) + if type(object.subscribe) == "function" then + local unsub = object.subscribe(function(...) + signalOrCallback(self, ...) + end) + self.on_destroy = unsub + return + end + local id = object["on_" .. signalOrCallback](function(_, ...) + callback(self, ...) + end) + self.on_destroy = function() + GObject.signal_handler_disconnect(object, id) + end + end + + return function(tbl) + local bindings = {} + local setup = tbl.setup + + local visible + if type(tbl.visible) == "boolean" then + visible = tbl.visible + else + visible = true + end + + local props = filter(tbl, function(_, key) + return key ~= "visible" and key ~= "setup" + end) + + for prop, value in pairs(props) do + if getmetatable(value) == Binding then + bindings[prop] = value + props[prop] = value:get() + end + end + + local widget = ctor(props) + + for prop, binding in pairs(bindings) do + widget.on_destroy = binding:subscribe(function(v) + widget[prop] = v + end) + end + + widget.visible = visible + if type(setup) == "function" then + setup(widget) + end + return widget + end +end + +local Widget = { + astalify = astalify, + Box = astalify(Astal.Box), + Button = astalify(Astal.Button), + CenterBox = astalify(Astal.CenterBox), + Label = astalify(Gtk.Label), + Icon = astalify(Astal.Icon), + Window = astalify(Astal.Window), + EventBox = astalify(Astal.EventBox), +} + +return setmetatable(Widget, { + __call = function(_, ctor) + return astalify(ctor) + end, +}) |