diff options
Diffstat (limited to 'lib/astal/io')
-rw-r--r-- | lib/astal/io/application.vala | 186 | ||||
-rw-r--r-- | lib/astal/io/cli.vala | 104 | ||||
-rw-r--r-- | lib/astal/io/config.vala.in | 6 | ||||
-rw-r--r-- | lib/astal/io/file.vala | 98 | ||||
-rw-r--r-- | lib/astal/io/gir.py | 58 | ||||
-rw-r--r-- | lib/astal/io/meson.build | 106 | ||||
-rw-r--r-- | lib/astal/io/process.vala | 172 | ||||
-rw-r--r-- | lib/astal/io/time.vala | 111 | ||||
-rw-r--r-- | lib/astal/io/variable.vala | 198 | ||||
-rw-r--r-- | lib/astal/io/version | 1 |
10 files changed, 1040 insertions, 0 deletions
diff --git a/lib/astal/io/application.vala b/lib/astal/io/application.vala new file mode 100644 index 0000000..09b61b5 --- /dev/null +++ b/lib/astal/io/application.vala @@ -0,0 +1,186 @@ +namespace AstalIO { +public errordomain AppError { + NAME_OCCUPIED, + TAKEOVER_FAILED, +} + +/** + * This interface is used as a placeholder for the Astal Application class. + * It is not meant to be used by consumers. + */ +public interface Application : Object { + public abstract void quit() throws Error; + public abstract void inspector() throws Error; + public abstract void toggle_window(string window) throws Error; + + public abstract string instance_name { owned get; construct set; } + public abstract void acquire_socket() throws Error; + public virtual void request(string msg, SocketConnection conn) throws Error { + write_sock.begin(conn, @"missing response implementation on $instance_name"); + } +} + +/** + * Starts a [[email protected]] and binds `XDG_RUNTIME_DIR/astal/<instance_name>.sock`. + * This socket is then used by the astal cli. Not meant for public usage, but for [[email protected]_socket]. + */ +public SocketService acquire_socket(Application app, out string sock) throws Error { + var name = app.instance_name; + foreach (var instance in get_instances()) { + if (instance == name) { + throw new AppError.NAME_OCCUPIED(@"$name is occupied"); + } + } + + var rundir = Environment.get_user_runtime_dir(); + var dir = @"$rundir/astal"; + var path = @"$dir/$name.sock"; + sock = path; + + if (!FileUtils.test(dir, FileTest.IS_DIR)) { + File.new_for_path(path).make_directory_with_parents(null); + } + + if (FileUtils.test(path, FileTest.EXISTS)) { + try { + File.new_for_path(path).delete(null); + } catch (Error err) { + throw new AppError.TAKEOVER_FAILED("could not delete previous socket"); + } + } + + var service = new SocketService(); + service.add_address( + new UnixSocketAddress(path), + SocketType.STREAM, + SocketProtocol.DEFAULT, + null, + null + ); + + service.incoming.connect((conn) => { + read_sock.begin(conn, (_, res) => { + try { + string message = read_sock.end(res); + app.request(message != null ? message.strip() : "", conn); + } catch (Error err) { + critical(err.message); + } + }); + return false; + }); + + return service; +} + +/** + * Get a list of running Astal.Application instances. + * It is the equivalent of `astal --list`. + */ +public static List<string> get_instances() { + var list = new List<string>(); + var prefix = "io.Astal."; + + try { + DBusImpl dbus = Bus.get_proxy_sync( + BusType.SESSION, + "org.freedesktop.DBus", + "/org/freedesktop/DBus" + ); + + foreach (var busname in dbus.list_names()) { + if (busname.has_prefix(prefix)) + list.append(busname.replace(prefix, "")); + } + } catch (Error err) { + critical(err.message); + } + + return list; +} + +/** + * Quit an an Astal instances. + * It is the equivalent of `astal --quit -i instance`. + */ +public static void quit_instance(string instance) throws Error { + IApplication proxy = Bus.get_proxy_sync( + BusType.SESSION, + "io.Astal." + instance, + "/io/Astal/Application" + ); + + proxy.quit(); +} + +/** + * Open the Gtk debug tool of an an Astal instances. + * It is the equivalent of `astal --inspector -i instance`. + */ +public static void open_inspector(string instance) throws Error { + IApplication proxy = Bus.get_proxy_sync( + BusType.SESSION, + "io.Astal." + instance, + "/io/Astal/Application" + ); + + proxy.inspector(); +} + +/** + * Toggle a Window of an Astal instances. + * It is the equivalent of `astal -i instance --toggle window`. + */ +public static void toggle_window_by_name(string instance, string window) throws Error { + IApplication proxy = Bus.get_proxy_sync( + BusType.SESSION, + "io.Astal." + instance, + "/io/Astal/Application" + ); + + proxy.toggle_window(window); +} + +/** + * Send a message to an Astal instances. + * It is the equivalent of `astal -i instance content of the message`. + */ +public static string send_message(string instance, string msg) throws Error { + var rundir = Environment.get_user_runtime_dir(); + var socket_path = @"$rundir/astal/$instance.sock"; + var client = new SocketClient(); + + var conn = client.connect(new UnixSocketAddress(socket_path), null); + conn.output_stream.write(msg.concat("\x04").data); + + var stream = new DataInputStream(conn.input_stream); + return stream.read_upto("\x04", -1, null, null); +} + +/** + * Read the socket of an Astal.Application instance. + */ +public async string read_sock(SocketConnection conn) throws IOError { + var stream = new DataInputStream(conn.input_stream); + return yield stream.read_upto_async("\x04", -1, Priority.DEFAULT, null, null); +} + +/** + * Write the socket of an Astal.Application instance. + */ +public async void write_sock(SocketConnection conn, string response) throws IOError { + yield conn.output_stream.write_async(@"$response\x04".data, Priority.DEFAULT); +} + +[DBus (name="io.Astal.Application")] +private interface IApplication : DBusProxy { + public abstract void quit() throws GLib.Error; + public abstract void inspector() throws GLib.Error; + public abstract void toggle_window(string window) throws GLib.Error; +} + +[DBus (name="org.freedesktop.DBus")] +private interface DBusImpl : DBusProxy { + public abstract string[] list_names() throws Error; +} +} diff --git a/lib/astal/io/cli.vala b/lib/astal/io/cli.vala new file mode 100644 index 0000000..f69cf0b --- /dev/null +++ b/lib/astal/io/cli.vala @@ -0,0 +1,104 @@ +static bool version; +static bool help; +static bool list; +static bool quit; +static bool inspector; +static string? toggle_window; +static string? instance_name; + +const OptionEntry[] options = { + { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null }, + { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null }, + { "list", 'l', OptionFlags.NONE, OptionArg.NONE, ref list, null, null }, + { "quit", 'q', OptionFlags.NONE, OptionArg.NONE, ref quit, null, null }, + { "inspector", 'I', OptionFlags.NONE, OptionArg.NONE, ref inspector, null, null }, + { "toggle-window", 't', OptionFlags.NONE, OptionArg.STRING, ref toggle_window, null, null }, + { "instance", 'i', OptionFlags.NONE, OptionArg.STRING, ref instance_name, null, null }, + { null }, +}; + +int err(string msg) { + var red = "\x1b[31m"; + var r = "\x1b[0m"; + printerr(@"$(red)error: $(r)$msg"); + return 1; +} + +int main(string[] argv) { + try { + var opts = new OptionContext(); + opts.add_main_entries(options, null); + opts.set_help_enabled(false); + opts.set_ignore_unknown_options(false); + opts.parse(ref argv); + } catch (OptionError e) { + return err(e.message); + } + + if (help) { + print("Client for Astal.Application instances\n\n"); + print("Usage:\n"); + print(" %s [flags] message\n\n", argv[0]); + print("Flags:\n"); + print(" -h, --help Print this help and exit\n"); + print(" -v, --version Print version number and exit\n"); + print(" -l, --list List running Astal instances and exit\n"); + print(" -q, --quit Quit an Astal.Application instance\n"); + print(" -i, --instance Instance name of the Astal instance\n"); + print(" -I, --inspector Open up Gtk debug tool\n"); + print(" -t, --toggle-window Show or hide a window\n"); + return 0; + } + + if (version) { + print(AstalIO.VERSION); + return 0; + } + + if (instance_name == null) + instance_name = "astal"; + + if (list) { + foreach (var name in AstalIO.get_instances()) + print(@"$name\n"); + + return 0; + } + + try { + if (quit) { + AstalIO.quit_instance(instance_name); + return 0; + } + + if (inspector) { + AstalIO.open_inspector(instance_name); + return 0; + } + + if (toggle_window != null) { + AstalIO.toggle_window_by_name(instance_name, toggle_window); + return 0; + } + } catch (DBusError.SERVICE_UNKNOWN e) { + return err(@"there is no \"$instance_name\" instance runnning"); + } catch (Error e) { + return err(e.message); + } + + var request = ""; + for (var i = 1; i < argv.length; ++i) { + request = request.concat(" ", argv[i]); + } + + try { + var reply = AstalIO.send_message(instance_name, request); + print("%s\n", reply); + } catch (IOError.NOT_FOUND e) { + return err(@"there is no \"$instance_name\" instance runnning"); + } catch (Error e) { + return err(e.message); + } + + return 0; +} diff --git a/lib/astal/io/config.vala.in b/lib/astal/io/config.vala.in new file mode 100644 index 0000000..fe1e450 --- /dev/null +++ b/lib/astal/io/config.vala.in @@ -0,0 +1,6 @@ +namespace AstalIO { + public const int MAJOR_VERSION = @MAJOR_VERSION@; + public const int MINOR_VERSION = @MINOR_VERSION@; + public const int MICRO_VERSION = @MICRO_VERSION@; + public const string VERSION = "@VERSION@"; +} diff --git a/lib/astal/io/file.vala b/lib/astal/io/file.vala new file mode 100644 index 0000000..57b6dc0 --- /dev/null +++ b/lib/astal/io/file.vala @@ -0,0 +1,98 @@ +namespace AstalIO { +/** + * Read the contents of a file synchronously. + */ +public string read_file(string path) { + var str = ""; + try { + FileUtils.get_contents(path, out str, null); + } catch (Error error) { + critical(error.message); + } + return str; +} + +/** + * Read the contents of a file asynchronously. + */ +public async string read_file_async(string path) throws Error { + uint8[] content; + yield File.new_for_path(path).load_contents_async(null, out content, null); + return (string)content; +} + +/** + * Write content to a file synchronously. + */ +public void write_file(string path, string content) { + try { + FileUtils.set_contents(path, content); + } catch (Error error) { + critical(error.message); + } +} + +/** + * Write content to a file asynchronously. + */ +public async void write_file_async(string path, string content) throws Error { + yield File.new_for_path(path).replace_contents_async( + content.data, + null, + false, + FileCreateFlags.REPLACE_DESTINATION, + null, + null); +} + +/** + * Monitor a file for changes. If the path is a directory, monitor it recursively. + * The callback will be called passed two parameters: the path of the file + * that changed and an [[email protected]] indicating the reason. + */ +public FileMonitor? monitor_file(string path, Closure callback) { + try { + var file = File.new_for_path(path); + var mon = file.monitor(FileMonitorFlags.NONE); + + mon.changed.connect((file, _file, event) => { + var f = Value(Type.STRING); + var e = Value(Type.INT); + var ret = Value(Type.POINTER); + + f.set_string(file.get_path()); + e.set_int(event); + + callback.invoke(ref ret, { f, e }); + }); + + if (FileUtils.test(path, FileTest.IS_DIR)) { + var enumerator = file.enumerate_children("standard::*", + FileQueryInfoFlags.NONE, null); + + var i = enumerator.next_file(null); + while (i != null) { + if (i.get_file_type() == FileType.DIRECTORY) { + var filepath = file.get_child(i.get_name()).get_path(); + if (filepath != null) { + var m = monitor_file(path, callback); + mon.notify["cancelled"].connect(() => { + m.cancel(); + }); + } + } + i = enumerator.next_file(null); + } + } + + mon.ref(); + mon.notify["cancelled"].connect(() => { + mon.unref(); + }); + return mon; + } catch (Error error) { + critical(error.message); + return null; + } +} +} diff --git a/lib/astal/io/gir.py b/lib/astal/io/gir.py new file mode 100644 index 0000000..9ef680f --- /dev/null +++ b/lib/astal/io/gir.py @@ -0,0 +1,58 @@ +""" +Vala's generated gir does not contain comments, +so we use valadoc to generate them. However, they are formatted +for valadoc and not gi-docgen so we need to fix it. +""" + +import xml.etree.ElementTree as ET +import html +import sys +import subprocess + + +def fix_gir(name: str, gir: str, out: str): + namespaces = { + "": "http://www.gtk.org/introspection/core/1.0", + "c": "http://www.gtk.org/introspection/c/1.0", + "glib": "http://www.gtk.org/introspection/glib/1.0", + } + for prefix, uri in namespaces.items(): + ET.register_namespace(prefix, uri) + + tree = ET.parse(gir) + root = tree.getroot() + + for doc in root.findall(".//doc", namespaces): + if doc.text: + doc.text = ( + html.unescape(doc.text).replace("<para>", "").replace("</para>", "") + ) + + if (inc := root.find("c:include", namespaces)) is not None: + inc.set("name", f"{name}.h") + else: + print("no c:include tag found", file=sys.stderr) + exit(1) + + tree.write(out, encoding="utf-8", xml_declaration=True) + + +def valadoc(name: str, gir: str, args: list[str]): + cmd = ["valadoc", "-o", "docs", "--package-name", name, "--gir", gir, *args] + try: + subprocess.run(cmd, check=True, text=True, capture_output=True) + except subprocess.CalledProcessError as e: + print(e.stderr, file=sys.stderr) + exit(1) + + +if __name__ == "__main__": + name = sys.argv[1] + in_out = sys.argv[2].split(":") + args = sys.argv[3:] + + gir = in_out[0] + out = in_out[1] if len(in_out) > 1 else gir + + valadoc(name, gir, args) + fix_gir(name, gir, out) diff --git a/lib/astal/io/meson.build b/lib/astal/io/meson.build new file mode 100644 index 0000000..023dece --- /dev/null +++ b/lib/astal/io/meson.build @@ -0,0 +1,106 @@ +project( + 'astal-io', + 'vala', + 'c', + version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), + meson_version: '>= 0.62.0', + default_options: [ + 'warning_level=2', + 'werror=false', + 'c_std=gnu11', + ], +) + +version_split = meson.project_version().split('.') +api_version = version_split[0] + '.' + version_split[1] +gir = 'AstalIO-' + api_version + '.gir' +typelib = 'AstalIO-' + api_version + '.typelib' +libdir = get_option('prefix') / get_option('libdir') +pkgdatadir = get_option('prefix') / get_option('datadir') / 'astal' + +config = configure_file( + input: 'config.vala.in', + output: 'config.vala', + configuration: { + 'VERSION': meson.project_version(), + 'MAJOR_VERSION': version_split[0], + 'MINOR_VERSION': version_split[1], + 'MICRO_VERSION': version_split[2], + }, +) + +deps = [ + dependency('glib-2.0'), + dependency('gio-unix-2.0'), + dependency('gobject-2.0'), + dependency('gio-2.0'), +] + +sources = [config] + files( + 'application.vala', + 'file.vala', + 'process.vala', + 'time.vala', + 'variable.vala', +) + +lib = library( + meson.project_name(), + sources, + dependencies: deps, + vala_args: ['--vapi-comments'], + vala_header: meson.project_name() + '.h', + vala_vapi: meson.project_name() + '-' + api_version + '.vapi', + version: meson.project_version(), + install: true, + install_dir: [true, true, true], +) + +pkgs = [] +foreach dep : deps + pkgs += ['--pkg=' + dep.name()] +endforeach + +gir_tgt = custom_target( + gir, + command: [find_program('python3'), files('gir.py'), meson.project_name(), gir] + + pkgs + + sources, + input: sources, + depends: lib, + output: gir, + install: true, + install_dir: get_option('datadir') / 'gir-1.0', +) + +custom_target( + typelib, + command: [ + find_program('g-ir-compiler'), + '--output', '@OUTPUT@', + '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@', + meson.current_build_dir() / gir, + ], + input: lib, + output: typelib, + depends: [lib, gir_tgt], + install: true, + install_dir: get_option('libdir') / 'girepository-1.0', +) + +import('pkgconfig').generate( + lib, + name: meson.project_name(), + filebase: meson.project_name() + '-' + api_version, + version: meson.project_version(), + subdirs: meson.project_name(), + requires: deps, + install_dir: get_option('libdir') / 'pkgconfig', +) + +executable( + 'astal', + ['cli.vala', sources], + dependencies: deps, + install: true, +) diff --git a/lib/astal/io/process.vala b/lib/astal/io/process.vala new file mode 100644 index 0000000..cfd05b9 --- /dev/null +++ b/lib/astal/io/process.vala @@ -0,0 +1,172 @@ +/** + * `Process` provides shortcuts for [[email protected]] with sane defaults. + */ +public class AstalIO.Process : Object { + private void read_stream(DataInputStream stream, bool err) { + stream.read_line_utf8_async.begin(Priority.DEFAULT, null, (_, res) => { + try { + var output = stream.read_line_utf8_async.end(res); + if (output != null) { + if (err) + stdout(output.strip()); + else + stderr(output.strip()); + + read_stream(stream, err); + } + } catch (Error err) { + printerr("%s\n", err.message); + } + }); + } + + private DataInputStream out_stream; + private DataInputStream err_stream; + private DataOutputStream in_stream; + private Subprocess process; + public string[] argv { construct; get; } + + + /** + * When the underlying subprocess writes to its stdout + * this signal is emitted with that line. + */ + public signal void stdout (string out); + + /** + * When the underlying subprocess writes to its stderr + * this signal is emitted with that line. + */ + public signal void stderr (string err); + + /** + * Force quit the subprocess. + */ + public void kill() { + process.force_exit(); + } + + /** + * Send a signal to the subprocess. + */ + public void signal(int signal_num) { + process.send_signal(signal_num); + } + + /** + * Write a line to the subprocess' stdin synchronously. + */ + public void write(string in) throws Error { + in_stream.put_string(in); + } + + /** + * Write a line to the subprocess' stdin asynchronously. + */ + public async void write_async(string in) { + try { + yield in_stream.write_all_async(in.data, in.data.length, null, null); + } catch (Error err) { + printerr("%s\n", err.message); + } + } + + /** + * Start a new subprocess with the given command. + * + * The first element of the vector is executed with the remaining elements as the argument list. + */ + public Process.subprocessv(string[] cmd) throws Error { + Object(argv: cmd); + process = new Subprocess.newv(cmd, + SubprocessFlags.STDIN_PIPE | + SubprocessFlags.STDERR_PIPE | + SubprocessFlags.STDOUT_PIPE + ); + out_stream = new DataInputStream(process.get_stdout_pipe()); + err_stream = new DataInputStream(process.get_stderr_pipe()); + in_stream = new DataOutputStream(process.get_stdin_pipe()); + read_stream(out_stream, true); + read_stream(err_stream, false); + } + + /** + * Start a new subprocess with the given command + * which is parsed using [[email protected]_parse_argv]. + */ + public static Process subprocess(string cmd) throws Error { + string[] argv; + Shell.parse_argv(cmd, out argv); + return new Process.subprocessv(argv); + } + + /** + * Execute a command synchronously. + * The first element of the vector is executed with the remaining elements as the argument list. + * + * @return stdout of the subprocess + */ + public static string execv(string[] cmd) throws Error { + var process = new Subprocess.newv( + cmd, + SubprocessFlags.STDERR_PIPE | + SubprocessFlags.STDOUT_PIPE + ); + + string err_str, out_str; + process.communicate_utf8(null, null, out out_str, out err_str); + var success = process.get_successful(); + process.dispose(); + if (success) + return out_str.strip(); + else + throw new IOError.FAILED(err_str.strip()); + } + + /** + * Execute a command synchronously. + * The command is parsed using [[email protected]_parse_argv]. + * + * @return stdout of the subprocess + */ + public static string exec(string cmd) throws Error { + string[] argv; + Shell.parse_argv(cmd, out argv); + return Process.execv(argv); + } + + /** + * Execute a command asynchronously. + * The first element of the vector is executed with the remaining elements as the argument list. + * + * @return stdout of the subprocess + */ + public static async string exec_asyncv(string[] cmd) throws Error { + var process = new Subprocess.newv( + cmd, + SubprocessFlags.STDERR_PIPE | + SubprocessFlags.STDOUT_PIPE + ); + + string err_str, out_str; + yield process.communicate_utf8_async(null, null, out out_str, out err_str); + var success = process.get_successful(); + process.dispose(); + if (success) + return out_str.strip(); + else + throw new IOError.FAILED(err_str.strip()); + } + + /** + * Execute a command asynchronously. + * The command is parsed using [[email protected]_parse_argv]. + * + * @return stdout of the subprocess + */ + public static async string exec_async(string cmd) throws Error { + string[] argv; + Shell.parse_argv(cmd, out argv); + return yield exec_asyncv(argv); + } +} diff --git a/lib/astal/io/time.vala b/lib/astal/io/time.vala new file mode 100644 index 0000000..a799f2b --- /dev/null +++ b/lib/astal/io/time.vala @@ -0,0 +1,111 @@ +/** + * `Time` provides shortcuts for GLib timeout functions. + */ +public class AstalIO.Time : Object { + private Cancellable cancellable; + private uint timeout_id; + private bool fulfilled = false; + + /** + * Emitted when the timer ticks. + */ + public signal void now (); + + /** + * Emitted when the timere is cancelled. + */ + public signal void cancelled (); + + construct { + cancellable = new Cancellable(); + cancellable.cancelled.connect(() => { + if (!fulfilled) { + Source.remove(timeout_id); + cancelled(); + dispose(); + } + }); + } + + private void connect_closure(Closure? closure) { + if (closure == null) + return; + + now.connect(() => { + Value ret = Value(Type.POINTER); // void + closure.invoke(ref ret, {}); + }); + } + + /** + * Start an interval timer with default Priority. + */ + public Time.interval_prio(uint interval, int prio = Priority.DEFAULT, Closure? fn) { + connect_closure(fn); + Idle.add_once(() => now()); + timeout_id = Timeout.add(interval, () => { + now(); + return Source.CONTINUE; + }, prio); + } + + /** + * Start a timeout timer with default Priority. + */ + public Time.timeout_prio(uint timeout, int prio = Priority.DEFAULT, Closure? fn) { + connect_closure(fn); + timeout_id = Timeout.add(timeout, () => { + now(); + fulfilled = true; + return Source.REMOVE; + }, prio); + } + + /** + * Start an idle timer with default priority. + */ + public Time.idle_prio(int prio = Priority.DEFAULT_IDLE, Closure? fn) { + connect_closure(fn); + timeout_id = Idle.add(() => { + now(); + fulfilled = true; + return Source.REMOVE; + }, prio); + } + + /** + * Start an interval timer. Ticks immediately then every `interval` milliseconds. + * + * @param interval Tick every milliseconds. + * @param fn Optional callback. + */ + public static Time interval(uint interval, Closure? fn) { + return new Time.interval_prio(interval, Priority.DEFAULT, fn); + } + + /** + * Start a timeout timer which ticks after `timeout` milliseconds. + * + * @param timeout Tick after milliseconds. + * @param fn Optional callback. + */ + public static Time timeout(uint timeout, Closure? fn) { + return new Time.timeout_prio(timeout, Priority.DEFAULT, fn); + } + + /** + * Start a timer which will tick when there are no higher priority tasks pending. + * + * @param fn Optional callback. + */ + public static Time idle(Closure? fn) { + return new Time.idle_prio(Priority.DEFAULT_IDLE, fn); + } + + /** + * Cancel timer and emit [[email protected]::cancelled] + */ + public void cancel() { + cancellable.cancel(); + } +} diff --git a/lib/astal/io/variable.vala b/lib/astal/io/variable.vala new file mode 100644 index 0000000..312a27a --- /dev/null +++ b/lib/astal/io/variable.vala @@ -0,0 +1,198 @@ +/* + * Base class for [[email protected]] mainly meant to be used + * in higher level language bindings such as Lua and Gjs. + */ +public class AstalIO.VariableBase : Object { + public signal void changed (); + public signal void dropped (); + public signal void error (string err); + + // lua-lgi crashes when using its emitting mechanism + public void emit_changed() { changed(); } + public void emit_dropped() { dropped(); } + public void emit_error(string err) { this.error(err); } + + ~VariableBase() { + dropped(); + } +} + +public class AstalIO.Variable : VariableBase { + public Value value { owned get; set; } + + private uint poll_id = 0; + private Process? watch_proc; + + private uint poll_interval { get; set; default = 1000; } + private string[] poll_exec { get; set; } + private Closure? poll_transform { get; set; } + private Closure? poll_fn { get; set; } + + private Closure? watch_transform { get; set; } + private string[] watch_exec { get; set; } + + public Variable(Value init) { + Object(value: init); + } + + public Variable poll( + uint interval, + string exec, + Closure? transform + ) throws Error { + string[] argv; + Shell.parse_argv(exec, out argv); + return pollv(interval, argv, transform); + } + + public Variable pollv( + uint interval, + string[] execv, + Closure? transform + ) throws Error { + if (is_polling()) + stop_poll(); + + poll_interval = interval; + poll_exec = execv; + poll_transform = transform; + poll_fn = null; + start_poll(); + return this; + } + + public Variable pollfn( + uint interval, + Closure fn + ) throws Error { + if (is_polling()) + stop_poll(); + + poll_interval = interval; + poll_fn = fn; + poll_exec = null; + start_poll(); + return this; + } + + public Variable watch( + string exec, + Closure? transform + ) throws Error { + string[] argv; + Shell.parse_argv(exec, out argv); + return watchv(argv, transform); + } + + public Variable watchv( + string[] execv, + Closure? transform + ) throws Error { + if (is_watching()) + stop_watch(); + + watch_exec = execv; + watch_transform = transform; + start_watch(); + return this; + } + + construct { + notify["value"].connect(() => changed()); + dropped.connect(() => { + if (is_polling()) + stop_poll(); + + if (is_watching()) + stop_watch(); + }); + } + + private void set_closure(string val, Closure? transform) { + if (transform != null) { + var str = Value(typeof(string)); + str.set_string(val); + + var ret_val = Value(this.value.type()); + transform.invoke(ref ret_val, { str, this.value }); + this.value = ret_val; + } + else { + if (this.value.type() == Type.STRING && this.value.get_string() == val) + return; + + var str = Value(typeof(string)); + str.set_string(val); + this.value = str; + } + } + + private void set_fn() { + var ret_val = Value(this.value.type()); + poll_fn.invoke(ref ret_val, { this.value }); + this.value = ret_val; + } + + public void start_poll() throws Error { + return_if_fail(poll_id == 0); + + if (poll_fn != null) { + set_fn(); + poll_id = Timeout.add(poll_interval, () => { + set_fn(); + return Source.CONTINUE; + }, Priority.DEFAULT); + } + if (poll_exec != null) { + Process.exec_asyncv.begin(poll_exec, (_, res) => { + try { + var str = Process.exec_asyncv.end(res); + set_closure(str, poll_transform); + } catch (Error err) { + this.error(err.message); + } + }); + poll_id = Timeout.add(poll_interval, () => { + Process.exec_asyncv.begin(poll_exec, (_, res) => { + try { + var str = Process.exec_asyncv.end(res); + set_closure(str, poll_transform); + } catch (Error err) { + this.error(err.message); + Source.remove(poll_id); + poll_id = 0; + } + }); + return Source.CONTINUE; + }, Priority.DEFAULT); + } + } + + public void start_watch() throws Error { + return_if_fail(watch_proc == null); + return_if_fail(watch_exec != null); + + watch_proc = new Process.subprocessv(watch_exec); + watch_proc.stdout.connect((str) => set_closure(str, watch_transform)); + watch_proc.stderr.connect((str) => this.error(str)); + } + + public void stop_poll() { + return_if_fail(poll_id != 0); + Source.remove(poll_id); + poll_id = 0; + } + + public void stop_watch() { + return_if_fail(watch_proc != null); + watch_proc.kill(); + watch_proc = null; + } + + public bool is_polling() { return poll_id > 0; } + public bool is_watching() { return watch_proc != null; } + + ~Variable() { + dropped(); + } +} diff --git a/lib/astal/io/version b/lib/astal/io/version new file mode 100644 index 0000000..6e8bf73 --- /dev/null +++ b/lib/astal/io/version @@ -0,0 +1 @@ +0.1.0 |