summaryrefslogtreecommitdiff
path: root/lua
diff options
context:
space:
mode:
authorAylur <[email protected]>2024-05-19 02:39:53 +0200
committerAylur <[email protected]>2024-05-19 02:39:53 +0200
commit1425b396b08f0e91d45bbd0f92b1309115c7c870 (patch)
tree8af1a899a14d8a01a9ef50e248c077b48aed25bc /lua
init 0.1.0
Diffstat (limited to 'lua')
-rw-r--r--lua/astal/application.lua76
-rw-r--r--lua/astal/binding.lua65
-rw-r--r--lua/astal/init.lua30
-rw-r--r--lua/astal/process.lua93
-rw-r--r--lua/astal/time.lua27
-rw-r--r--lua/astal/variable.lua270
-rw-r--r--lua/astal/widget.lua123
-rwxr-xr-xlua/sample.lua81
-rw-r--r--lua/stylua.toml3
9 files changed, 768 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,
+})
diff --git a/lua/sample.lua b/lua/sample.lua
new file mode 100755
index 0000000..2c76af6
--- /dev/null
+++ b/lua/sample.lua
@@ -0,0 +1,81 @@
+#!/usr/bin/env lua
+-- imports
+local astal = require("astal.init")
+local Widget, Variable, App, bind = astal.Widget, astal.Variable, astal.App, astal.bind
+
+-- state
+local player = astal.require("Playerctl").Player.new("spotify")
+
+local title = Variable(player:get_title()):observe(player, "metadata", function()
+ return player:get_title()
+end)
+
+local rnd = Variable(1):poll(1000, function()
+ return math.random(1, 10)
+end)
+
+-- ui
+local Bar = function(monitor)
+ return Widget.Window({
+ application = App,
+ id = "bar",
+ name = "bar",
+ monitor = monitor,
+ anchor = astal.Astal.WindowAnchor.BOTTOM
+ + astal.Astal.WindowAnchor.LEFT
+ + astal.Astal.WindowAnchor.RIGHT,
+ exclusivity = "EXCLUSIVE",
+
+ Widget.CenterBox({
+ class_name = "bar",
+ start_widget = Widget.Label({
+ valign = "CENTER",
+ label = "Welcome to Astal.lua",
+ }),
+ center_widget = Widget.Box({
+ children = bind(rnd):as(function(n)
+ local children = {}
+ for i = 1, n, 1 do
+ table.insert(
+ children,
+ Widget.Button({
+ label = tostring(i),
+ on_clicked = function()
+ print(i)
+ end,
+ })
+ )
+ end
+ return children
+ end),
+ }),
+ end_widget = Widget.Label({
+ valign = "CENTER",
+ label = bind(title),
+ }),
+ }),
+ })
+end
+
+-- css
+local css = [[
+.bar button {
+ color: blue;
+}
+]]
+
+-- main
+App:start({
+ request_handler = function(msg, res)
+ if msg == "quit" then
+ os.exit(0)
+ end
+ if msg == "inspector" then
+ res(App:inspector())
+ end
+ res("hi")
+ end,
+ css = css,
+}, function()
+ Bar(0)
+end)
diff --git a/lua/stylua.toml b/lua/stylua.toml
new file mode 100644
index 0000000..d4a4951
--- /dev/null
+++ b/lua/stylua.toml
@@ -0,0 +1,3 @@
+indent_type = "Spaces"
+indent_width = 4
+column_width = 100