local lgi = require("lgi") local Astal = lgi.require("AstalIO", "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 table ---@field private err_handler? function ---@field private _value any ---@field private _poll? table ---@field private _watch? table ---@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 ---@overload fun(transform?: fun(v: any): any): Binding 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_poll() end v.on_error = function(_, err) if variable.err_handler then variable.err_handler(err) end end return variable end ---@param transform? fun(v: any): any ---@return Binding function Variable:__call(transform) if type(transform) == "nil" then return Binding.new(self) else return Binding.new(self):as(transform) end end function Variable:__tostring() return "Variable<" .. tostring(self:get()) .. ">" end ---@return any function Variable:get() return self._value end ---@param value any ---@return nil function Variable:set(value) if value ~= self:get() then self._value = value self.variable:emit_changed() end end function Variable:is_polling() return self._poll ~= nil end function Variable:is_watching() return self._watch ~= nil end function Variable:start_poll() if self:is_polling() 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, err) if err ~= nil then return self.variable.emit_error(err) else self:set(self.poll_transform(out, self:get())) end end) end) end end function Variable:start_watch() if self:is_watching() 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:is_polling() then self._poll.cancel() end self._poll = nil end function Variable:stop_watch() if self:is_watching() then self._watch.kill() end self._watch = nil end function Variable:drop() self.variable.emit_dropped() 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_error = 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) transform = transform or function(next) return next 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 ---@return Variable function Variable:watch(exec, transform) transform = transform or function(next) return next end self:stop_watch() self.watch_exec = exec self.watch_transform = transform self:start_watch() return self end ---@param object table | 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) transform = transform or function(...) return { ... } end 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] = var() end end local function update() local params = {} for i, binding in ipairs(deps) do params[i] = binding:get() end return transform(table.unpack(params, 1, #deps)) end local var = Variable.new(update()) local unsubs = {} for i, b in ipairs(deps) do unsubs[i] = b:subscribe(function() var:set(update()) end) end var.variable.on_dropped = function() for _, unsub in ipairs(unsubs) do unsub() end end return var end return setmetatable(Variable, { __call = function(_, v) return Variable.new(v) end, })