summaryrefslogtreecommitdiff
path: root/lib/astal/io
diff options
context:
space:
mode:
authorAylur <[email protected]>2024-10-14 16:01:36 +0000
committerAylur <[email protected]>2024-10-14 16:44:51 +0000
commit6a8c41cd1d5e218d0dacffb836fdd7d4ec6333dd (patch)
tree7e027d39940838b10c742ef0d5a7aeddad6ef884 /lib/astal/io
parentbdb23e20f171da7c769cba9e393d7e406e563a78 (diff)
feat: astal-io
Diffstat (limited to 'lib/astal/io')
-rw-r--r--lib/astal/io/application.vala161
-rw-r--r--lib/astal/io/cli.vala87
-rw-r--r--lib/astal/io/config.vala.in6
-rw-r--r--lib/astal/io/file.vala81
-rw-r--r--lib/astal/io/meson.build90
-rw-r--r--lib/astal/io/process.vala119
-rw-r--r--lib/astal/io/time.vala71
-rw-r--r--lib/astal/io/variable.vala194
-rw-r--r--lib/astal/io/version1
9 files changed, 810 insertions, 0 deletions
diff --git a/lib/astal/io/application.vala b/lib/astal/io/application.vala
new file mode 100644
index 0000000..00bef57
--- /dev/null
+++ b/lib/astal/io/application.vala
@@ -0,0 +1,161 @@
+namespace AstalIO {
+public errordomain AppError {
+ NAME_OCCUPIED,
+ TAKEOVER_FAILED,
+}
+
+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 abstract void request(string msg, SocketConnection conn) throws Error;
+}
+
+public SocketService acquire_socket(Application app) 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 path = @"$rundir/$name.sock";
+
+ 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;
+}
+
+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;
+}
+
+public static void quit_instance(string instance) {
+ try {
+ IApplication proxy = Bus.get_proxy_sync(
+ BusType.SESSION,
+ "io.Astal." + instance,
+ "/io/Astal/Application"
+ );
+
+ proxy.quit();
+ } catch (Error err) {
+ critical(err.message);
+ }
+}
+
+public static void open_inspector(string instance) {
+ try {
+ IApplication proxy = Bus.get_proxy_sync(
+ BusType.SESSION,
+ "io.Astal." + instance,
+ "/io/Astal/Application"
+ );
+
+ proxy.inspector();
+ } catch (Error err) {
+ critical(err.message);
+ }
+}
+
+public static void toggle_window_by_name(string instance, string window) {
+ try {
+ IApplication proxy = Bus.get_proxy_sync(
+ BusType.SESSION,
+ "io.Astal." + instance,
+ "/io/Astal/Application"
+ );
+
+ proxy.toggle_window(window);
+ } catch (Error err) {
+ critical(err.message);
+ }
+}
+
+public static string send_message(string instance_name, string msg) {
+ var rundir = Environment.get_user_runtime_dir();
+ var socket_path = @"$rundir/$instance_name.sock";
+ var client = new SocketClient();
+
+ try {
+ 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);
+ } catch (Error err) {
+ printerr(err.message);
+ return "";
+ }
+}
+
+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);
+}
+
+public async void write_sock(SocketConnection conn, string response) throws IOError {
+ yield conn.output_stream.write_async(response.concat("\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..1db0b2e
--- /dev/null
+++ b/lib/astal/io/cli.vala
@@ -0,0 +1,87 @@
+private static bool version;
+private static bool help;
+private static bool list;
+private static bool quit;
+private static bool inspector;
+private static string? toggle_window;
+private static string? instance_name;
+
+private 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 },
+ { "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 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 err) {
+ printerr (err.message);
+ return 1;
+ }
+
+ 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())
+ stdout.printf("%s\n", name);
+
+ return 0;
+ }
+
+ 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;
+ }
+
+ var request = "";
+ for (var i = 1; i < argv.length; ++i) {
+ request = request.concat(" ", argv[i]);
+ }
+
+ var reply = AstalIO.send_message(instance_name, request);
+ print("%s\n", reply);
+
+ 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..b2d480c
--- /dev/null
+++ b/lib/astal/io/file.vala
@@ -0,0 +1,81 @@
+namespace AstalIO {
+public string read_file(string path) {
+ var str = "";
+ try {
+ FileUtils.get_contents(path, out str, null);
+ } catch (Error error) {
+ critical(error.message);
+ }
+ return str;
+}
+
+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;
+}
+
+public void write_file(string path, string content) {
+ try {
+ FileUtils.set_contents(path, content);
+ } catch (Error error) {
+ critical(error.message);
+ }
+}
+
+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);
+}
+
+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/meson.build b/lib/astal/io/meson.build
new file mode 100644
index 0000000..426a6d6
--- /dev/null
+++ b/lib/astal/io/meson.build
@@ -0,0 +1,90 @@
+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,
+ 'application.vala',
+ 'file.vala',
+ 'process.vala',
+ 'time.vala',
+ 'variable.vala',
+]
+
+lib = library(
+ meson.project_name(),
+ sources,
+ dependencies: deps,
+ vala_header: meson.project_name() + '.h',
+ vala_vapi: meson.project_name() + '-' + api_version + '.vapi',
+ vala_gir: gir,
+ version: meson.project_version(),
+ install: true,
+ install_dir: [true, true, true, true],
+)
+
+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: libdir / 'pkgconfig',
+)
+
+custom_target(
+ typelib,
+ command: [
+ find_program('g-ir-compiler'),
+ '--output', '@OUTPUT@',
+ '--shared-library', libdir / '@PLAINNAME@',
+ meson.current_build_dir() / gir,
+ ],
+ input: lib,
+ output: typelib,
+ depends: lib,
+ install: true,
+ install_dir: libdir / 'girepository-1.0',
+)
+
+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..e8637ab
--- /dev/null
+++ b/lib/astal/io/process.vala
@@ -0,0 +1,119 @@
+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; }
+
+ public signal void stdout (string out);
+ public signal void stderr (string err);
+
+ public void kill() {
+ process.force_exit();
+ }
+
+ public void signal(int signal_num) {
+ process.send_signal(signal_num);
+ }
+
+ public void write(string in) throws Error {
+ in_stream.put_string(in);
+ }
+
+ public void write_async(string in) {
+ in_stream.write_all_async.begin(
+ in.data,
+ Priority.DEFAULT, null, (_, res) => {
+ try {
+ in_stream.write_all_async.end(res, null);
+ } catch (Error err) {
+ printerr("%s\n", err.message);
+ }
+ }
+ );
+ }
+
+ 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);
+ }
+
+ public static Process subprocess(string cmd) throws Error {
+ string[] argv;
+ Shell.parse_argv(cmd, out argv);
+ return new Process.subprocessv(argv);
+ }
+
+ 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());
+ }
+
+ public static string exec(string cmd) throws Error {
+ string[] argv;
+ Shell.parse_argv(cmd, out argv);
+ return Process.execv(argv);
+ }
+
+ 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());
+ }
+
+ 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..1446441
--- /dev/null
+++ b/lib/astal/io/time.vala
@@ -0,0 +1,71 @@
+public class AstalIO.Time : Object {
+ public signal void now ();
+ public signal void cancelled ();
+ private Cancellable cancellable;
+ private uint timeout_id;
+ private bool fulfilled = false;
+
+ 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, {});
+ });
+ }
+
+ 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);
+ }
+
+ 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);
+ }
+
+ 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);
+ }
+
+ public static Time interval(uint interval, Closure? fn) {
+ return new Time.interval_prio(interval, Priority.DEFAULT, fn);
+ }
+
+ public static Time timeout(uint timeout, Closure? fn) {
+ return new Time.timeout_prio(timeout, Priority.DEFAULT, fn);
+ }
+
+ public static Time idle(Closure? fn) {
+ return new Time.idle_prio(Priority.DEFAULT_IDLE, fn);
+ }
+
+ 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..2a395b4
--- /dev/null
+++ b/lib/astal/io/variable.vala
@@ -0,0 +1,194 @@
+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