From eb5ca1125ba043fb83c3a5ae8182ec48aff85c1b Mon Sep 17 00:00:00 2001 From: kotontrion Date: Fri, 11 Oct 2024 11:22:26 +0200 Subject: apps: better fuzzy search algorithm --- lib/apps/application.vala | 50 +++++-------------------------- lib/apps/fuzzy.vala | 75 +++++++++++++++++++++++++++++++++++++++++++++++ lib/apps/meson.build | 1 + 3 files changed, 84 insertions(+), 42 deletions(-) create mode 100644 lib/apps/fuzzy.vala (limited to 'lib') diff --git a/lib/apps/application.vala b/lib/apps/application.vala index 5748fc6..75ff6b2 100644 --- a/lib/apps/application.vala +++ b/lib/apps/application.vala @@ -32,13 +32,13 @@ public class Application : Object { public Score fuzzy_match(string term) { var score = Score(); if (name != null) - score.name = levenshtein(term, name); + score.name = fuzzy_match_string(term, name); if (entry != null) - score.entry = levenshtein(term, entry); + score.entry = fuzzy_match_string(term, entry); if (executable != null) - score.executable = levenshtein(term, executable); + score.executable = fuzzy_match_string(term, executable); if (description != null) - score.description = levenshtein(term, description); + score.description = fuzzy_match_string(term, description); return score; } @@ -75,44 +75,10 @@ int min3(int a, int b, int c) { return (a < b) ? ((a < c) ? a : c) : ((b < c) ? b : c); } -double levenshtein(string s1, string s2) { - int len1 = s1.length; - int len2 = s2.length; - - int[, ] d = new int[len1 + 1, len2 + 1]; - - for (int i = 0; i <= len1; i++) { - d[i, 0] = i; - } - for (int j = 0; j <= len2; j++) { - d[0, j] = j; - } - - for (int i = 1; i <= len1; i++) { - for (int j = 1; j <= len2; j++) { - int cost = (s1[i - 1] == s2[j - 1]) ? 0 : 1; - d[i, j] = min3( - d[i - 1, j] + 1, // deletion - d[i, j - 1] + 1, // insertion - d[i - 1, j - 1] + cost // substitution - ); - } - } - - var distance = d[len1, len2]; - int max_len = len1 > len2 ? len1 : len2; - - if (max_len == 0) { - return 1.0; - } - - return 1.0 - ((double)distance / max_len); -} - public struct Score { - double name; - double entry; - double executable; - double description; + int name; + int entry; + int executable; + int description; } } diff --git a/lib/apps/fuzzy.vala b/lib/apps/fuzzy.vala new file mode 100644 index 0000000..d6dac3d --- /dev/null +++ b/lib/apps/fuzzy.vala @@ -0,0 +1,75 @@ + +namespace AstalApps { + + private int max(int a, int b) { + return a > b ? a : b; + } + + public int fuzzy_match_string(string pattern, string str) { + const int unmatched_letter_penalty = -1; + int score = 100; + + if (pattern.length == 0) return score; + if (str.length < pattern.length) return int.MIN; + + score += unmatched_letter_penalty * (str.length - pattern.length); + score = fuzzy_match_recurse(pattern, str, score, true); + + return score; + } + + private int fuzzy_match_recurse(string pattern, string str, int score, bool first_char) { + if (pattern.length == 0) return score; + + int match_idx = 0; + int offset = 0; + unichar search = pattern.casefold().get_char(0); + int best_score = int.MIN; + + while ((match_idx = str.casefold().substring(offset).index_of_char(search)) >= 0) { + offset += match_idx; + int subscore = fuzzy_match_recurse( + pattern.substring(1), + str.substring(offset + 1), + compute_score(offset, first_char, str, offset), false); + best_score = max(best_score, subscore); + offset++; + } + + + if (best_score == int.MIN) return int.MIN; + return score + best_score; + } + + private int compute_score(int jump, bool first_char, string match, int idx) { + const int adjacency_bonus = 15; + const int separator_bonus = 30; + const int camel_bonus = 30; + const int first_letter_bonus = 15; + const int leading_letter_penalty = -5; + const int max_leading_letter_penalty = -15; + + int score = 0; + + if (!first_char && jump == 0) { + score += adjacency_bonus; + } + if (!first_char || jump > 0) { + if (match[idx].isupper() && match[idx-1].islower()) { + score += camel_bonus; + } + if (match[idx].isalnum() && !match[idx-1].isalnum()) { + score += separator_bonus; + } + } + if (first_char && jump == 0) { + score += first_letter_bonus; + } + if (first_char) { + score += max(leading_letter_penalty * jump, max_leading_letter_penalty); + } + + return score; + } + +} diff --git a/lib/apps/meson.build b/lib/apps/meson.build index fb87e22..b83b216 100644 --- a/lib/apps/meson.build +++ b/lib/apps/meson.build @@ -44,6 +44,7 @@ sources = [ 'apps.vala', 'application.vala', 'cli.vala', + 'fuzzy.vala', ] if get_option('lib') -- cgit v1.2.3 From 856f6d06464f5ced12be8dd1a288daccda44c3e5 Mon Sep 17 00:00:00 2001 From: kotontrion Date: Sun, 13 Oct 2024 08:40:10 +0200 Subject: apps: fix style --- lib/apps/apps.vala | 2 +- lib/apps/fuzzy.vala | 112 ++++++++++++++++++++++++++-------------------------- 2 files changed, 56 insertions(+), 58 deletions(-) (limited to 'lib') diff --git a/lib/apps/apps.vala b/lib/apps/apps.vala index 2a0d507..b07961e 100644 --- a/lib/apps/apps.vala +++ b/lib/apps/apps.vala @@ -8,7 +8,7 @@ public class Apps : Object { public bool show_hidden { get; set; } public List list { owned get { return _list.copy(); } } - public double min_score { get; set; default = 0.5; } + public int min_score { get; set; default = 0; } public double name_multiplier { get; set; default = 2; } public double entry_multiplier { get; set; default = 1; } diff --git a/lib/apps/fuzzy.vala b/lib/apps/fuzzy.vala index d6dac3d..f93b2eb 100644 --- a/lib/apps/fuzzy.vala +++ b/lib/apps/fuzzy.vala @@ -1,75 +1,73 @@ namespace AstalApps { - private int max(int a, int b) { - return a > b ? a : b; - } +private int max(int a, int b) { + return a > b ? a : b; +} - public int fuzzy_match_string(string pattern, string str) { - const int unmatched_letter_penalty = -1; - int score = 100; +public int fuzzy_match_string(string pattern, string str) { + const int unmatched_letter_penalty = -1; + int score = 100; - if (pattern.length == 0) return score; - if (str.length < pattern.length) return int.MIN; + if (pattern.length == 0) return score; + if (str.length < pattern.length) return int.MIN; - score += unmatched_letter_penalty * (str.length - pattern.length); - score = fuzzy_match_recurse(pattern, str, score, true); + score += unmatched_letter_penalty * (str.length - pattern.length); + score = fuzzy_match_recurse(pattern, str, score, true); - return score; - } + return score; +} - private int fuzzy_match_recurse(string pattern, string str, int score, bool first_char) { - if (pattern.length == 0) return score; +private int fuzzy_match_recurse(string pattern, string str, int score, bool first_char) { + if (pattern.length == 0) return score; + + int match_idx = 0; + int offset = 0; + unichar search = pattern.casefold().get_char(0); + int best_score = int.MIN; + + while ((match_idx = str.casefold().substring(offset).index_of_char(search)) >= 0) { + offset += match_idx; + int subscore = fuzzy_match_recurse( + pattern.substring(1), + str.substring(offset + 1), + compute_score(offset, first_char, str, offset), false); + best_score = max(best_score, subscore); + offset++; + } - int match_idx = 0; - int offset = 0; - unichar search = pattern.casefold().get_char(0); - int best_score = int.MIN; + if (best_score == int.MIN) return int.MIN; + return score + best_score; +} - while ((match_idx = str.casefold().substring(offset).index_of_char(search)) >= 0) { - offset += match_idx; - int subscore = fuzzy_match_recurse( - pattern.substring(1), - str.substring(offset + 1), - compute_score(offset, first_char, str, offset), false); - best_score = max(best_score, subscore); - offset++; - } +private int compute_score(int jump, bool first_char, string match, int idx) { + const int adjacency_bonus = 15; + const int separator_bonus = 30; + const int camel_bonus = 30; + const int first_letter_bonus = 15; + const int leading_letter_penalty = -5; + const int max_leading_letter_penalty = -15; + int score = 0; - if (best_score == int.MIN) return int.MIN; - return score + best_score; + if (!first_char && jump == 0) { + score += adjacency_bonus; } - - private int compute_score(int jump, bool first_char, string match, int idx) { - const int adjacency_bonus = 15; - const int separator_bonus = 30; - const int camel_bonus = 30; - const int first_letter_bonus = 15; - const int leading_letter_penalty = -5; - const int max_leading_letter_penalty = -15; - - int score = 0; - - if (!first_char && jump == 0) { - score += adjacency_bonus; - } - if (!first_char || jump > 0) { - if (match[idx].isupper() && match[idx-1].islower()) { - score += camel_bonus; - } - if (match[idx].isalnum() && !match[idx-1].isalnum()) { - score += separator_bonus; - } - } - if (first_char && jump == 0) { - score += first_letter_bonus; + if (!first_char || jump > 0) { + if (match[idx].isupper() && match[idx-1].islower()) { + score += camel_bonus; } - if (first_char) { - score += max(leading_letter_penalty * jump, max_leading_letter_penalty); + if (match[idx].isalnum() && !match[idx-1].isalnum()) { + score += separator_bonus; } - - return score; + } + if (first_char && jump == 0) { + score += first_letter_bonus; + } + if (first_char) { + score += max(leading_letter_penalty * jump, max_leading_letter_penalty); } + return score; +} } -- cgit v1.2.3 From 25528b00006745b4def7d2f61cc910592b4b3ae9 Mon Sep 17 00:00:00 2001 From: gnat Date: Sun, 13 Oct 2024 15:25:47 -0700 Subject: check that conn is not null --- lib/hyprland/hyprland.vala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/hyprland/hyprland.vala b/lib/hyprland/hyprland.vala index 3886486..8834ea2 100644 --- a/lib/hyprland/hyprland.vala +++ b/lib/hyprland/hyprland.vala @@ -158,8 +158,10 @@ public class Hyprland : Object { out DataInputStream stream ) throws Error { conn = connection("socket"); - conn.output_stream.write(message.data, null); - stream = new DataInputStream(conn.input_stream); + if (conn != null) { + conn.output_stream.write(message.data, null); + stream = new DataInputStream(conn.input_stream); + } } public string message(string message) { -- cgit v1.2.3 From f8745db77f62c743496a6b22211a4ac1ca7c86e3 Mon Sep 17 00:00:00 2001 From: Aylur Date: Mon, 14 Oct 2024 09:39:32 +0000 Subject: lib(hyprland): check if conn and strea is not null --- lib/hyprland/hyprland.vala | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) (limited to 'lib') diff --git a/lib/hyprland/hyprland.vala b/lib/hyprland/hyprland.vala index 8834ea2..ea95cab 100644 --- a/lib/hyprland/hyprland.vala +++ b/lib/hyprland/hyprland.vala @@ -161,42 +161,40 @@ public class Hyprland : Object { if (conn != null) { conn.output_stream.write(message.data, null); stream = new DataInputStream(conn.input_stream); + } else { + stream = null; + critical("could not write to the Hyprland socket"); } } public string message(string message) { - SocketConnection conn; - DataInputStream stream; + SocketConnection? conn; + DataInputStream? stream; try { write_socket(message, out conn, out stream); - return stream.read_upto("\x04", -1, null, null); + if (stream != null && conn != null) { + var res = stream.read_upto("\x04", -1, null, null); + conn.close(null); + return res; + } } catch (Error err) { critical(err.message); - } finally { - try { - if (conn != null) - conn.close(null); - } catch (Error err) { - critical(err.message); - } } return ""; } public async string message_async(string message) { - SocketConnection conn; - DataInputStream stream; + SocketConnection? conn; + DataInputStream? stream; try { write_socket(message, out conn, out stream); - return yield stream.read_upto_async("\x04", -1, Priority.DEFAULT, null, null); - } catch (Error err) { - critical(err.message); - } finally { - try { + if (stream != null && conn != null) { + var res = yield stream.read_upto_async("\x04", -1, Priority.DEFAULT, null, null); conn.close(null); - } catch (Error err) { - critical(err.message); + return res; } + } catch (Error err) { + critical(err.message); } return ""; } -- cgit v1.2.3 From 6a8c41cd1d5e218d0dacffb836fdd7d4ec6333dd Mon Sep 17 00:00:00 2001 From: Aylur Date: Mon, 14 Oct 2024 16:01:36 +0000 Subject: feat: astal-io --- lib/astal/io/application.vala | 161 +++++++++++++++++++++++++++++++++++ lib/astal/io/cli.vala | 87 +++++++++++++++++++ lib/astal/io/config.vala.in | 6 ++ lib/astal/io/file.vala | 81 ++++++++++++++++++ lib/astal/io/meson.build | 90 ++++++++++++++++++++ lib/astal/io/process.vala | 119 ++++++++++++++++++++++++++ lib/astal/io/time.vala | 71 ++++++++++++++++ lib/astal/io/variable.vala | 194 ++++++++++++++++++++++++++++++++++++++++++ lib/astal/io/version | 1 + 9 files changed, 810 insertions(+) create mode 100644 lib/astal/io/application.vala create mode 100644 lib/astal/io/cli.vala create mode 100644 lib/astal/io/config.vala.in create mode 100644 lib/astal/io/file.vala create mode 100644 lib/astal/io/meson.build create mode 100644 lib/astal/io/process.vala create mode 100644 lib/astal/io/time.vala create mode 100644 lib/astal/io/variable.vala create mode 100644 lib/astal/io/version (limited to 'lib') 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 get_instances() { + var list = new List(); + 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 -- cgit v1.2.3 From 9fab13452a26ed55c01047d4225f699f43bba20d Mon Sep 17 00:00:00 2001 From: Aylur Date: Mon, 14 Oct 2024 16:45:03 +0000 Subject: feat: Astal3 --- lib/astal/gtk3/meson.build | 17 ++ lib/astal/gtk3/src/application.vala | 217 ++++++++++++++++++++ lib/astal/gtk3/src/config.vala.in | 6 + lib/astal/gtk3/src/idle-inhibit.c | 114 +++++++++++ lib/astal/gtk3/src/idle-inhibit.h | 22 ++ lib/astal/gtk3/src/meson.build | 120 +++++++++++ lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi | 13 ++ lib/astal/gtk3/src/widget/box.vala | 50 +++++ lib/astal/gtk3/src/widget/button.vala | 99 +++++++++ lib/astal/gtk3/src/widget/centerbox.vala | 52 +++++ lib/astal/gtk3/src/widget/circularprogress.vala | 180 +++++++++++++++++ lib/astal/gtk3/src/widget/eventbox.vala | 64 ++++++ lib/astal/gtk3/src/widget/icon.vala | 105 ++++++++++ lib/astal/gtk3/src/widget/label.vala | 18 ++ lib/astal/gtk3/src/widget/levelbar.vala | 13 ++ lib/astal/gtk3/src/widget/overlay.vala | 57 ++++++ lib/astal/gtk3/src/widget/scrollable.vala | 40 ++++ lib/astal/gtk3/src/widget/slider.vala | 71 +++++++ lib/astal/gtk3/src/widget/stack.vala | 26 +++ lib/astal/gtk3/src/widget/widget.vala | 157 +++++++++++++++ lib/astal/gtk3/src/widget/window.vala | 246 +++++++++++++++++++++++ lib/astal/gtk3/version | 1 + 22 files changed, 1688 insertions(+) create mode 100644 lib/astal/gtk3/meson.build create mode 100644 lib/astal/gtk3/src/application.vala create mode 100644 lib/astal/gtk3/src/config.vala.in create mode 100644 lib/astal/gtk3/src/idle-inhibit.c create mode 100644 lib/astal/gtk3/src/idle-inhibit.h create mode 100644 lib/astal/gtk3/src/meson.build create mode 100644 lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi create mode 100644 lib/astal/gtk3/src/widget/box.vala create mode 100644 lib/astal/gtk3/src/widget/button.vala create mode 100644 lib/astal/gtk3/src/widget/centerbox.vala create mode 100644 lib/astal/gtk3/src/widget/circularprogress.vala create mode 100644 lib/astal/gtk3/src/widget/eventbox.vala create mode 100644 lib/astal/gtk3/src/widget/icon.vala create mode 100644 lib/astal/gtk3/src/widget/label.vala create mode 100644 lib/astal/gtk3/src/widget/levelbar.vala create mode 100644 lib/astal/gtk3/src/widget/overlay.vala create mode 100644 lib/astal/gtk3/src/widget/scrollable.vala create mode 100644 lib/astal/gtk3/src/widget/slider.vala create mode 100644 lib/astal/gtk3/src/widget/stack.vala create mode 100644 lib/astal/gtk3/src/widget/widget.vala create mode 100644 lib/astal/gtk3/src/widget/window.vala create mode 100644 lib/astal/gtk3/version (limited to 'lib') diff --git a/lib/astal/gtk3/meson.build b/lib/astal/gtk3/meson.build new file mode 100644 index 0000000..b1a0b43 --- /dev/null +++ b/lib/astal/gtk3/meson.build @@ -0,0 +1,17 @@ +project( + 'astal', + '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', + ], +) + +libdir = get_option('prefix') / get_option('libdir') +pkgdatadir = get_option('prefix') / get_option('datadir') / 'astal' + +subdir('src') diff --git a/lib/astal/gtk3/src/application.vala b/lib/astal/gtk3/src/application.vala new file mode 100644 index 0000000..8539aa0 --- /dev/null +++ b/lib/astal/gtk3/src/application.vala @@ -0,0 +1,217 @@ +[DBus (name="io.Astal.Application")] +public class Astal.Application : Gtk.Application, AstalIO.Application { + private List css_providers = new List(); + private SocketService service; + private DBusConnection conn; + private string socket_path { get; set; } + private string _instance_name; + + [DBus (visible=false)] + public signal void monitor_added(Gdk.Monitor monitor); + + [DBus (visible=false)] + public signal void monitor_removed(Gdk.Monitor monitor); + + [DBus (visible=false)] + public signal void window_toggled(Gtk.Window window); + + [DBus (visible=false)] + public List monitors { + owned get { + var display = Gdk.Display.get_default(); + var list = new List(); + for (var i = 0; i <= display.get_n_monitors(); ++i) { + var mon = display.get_monitor(i); + if (mon != null) { + list.append(mon); + } + } + return list; + } + } + + [DBus (visible=false)] + public string instance_name { + owned get { return _instance_name; } + construct set { + application_id = "io.Astal." + value; + _instance_name = value; + } + } + + [DBus (visible=false)] + public List windows { + get { return get_windows(); } + } + + [DBus (visible=false)] + public Gtk.Settings settings { + get { return Gtk.Settings.get_default(); } + } + + [DBus (visible=false)] + public Gdk.Screen screen { + get { return Gdk.Screen.get_default(); } + } + + [DBus (visible=false)] + public string gtk_theme { + owned get { return settings.gtk_theme_name; } + set { settings.gtk_theme_name = value; } + } + + [DBus (visible=false)] + public string icon_theme { + owned get { return settings.gtk_icon_theme_name; } + set { settings.gtk_icon_theme_name = value; } + } + + [DBus (visible=false)] + public string cursor_theme { + owned get { return settings.gtk_cursor_theme_name; } + set { settings.gtk_cursor_theme_name = value; } + } + + [DBus (visible=false)] + public void reset_css() { + foreach(var provider in css_providers) { + Gtk.StyleContext.remove_provider_for_screen(screen, provider); + } + css_providers = new List(); + } + + public void inspector() throws DBusError, IOError { + Gtk.Window.set_interactive_debugging(true); + } + + [DBus (visible=false)] + public Gtk.Window? get_window(string name) { + foreach(var win in windows) { + if (win.name == name) + return win; + } + + critical("no window with name \"%s\"".printf(name)); + return null; + } + + public void toggle_window(string window) throws Error { + var win = get_window(window); + if (win != null) { + win.visible = !win.visible; + } else { + throw new IOError.FAILED("window not found"); + } + } + + [DBus (visible=false)] + public void apply_css(string style, bool reset = false) { + var provider = new Gtk.CssProvider(); + + if (reset) + reset_css(); + + try { + if (FileUtils.test(style, FileTest.EXISTS)) + provider.load_from_path(style); + else + provider.load_from_data(style); + } catch (Error err) { + critical(err.message); + } + + Gtk.StyleContext.add_provider_for_screen( + screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER); + + css_providers.append(provider); + } + + [DBus (visible=false)] + public void add_icons(string? path) { + if (path != null) { + Gtk.IconTheme.get_default().prepend_search_path(path); + } + } + + [DBus (visible=false)] + public virtual void request(string msg, SocketConnection conn) { + AstalIO.write_sock.begin(conn, @"missing response implementation on $application_id"); + } + + /** + * should be called before `run()` + * the return value indicates if instance is already running + */ + [DBus (visible=false)] + public void acquire_socket() { + try { + service = AstalIO.acquire_socket(this); + + Bus.own_name( + BusType.SESSION, + "io.Astal." + instance_name, + BusNameOwnerFlags.NONE, + (conn) => { + try { + this.conn = conn; + conn.register_object("/io/Astal/Application", this); + } catch (Error err) { + critical(err.message); + } + }, + () => {}, + () => {} + ); + } catch (Error err) { + critical("could not acquire socket %s\n", application_id); + critical(err.message); + } + } + + public new void quit() throws DBusError, IOError { + if (service != null) { + if (FileUtils.test(socket_path, GLib.FileTest.EXISTS)){ + try { + File.new_for_path(socket_path).delete(null); + } catch (Error err) { + warning(err.message); + } + } + } + + base.quit(); + } + + construct { + if (instance_name == null) + instance_name = "astal"; + + activate.connect(() => { + var display = Gdk.Display.get_default(); + display.monitor_added.connect((mon) => { + monitor_added(mon); + notify_property("monitors"); + }); + display.monitor_removed.connect((mon) => { + monitor_removed(mon); + notify_property("monitors"); + }); + }); + + window_added.connect((window) => { + ulong id1, id2; + id1 = window.notify["visible"].connect(() => window_toggled(window)); + id2 = window_removed.connect((removed) => { + if (removed == window) { + window.disconnect(id1); + this.disconnect(id2); + } + }); + }); + + shutdown.connect(() => { try { quit(); } catch(Error err) {} }); + Unix.signal_add(1, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); + Unix.signal_add(2, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); + Unix.signal_add(15, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH); + } +} diff --git a/lib/astal/gtk3/src/config.vala.in b/lib/astal/gtk3/src/config.vala.in new file mode 100644 index 0000000..88bfe9c --- /dev/null +++ b/lib/astal/gtk3/src/config.vala.in @@ -0,0 +1,6 @@ +namespace Astal { + 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/gtk3/src/idle-inhibit.c b/lib/astal/gtk3/src/idle-inhibit.c new file mode 100644 index 0000000..48f2471 --- /dev/null +++ b/lib/astal/gtk3/src/idle-inhibit.c @@ -0,0 +1,114 @@ +#include "idle-inhibit.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "idle-inhibit-unstable-v1-client.h" + +struct _AstalInhibitManager { + GObject parent_instance; +}; + +typedef struct { + gboolean init; + struct wl_registry* wl_registry; + struct wl_display* display; + struct zwp_idle_inhibit_manager_v1* idle_inhibit_manager; +} AstalInhibitManagerPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE(AstalInhibitManager, astal_inhibit_manager, G_TYPE_OBJECT) + +AstalInhibitor* astal_inhibit_manager_inhibit(AstalInhibitManager* self, GtkWindow* window) { + AstalInhibitManagerPrivate* priv = astal_inhibit_manager_get_instance_private(self); + g_assert_true(priv->init); + GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(window)); + struct wl_surface* surface = gdk_wayland_window_get_wl_surface(gdk_window); + return zwp_idle_inhibit_manager_v1_create_inhibitor(priv->idle_inhibit_manager, surface); +} + +static void global_registry_handler(void* data, struct wl_registry* registry, uint32_t id, + const char* interface, uint32_t version) { + AstalInhibitManager* self = ASTAL_INHIBIT_MANAGER(data); + AstalInhibitManagerPrivate* priv = astal_inhibit_manager_get_instance_private(self); + + if (strcmp(interface, zwp_idle_inhibit_manager_v1_interface.name) == 0) { + priv->idle_inhibit_manager = + wl_registry_bind(registry, id, &zwp_idle_inhibit_manager_v1_interface, 1); + } +} + +static void global_registry_remover(void* data, struct wl_registry* registry, uint32_t id) { + // neither inhibit_manager nor inhibitor is going to be removed by the compositor, so we don't + // need do anything here. +} + +static const struct wl_registry_listener registry_listener = {global_registry_handler, + global_registry_remover}; + +static gboolean astal_inhibit_manager_wayland_init(AstalInhibitManager* self) { + AstalInhibitManagerPrivate* priv = astal_inhibit_manager_get_instance_private(self); + + if (priv->init) return TRUE; + + GdkDisplay* gdk_display = gdk_display_get_default(); + priv->display = gdk_wayland_display_get_wl_display(gdk_display); + + priv->wl_registry = wl_display_get_registry(priv->display); + wl_registry_add_listener(priv->wl_registry, ®istry_listener, self); + + wl_display_roundtrip(priv->display); + + if (priv->idle_inhibit_manager == NULL) { + g_critical("Can not connect idle inhibitor protocol"); + return FALSE; + } + + priv->init = TRUE; + return TRUE; +} + +AstalInhibitManager* astal_inhibit_manager_get_default() { + static AstalInhibitManager* self = NULL; + + if (self == NULL) { + self = g_object_new(ASTAL_TYPE_INHIBIT_MANAGER, NULL); + if (!astal_inhibit_manager_wayland_init(self)) { + g_object_unref(self); + self = NULL; + } + } + + return self; +} + +static void astal_inhibit_manager_init(AstalInhibitManager* self) { + AstalInhibitManagerPrivate* priv = astal_inhibit_manager_get_instance_private(self); + priv->init = FALSE; + priv->display = NULL; + priv->wl_registry = NULL; + priv->idle_inhibit_manager = NULL; +} + +static void astal_inhibit_manager_finalize(GObject* object) { + AstalInhibitManager* self = ASTAL_INHIBIT_MANAGER(object); + AstalInhibitManagerPrivate* priv = astal_inhibit_manager_get_instance_private(self); + + if (priv->display != NULL) wl_display_roundtrip(priv->display); + + if (priv->wl_registry != NULL) wl_registry_destroy(priv->wl_registry); + if (priv->idle_inhibit_manager != NULL) + zwp_idle_inhibit_manager_v1_destroy(priv->idle_inhibit_manager); + + G_OBJECT_CLASS(astal_inhibit_manager_parent_class)->finalize(object); +} + +static void astal_inhibit_manager_class_init(AstalInhibitManagerClass* class) { + GObjectClass* object_class = G_OBJECT_CLASS(class); + object_class->finalize = astal_inhibit_manager_finalize; +} diff --git a/lib/astal/gtk3/src/idle-inhibit.h b/lib/astal/gtk3/src/idle-inhibit.h new file mode 100644 index 0000000..5e9a3ab --- /dev/null +++ b/lib/astal/gtk3/src/idle-inhibit.h @@ -0,0 +1,22 @@ +#ifndef ASTAL_IDLE_INHIBITOR_H +#define ASTAL_IDLE_INHIBITOR_H + +#include +#include + +#include "idle-inhibit-unstable-v1-client.h" + +G_BEGIN_DECLS + +#define ASTAL_TYPE_INHIBIT_MANAGER (astal_inhibit_manager_get_type()) + +G_DECLARE_FINAL_TYPE(AstalInhibitManager, astal_inhibit_manager, ASTAL, INHIBIT_MANAGER, GObject) + +typedef struct zwp_idle_inhibitor_v1 AstalInhibitor; + +AstalInhibitManager* astal_inhibit_manager_get_default(); +AstalInhibitor* astal_inhibit_manager_inhibit(AstalInhibitManager* self, GtkWindow* window); + +G_END_DECLS + +#endif // !ASTAL_IDLE_INHIBITOR_H diff --git a/lib/astal/gtk3/src/meson.build b/lib/astal/gtk3/src/meson.build new file mode 100644 index 0000000..c8c7df2 --- /dev/null +++ b/lib/astal/gtk3/src/meson.build @@ -0,0 +1,120 @@ +version_split = meson.project_version().split('.') +api_version = version_split[0] + '.' + version_split[1] +gir = 'Astal-' + api_version + '.gir' +typelib = 'Astal-' + api_version + '.typelib' + +vapi_dir = meson.current_source_dir() / 'vapi' +add_project_arguments(['--vapidir', vapi_dir], language: 'vala') + +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], + }, +) + +pkgconfig_deps = [ + dependency('astal-io-0.1'), + dependency('glib-2.0'), + dependency('gio-unix-2.0'), + dependency('gobject-2.0'), + dependency('gio-2.0'), + dependency('gtk+-3.0'), + dependency('gdk-pixbuf-2.0'), + dependency('gtk-layer-shell-0'), + dependency('wayland-client'), +] + +deps = pkgconfig_deps + meson.get_compiler('c').find_library('m') + +wayland_protos = dependency('wayland-protocols') +wayland_scanner = find_program('wayland-scanner') + +wl_protocol_dir = wayland_protos.get_variable(pkgconfig: 'pkgdatadir') + +gen_client_header = generator( + wayland_scanner, + output: ['@BASENAME@-client.h'], + arguments: ['-c', 'client-header', '@INPUT@', '@BUILD_DIR@/@BASENAME@-client.h'], +) + +gen_private_code = generator( + wayland_scanner, + output: ['@BASENAME@.c'], + arguments: ['-c', 'private-code', '@INPUT@', '@BUILD_DIR@/@BASENAME@.c'], +) + +protocols = [ + join_paths(wl_protocol_dir, 'unstable/idle-inhibit/idle-inhibit-unstable-v1.xml'), +] + +client_protocol_srcs = [] + +foreach protocol : protocols + client_header = gen_client_header.process(protocol) + code = gen_private_code.process(protocol) + client_protocol_srcs += [client_header, code] +endforeach + +sources = [ + config, + 'widget/box.vala', + 'widget/button.vala', + 'widget/centerbox.vala', + 'widget/circularprogress.vala', + 'widget/eventbox.vala', + 'widget/icon.vala', + 'widget/label.vala', + 'widget/levelbar.vala', + 'widget/overlay.vala', + 'widget/scrollable.vala', + 'widget/slider.vala', + 'widget/stack.vala', + 'widget/widget.vala', + 'widget/window.vala', + 'application.vala', + 'idle-inhibit.h', + 'idle-inhibit.c', +] + client_protocol_srcs + +lib = library( + meson.project_name(), + sources, + dependencies: deps, + vala_args: ['--pkg', 'AstalInhibitManager'], + 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: pkgconfig_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', +) diff --git a/lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi b/lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi new file mode 100644 index 0000000..6232a3c --- /dev/null +++ b/lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi @@ -0,0 +1,13 @@ +[CCode (cprefix = "Astal", gir_namespace = "Astal", lower_case_cprefix = "astal_")] +namespace Astal { + [CCode (cheader_filename = "idle-inhibit.h", type_id = "astal_idle_inhibit_manager_get_type()")] + public class InhibitManager : GLib.Object { + public static unowned InhibitManager? get_default(); + public Inhibitor inhibit (Gtk.Window window); + } + + [CCode (cheader_filename = "idle-inhibit.h", free_function = "zwp_idle_inhibitor_v1_destroy")] + [Compact] + public class Inhibitor { + } +} diff --git a/lib/astal/gtk3/src/widget/box.vala b/lib/astal/gtk3/src/widget/box.vala new file mode 100644 index 0000000..d23a799 --- /dev/null +++ b/lib/astal/gtk3/src/widget/box.vala @@ -0,0 +1,50 @@ +public class Astal.Box : Gtk.Box { + [CCode (notify = false)] + public bool vertical { + get { return orientation == Gtk.Orientation.VERTICAL; } + set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } + } + + public List children { + set { _set_children(value); } + owned get { return get_children(); } + } + + public new Gtk.Widget child { + owned get { return _get_child(); } + set { _set_child(value); } + } + + construct { + notify["orientation"].connect(() => { + notify_property("vertical"); + }); + } + + private void _set_child(Gtk.Widget child) { + var list = new List(); + list.append(child); + _set_children(list); + } + + private Gtk.Widget? _get_child() { + foreach(var child in get_children()) + return child; + + return null; + } + + private void _set_children(List arr) { + foreach(var child in get_children()) { + remove(child); + } + + foreach(var child in arr) + add(child); + } + + public Box(bool vertical, List children) { + this.vertical = vertical; + _set_children(children); + } +} diff --git a/lib/astal/gtk3/src/widget/button.vala b/lib/astal/gtk3/src/widget/button.vala new file mode 100644 index 0000000..bc10577 --- /dev/null +++ b/lib/astal/gtk3/src/widget/button.vala @@ -0,0 +1,99 @@ +public class Astal.Button : Gtk.Button { + public signal void hover (HoverEvent event); + public signal void hover_lost (HoverEvent event); + public signal void click (ClickEvent event); + public signal void click_release (ClickEvent event); + public signal void scroll (ScrollEvent event); + + construct { + add_events(Gdk.EventMask.SCROLL_MASK); + add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK); + + enter_notify_event.connect((self, event) => { + hover(HoverEvent(event) { lost = false }); + }); + + leave_notify_event.connect((self, event) => { + hover_lost(HoverEvent(event) { lost = true }); + }); + + button_press_event.connect((event) => { + click(ClickEvent(event) { release = false }); + }); + + button_release_event.connect((event) => { + click_release(ClickEvent(event) { release = true }); + }); + + scroll_event.connect((event) => { + scroll(ScrollEvent(event)); + }); + } +} + +public enum Astal.MouseButton { + PRIMARY = 1, + MIDDLE = 2, + SECONDARY = 3, + BACK = 4, + FORWARD = 5, +} + +// these structs are here because gjs converts every event +// into a union Gdk.Event, which cannot be destructured +// and are not as convinent to work with as a struct +public struct Astal.ClickEvent { + bool release; + uint time; + double x; + double y; + Gdk.ModifierType modifier; + MouseButton button; + + public ClickEvent(Gdk.EventButton event) { + this.time = event.time; + this.x = event.x; + this.y = event.y; + this.button = (MouseButton)event.button; + this.modifier = event.state; + } +} + +public struct Astal.HoverEvent { + bool lost; + uint time; + double x; + double y; + Gdk.ModifierType modifier; + Gdk.CrossingMode mode; + Gdk.NotifyType detail; + + public HoverEvent(Gdk.EventCrossing event) { + this.time = event.time; + this.x = event.x; + this.y = event.y; + this.modifier = event.state; + this.mode = event.mode; + this.detail = event.detail; + } +} + +public struct Astal.ScrollEvent { + uint time; + double x; + double y; + Gdk.ModifierType modifier; + Gdk.ScrollDirection direction; + double delta_x; + double delta_y; + + public ScrollEvent(Gdk.EventScroll event) { + this.time = event.time; + this.x = event.x; + this.y = event.y; + this.modifier = event.state; + this.direction = event.direction; + this.delta_x = event.delta_x; + this.delta_y = event.delta_y; + } +} diff --git a/lib/astal/gtk3/src/widget/centerbox.vala b/lib/astal/gtk3/src/widget/centerbox.vala new file mode 100644 index 0000000..89bf50b --- /dev/null +++ b/lib/astal/gtk3/src/widget/centerbox.vala @@ -0,0 +1,52 @@ +public class Astal.CenterBox : Gtk.Box { + [CCode (notify = false)] + public bool vertical { + get { return orientation == Gtk.Orientation.VERTICAL; } + set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } + } + + construct { + notify["orientation"].connect(() => { + notify_property("vertical"); + }); + } + + static construct { + set_css_name("centerbox"); + } + + private Gtk.Widget _start_widget; + public Gtk.Widget start_widget { + get { return _start_widget; } + set { + if (_start_widget != null) + remove(_start_widget); + + if (value != null) + pack_start(value, true, true, 0); + } + } + + private Gtk.Widget _end_widget; + public Gtk.Widget end_widget { + get { return _end_widget; } + set { + if (_end_widget != null) + remove(_end_widget); + + if (value != null) + pack_end(value, true, true, 0); + } + } + + public Gtk.Widget center_widget { + get { return get_center_widget(); } + set { + if (center_widget != null) + remove(center_widget); + + if (value != null) + set_center_widget(value); + } + } +} diff --git a/lib/astal/gtk3/src/widget/circularprogress.vala b/lib/astal/gtk3/src/widget/circularprogress.vala new file mode 100644 index 0000000..dd7c97b --- /dev/null +++ b/lib/astal/gtk3/src/widget/circularprogress.vala @@ -0,0 +1,180 @@ +public class Astal.CircularProgress : Gtk.Bin { + public double start_at { get; set; } + public double end_at { get; set; } + public double value { get; set; } + public bool inverted { get; set; } + public bool rounded { get; set; } + + construct { + notify["start-at"].connect(queue_draw); + notify["end-at"].connect(queue_draw); + notify["value"].connect(queue_draw); + notify["inverted"].connect(queue_draw); + notify["rounded"].connect(queue_draw); + notify["child"].connect(queue_draw); + } + + static construct { + set_css_name("circular-progress"); + } + + public override void get_preferred_height(out int minh, out int nath) { + var val = get_style_context().get_property("min-height", Gtk.StateFlags.NORMAL); + if (val.get_int() <= 0) { + minh = 40; + nath = 40; + } + + minh = val.get_int(); + nath = val.get_int(); + } + + public override void get_preferred_width(out int minw, out int natw) { + var val = get_style_context().get_property("min-width", Gtk.StateFlags.NORMAL); + if (val.get_int() <= 0) { + minw = 40; + natw = 40; + } + + minw = val.get_int(); + natw = val.get_int(); + } + + private double to_radian(double percentage) { + percentage = Math.floor(percentage * 100); + return (percentage / 100) * (2 * Math.PI); + } + + private bool is_full_circle(double start, double end, double epsilon = 1e-10) { + // Ensure that start and end are between 0 and 1 + start = (start % 1 + 1) % 1; + end = (end % 1 + 1) % 1; + + // Check if the difference between start and end is close to 1 + return Math.fabs(start - end) <= epsilon; + } + + private double scale_arc_value(double start, double end, double value) { + // Ensure that start and end are between 0 and 1 + start = (start % 1 + 1) % 1; + end = (end % 1 + 1) % 1; + + // Calculate the length of the arc + var arc_length = end - start; + if (arc_length < 0) + arc_length += 1; // Adjust for circular representation + + // Calculate the position on the arc based on the percentage value + var scaled = arc_length + value; + + // Ensure the position is between 0 and 1 + return (scaled % 1 + 1) % 1; + } + + private double min(double[] arr) { + double min = arr[0]; + foreach(var i in arr) + if (min > i) min = i; + return min; + } + + private double max(double[] arr) { + double max = arr[0]; + foreach(var i in arr) + if (max < i) max = i; + return max; + } + + public override bool draw(Cairo.Context cr) { + Gtk.Allocation allocation; + get_allocation(out allocation); + + var styles = get_style_context(); + var width = allocation.width; + var height = allocation.height; + var thickness = styles.get_property("font-size", Gtk.StateFlags.NORMAL).get_double(); + var margin = styles.get_margin(Gtk.StateFlags.NORMAL); + var fg = styles.get_color(Gtk.StateFlags.NORMAL); + var bg = styles.get_background_color(Gtk.StateFlags.NORMAL); + + var bg_stroke = thickness + min({margin.bottom, margin.top, margin.left, margin.right}); + var fg_stroke = thickness; + var radius = min({width, height}) / 2.0 - max({bg_stroke, fg_stroke}) / 2.0; + var center_x = width / 2; + var center_y = height / 2; + + var start_background = to_radian(start_at); + var end_background = to_radian(end_at); + var ranged_value = value + start_at; + + var is_circle = is_full_circle(this.start_at, this.end_at); + + if (is_circle) { + // Redefine end_draw in radius to create an accurate full circle + end_background = start_background + 2 * Math.PI; + ranged_value = to_radian(value); + } else { + // Range the value for the arc shape + ranged_value = to_radian(scale_arc_value( + start_at, + end_at, + value + )); + } + + double start_progress, end_progress; + + if (inverted) { + start_progress = end_background - ranged_value; + end_progress = end_background; + } else { + start_progress = start_background; + end_progress = start_background + ranged_value; + } + + // Draw background + cr.set_source_rgba(bg.red, bg.green, bg.blue, bg.alpha); + cr.arc(center_x, center_y, radius, start_background, end_background); + cr.set_line_width(bg_stroke); + cr.stroke(); + + // Draw rounded background ends + if (rounded) { + var start_x = center_x + Math.cos(start_background) * radius; + var start_y = center_y + Math.sin(start_background) * radius; + var end_x = center_x + Math.cos(end_background) * radius; + var end_y = center_y + Math.sin(end_background) * radius; + cr.set_line_width(0); + cr.arc(start_x, start_y, bg_stroke / 2, 0, 0 - 0.01); + cr.fill(); + cr.arc(end_x, end_y, bg_stroke / 2, 0, 0 - 0.01); + cr.fill(); + } + + // Draw progress + cr.set_source_rgba(fg.red, fg.green, fg.blue, fg.alpha); + cr.arc(center_x, center_y, radius, start_progress, end_progress); + cr.set_line_width(fg_stroke); + cr.stroke(); + + // Draw rounded progress ends + if (rounded) { + var start_x = center_x + Math.cos(start_progress) * radius; + var start_y = center_y + Math.sin(start_progress) * radius; + var end_x = center_x + Math.cos(end_progress) * radius; + var end_y = center_y + Math.sin(end_progress) * radius; + cr.set_line_width(0); + cr.arc(start_x, start_y, fg_stroke / 2, 0, 0 - 0.01); + cr.fill(); + cr.arc(end_x, end_y, fg_stroke / 2, 0, 0 - 0.01); + cr.fill(); + } + + if (get_child() != null) { + get_child().size_allocate(allocation); + propagate_draw(get_child(), cr); + } + + return true; + } +} diff --git a/lib/astal/gtk3/src/widget/eventbox.vala b/lib/astal/gtk3/src/widget/eventbox.vala new file mode 100644 index 0000000..611da2a --- /dev/null +++ b/lib/astal/gtk3/src/widget/eventbox.vala @@ -0,0 +1,64 @@ +public class Astal.EventBox : Gtk.EventBox { + public signal void hover (HoverEvent event); + public signal void hover_lost (HoverEvent event); + public signal void click (ClickEvent event); + public signal void click_release (ClickEvent event); + public signal void scroll (ScrollEvent event); + public signal void motion (MotionEvent event); + + static construct { + set_css_name("eventbox"); + } + + construct { + add_events(Gdk.EventMask.SCROLL_MASK); + add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK); + add_events(Gdk.EventMask.POINTER_MOTION_MASK); + + enter_notify_event.connect((self, event) => { + if (event.window == self.get_window() && + event.detail != Gdk.NotifyType.INFERIOR) { + this.set_state_flags(Gtk.StateFlags.PRELIGHT, false); + hover(HoverEvent(event) { lost = false }); + } + }); + + leave_notify_event.connect((self, event) => { + if (event.window == self.get_window() && + event.detail != Gdk.NotifyType.INFERIOR) { + this.unset_state_flags(Gtk.StateFlags.PRELIGHT); + hover_lost(HoverEvent(event) { lost = true }); + } + }); + + button_press_event.connect((event) => { + click(ClickEvent(event) { release = false }); + }); + + button_release_event.connect((event) => { + click_release(ClickEvent(event) { release = true }); + }); + + scroll_event.connect((event) => { + scroll(ScrollEvent(event)); + }); + + motion_notify_event.connect((event) => { + motion(MotionEvent(event)); + }); + } +} + +public struct Astal.MotionEvent { + uint time; + double x; + double y; + Gdk.ModifierType modifier; + + public MotionEvent(Gdk.EventMotion event) { + this.time = event.time; + this.x = event.x; + this.y = event.y; + this.modifier = event.state; + } +} diff --git a/lib/astal/gtk3/src/widget/icon.vala b/lib/astal/gtk3/src/widget/icon.vala new file mode 100644 index 0000000..f2d59a2 --- /dev/null +++ b/lib/astal/gtk3/src/widget/icon.vala @@ -0,0 +1,105 @@ +public class Astal.Icon : Gtk.Image { + private IconType type = IconType.NAMED; + private double size { get; set; default = 14; } + + public new Gdk.Pixbuf pixbuf { get; set; } + public string icon { get; set; default = ""; } + public GLib.Icon g_icon {get; set;} + + public static Gtk.IconInfo? lookup_icon(string icon) { + var theme = Gtk.IconTheme.get_default(); + return theme.lookup_icon(icon, 16, Gtk.IconLookupFlags.USE_BUILTIN); + } + + private async void display_icon() { + switch(type) { + case IconType.NAMED: + icon_name = icon; + pixel_size = (int)size; + break; + case IconType.FILE: + try { + var file = File.new_for_path(icon); + var stream = yield file.read_async(); + var pb = yield new Gdk.Pixbuf.from_stream_at_scale_async( + stream, + (int)size * scale_factor, + (int)size * scale_factor, + true, + null + ); + var cs = Gdk.cairo_surface_create_from_pixbuf(pb, 0, this.get_window()); + set_from_surface(cs); + } catch (Error err) { + printerr(err.message); + } + break; + case IconType.PIXBUF: + var pb_scaled = pixbuf.scale_simple( + (int)size * scale_factor, + (int)size * scale_factor, + Gdk.InterpType.BILINEAR + ); + if (pb_scaled != null) { + var cs = Gdk.cairo_surface_create_from_pixbuf(pb_scaled, 0, this.get_window()); + set_from_surface(cs); + } + break; + case IconType.GICON: + pixel_size = (int)size; + gicon = g_icon; + break; + + } + } + + static construct { + set_css_name("icon"); + } + + construct { + notify["icon"].connect(() => { + if(FileUtils.test(icon, GLib.FileTest.EXISTS)) + type = IconType.FILE; + else if (lookup_icon(icon) != null) + type = IconType.NAMED; + else { + type = IconType.NAMED; + warning("cannot assign %s as icon, "+ + "it is not a file nor a named icon", icon); + } + display_icon.begin(); + }); + + notify["pixbuf"].connect(() => { + type = IconType.PIXBUF; + display_icon.begin(); + }); + + notify["g-icon"].connect(() => { + type = IconType.GICON; + display_icon.begin(); + }); + + size_allocate.connect(() => { + size = get_style_context() + .get_property("font-size", Gtk.StateFlags.NORMAL).get_double(); + + display_icon.begin(); + }); + + get_style_context().changed.connect(() => { + size = get_style_context() + .get_property("font-size", Gtk.StateFlags.NORMAL).get_double(); + + display_icon.begin(); + }); + } +} + +private enum Astal.IconType { + NAMED, + FILE, + PIXBUF, + GICON, +} diff --git a/lib/astal/gtk3/src/widget/label.vala b/lib/astal/gtk3/src/widget/label.vala new file mode 100644 index 0000000..4063b6f --- /dev/null +++ b/lib/astal/gtk3/src/widget/label.vala @@ -0,0 +1,18 @@ +using Pango; + +public class Astal.Label : Gtk.Label { + public bool truncate { + set { ellipsize = value ? EllipsizeMode.END : EllipsizeMode.NONE; } + get { return ellipsize == EllipsizeMode.END; } + } + + public new bool justify_fill { + set { justify = value ? Gtk.Justification.FILL : Gtk.Justification.LEFT; } + get { return justify == Gtk.Justification.FILL; } + } + + construct { + notify["ellipsize"].connect(() => notify_property("truncate")); + notify["justify"].connect(() => notify_property("justify_fill")); + } +} diff --git a/lib/astal/gtk3/src/widget/levelbar.vala b/lib/astal/gtk3/src/widget/levelbar.vala new file mode 100644 index 0000000..9b61957 --- /dev/null +++ b/lib/astal/gtk3/src/widget/levelbar.vala @@ -0,0 +1,13 @@ +public class Astal.LevelBar : Gtk.LevelBar { + [CCode (notify = false)] + public bool vertical { + get { return orientation == Gtk.Orientation.VERTICAL; } + set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } + } + + construct { + notify["orientation"].connect(() => { + notify_property("vertical"); + }); + } +} diff --git a/lib/astal/gtk3/src/widget/overlay.vala b/lib/astal/gtk3/src/widget/overlay.vala new file mode 100644 index 0000000..603ee66 --- /dev/null +++ b/lib/astal/gtk3/src/widget/overlay.vala @@ -0,0 +1,57 @@ +public class Astal.Overlay : Gtk.Overlay { + public bool pass_through { get; set; } + + public Gtk.Widget? overlay { + get { return overlays.nth_data(0); } + set { + foreach (var ch in get_children()) { + if (ch != child) + remove(ch); + } + + if (value != null) + add_overlay(value); + } + } + + public List overlays { + owned get { return get_children(); } + set { + foreach (var ch in get_children()) { + if (ch != child) + remove(ch); + } + + foreach (var ch in value) + add_overlay(ch); + } + } + + public new Gtk.Widget? child { + get { return get_child(); } + set { + var ch = get_child(); + if (ch != null) + remove(ch); + + if (value != null) + add(value); + } + } + + construct { + notify["pass-through"].connect(() => { + update_pass_through(); + }); + } + + private void update_pass_through() { + foreach (var child in get_children()) + set_overlay_pass_through(child, pass_through); + } + + public new void add_overlay(Gtk.Widget widget) { + base.add_overlay(widget); + set_overlay_pass_through(widget, pass_through); + } +} diff --git a/lib/astal/gtk3/src/widget/scrollable.vala b/lib/astal/gtk3/src/widget/scrollable.vala new file mode 100644 index 0000000..57afb6e --- /dev/null +++ b/lib/astal/gtk3/src/widget/scrollable.vala @@ -0,0 +1,40 @@ +public class Astal.Scrollable : Gtk.ScrolledWindow { + private Gtk.PolicyType _hscroll = Gtk.PolicyType.AUTOMATIC; + private Gtk.PolicyType _vscroll = Gtk.PolicyType.AUTOMATIC; + + public Gtk.PolicyType hscroll { + get { return _hscroll; } + set { + _hscroll = value; + set_policy(value, vscroll); + } + } + + public Gtk.PolicyType vscroll { + get { return _vscroll; } + set { + _vscroll = value; + set_policy(hscroll, value); + } + } + + static construct { + set_css_name("scrollable"); + } + + construct { + if (hadjustment != null) + hadjustment = new Gtk.Adjustment(0,0,0,0,0,0); + + if (vadjustment != null) + vadjustment = new Gtk.Adjustment(0,0,0,0,0,0); + } + + public new Gtk.Widget get_child() { + var ch = base.get_child(); + if (ch is Gtk.Viewport) { + return ch.get_child(); + } + return ch; + } +} diff --git a/lib/astal/gtk3/src/widget/slider.vala b/lib/astal/gtk3/src/widget/slider.vala new file mode 100644 index 0000000..466275b --- /dev/null +++ b/lib/astal/gtk3/src/widget/slider.vala @@ -0,0 +1,71 @@ +public class Astal.Slider : Gtk.Scale { + [CCode (notify = false)] + public bool vertical { + get { return orientation == Gtk.Orientation.VERTICAL; } + set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } + } + + // emitted when the user drags the slider + public signal void dragged (); + + construct { + draw_value = false; + + if (adjustment == null) + adjustment = new Gtk.Adjustment(0,0,0,0,0,0); + + if (max == 0 && min == 0) { + max = 1; + } + + if (step == 0) { + step = 0.05; + } + + notify["orientation"].connect(() => { + notify_property("vertical"); + }); + + button_press_event.connect(() => { dragging = true; }); + key_press_event.connect(() => { dragging = true; }); + button_release_event.connect(() => { dragging = false; }); + key_release_event.connect(() => { dragging = false; }); + scroll_event.connect((event) => { + dragging = true; + if (event.delta_y > 0) + value -= step; + else + value += step; + dragging = false; + }); + + value_changed.connect(() => { + if (dragging) + dragged(); + }); + } + + public bool dragging { get; private set; } + + public double value { + get { return adjustment.value; } + set { if (!dragging) adjustment.value = value; } + } + + public double min { + get { return adjustment.lower; } + set { adjustment.lower = value; } + } + + public double max { + get { return adjustment.upper; } + set { adjustment.upper = value; } + } + + public double step { + get { return adjustment.step_increment; } + set { adjustment.step_increment = value; } + } + + // TODO: marks +} diff --git a/lib/astal/gtk3/src/widget/stack.vala b/lib/astal/gtk3/src/widget/stack.vala new file mode 100644 index 0000000..02f9959 --- /dev/null +++ b/lib/astal/gtk3/src/widget/stack.vala @@ -0,0 +1,26 @@ +public class Astal.Stack : Gtk.Stack { + public string shown { + get { return visible_child_name; } + set { visible_child_name = value; } + } + + public List children { + set { _set_children(value); } + owned get { return get_children(); } + } + + private void _set_children(List arr) { + foreach(var child in get_children()) { + remove(child); + } + + var i = 0; + foreach(var child in arr) { + if (child.name != null) { + add_named(child, child.name); + } else { + add_named(child, (++i).to_string()); + } + } + } +} diff --git a/lib/astal/gtk3/src/widget/widget.vala b/lib/astal/gtk3/src/widget/widget.vala new file mode 100644 index 0000000..2506bc8 --- /dev/null +++ b/lib/astal/gtk3/src/widget/widget.vala @@ -0,0 +1,157 @@ +namespace Astal { +private class Css { + private static HashTable _providers; + public static HashTable providers { + get { + if (_providers == null) { + _providers = new HashTable( + (w) => (uint)w, + (a, b) => a == b); + } + + return _providers; + } + } +} + +private void remove_provider(Gtk.Widget widget) { + var providers = Css.providers; + + if (providers.contains(widget)) { + var p = providers.get(widget); + widget.get_style_context().remove_provider(p); + providers.remove(widget); + p.dispose(); + } +} + +public void widget_set_css(Gtk.Widget widget, string css) { + var providers = Css.providers; + + if (providers.contains(widget)) { + remove_provider(widget); + } else { + widget.destroy.connect(() => { + remove_provider(widget); + }); + } + + var style = !css.contains("{") || !css.contains("}") + ? "* { ".concat(css, "}") : css; + + var p = new Gtk.CssProvider(); + widget.get_style_context() + .add_provider(p, Gtk.STYLE_PROVIDER_PRIORITY_USER); + + try { + p.load_from_data(style, style.length); + providers.set(widget, p); + } catch (Error err) { + warning(err.message); + } +} + +public string widget_get_css(Gtk.Widget widget) { + var providers = Css.providers; + + if (providers.contains(widget)) + return providers.get(widget).to_string(); + + return ""; +} + +public void widget_set_class_names(Gtk.Widget widget, string[] class_names) { + foreach (var name in widget_get_class_names(widget)) + widget_toggle_class_name(widget, name, false); + + foreach (var name in class_names) + widget_toggle_class_name(widget, name, true); +} + +public List widget_get_class_names(Gtk.Widget widget) { + return widget.get_style_context().list_classes(); +} + +public void widget_toggle_class_name( + Gtk.Widget widget, + string class_name, + bool condition = true +) { + var c = widget.get_style_context(); + if (condition) + c.add_class(class_name); + else + c.remove_class(class_name); +} + +private class Cursor { + private static HashTable _cursors; + public static HashTable cursors { + get { + if (_cursors == null) { + _cursors = new HashTable( + (w) => (uint)w, + (a, b) => a == b); + } + return _cursors; + } + } +} + +private void widget_setup_cursor(Gtk.Widget widget) { + widget.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK); + widget.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK); + widget.enter_notify_event.connect(() => { + widget.get_window().set_cursor( + new Gdk.Cursor.from_name( + Gdk.Display.get_default(), + Cursor.cursors.get(widget))); + return false; + }); + widget.leave_notify_event.connect(() => { + widget.get_window().set_cursor( + new Gdk.Cursor.from_name( + Gdk.Display.get_default(), + "default")); + return false; + }); + widget.destroy.connect(() => { + if (Cursor.cursors.contains(widget)) + Cursor.cursors.remove(widget); + }); +} + +public void widget_set_cursor(Gtk.Widget widget, string cursor) { + if (!Cursor.cursors.contains(widget)) + widget_setup_cursor(widget); + + Cursor.cursors.set(widget, cursor); +} + +public string widget_get_cursor(Gtk.Widget widget) { + return Cursor.cursors.get(widget); +} + +private class ClickThrough { + private static HashTable _click_through; + public static HashTable click_through { + get { + if (_click_through == null) { + _click_through = new HashTable( + (w) => (uint)w, + (a, b) => a == b); + } + return _click_through; + } + } +} + +public void widget_set_click_through(Gtk.Widget widget, bool click_through) { + ClickThrough.click_through.set(widget, click_through); + widget.input_shape_combine_region(click_through ? new Cairo.Region() : null); +} + +public bool widget_get_click_through(Gtk.Widget widget) { + return ClickThrough.click_through.get(widget); +} +} diff --git a/lib/astal/gtk3/src/widget/window.vala b/lib/astal/gtk3/src/widget/window.vala new file mode 100644 index 0000000..946e766 --- /dev/null +++ b/lib/astal/gtk3/src/widget/window.vala @@ -0,0 +1,246 @@ +using GtkLayerShell; + +public enum Astal.WindowAnchor { + NONE = 0, + TOP = 1, + RIGHT = 2, + LEFT = 4, + BOTTOM = 8, +} + +public enum Astal.Exclusivity { + NORMAL, + EXCLUSIVE, + IGNORE, +} + +public enum Astal.Layer { + BACKGROUND = 0, // GtkLayerShell.Layer.BACKGROUND + BOTTOM = 1, // GtkLayerShell.Layer.BOTTOM + TOP = 2, // GtkLayerShell.Layer.TOP + OVERLAY = 3, // GtkLayerShell.Layer.OVERLAY +} + +public enum Astal.Keymode { + NONE = 0, // GtkLayerShell.KeyboardMode.NONE + ON_DEMAND = 1, // GtkLayerShell.KeyboardMode.ON_DEMAND + EXCLUSIVE = 2, // GtkLayerShell.KeyboardMode.EXCLUSIVE +} + +public class Astal.Window : Gtk.Window { + private static bool check(string action) { + if (!is_supported()) { + critical(@"can not $action on window: layer shell not supported"); + print("tip: running from an xwayland terminal can cause this, for example VsCode"); + return true; + } + return false; + } + + private InhibitManager? inhibit_manager; + private Inhibitor? inhibitor; + + construct { + if (check("initialize layer shell")) + return; + + height_request = 1; + width_request = 1; + init_for_window(this); + inhibit_manager = InhibitManager.get_default(); + } + + public bool inhibit { + set { + if (inhibit_manager == null) { + return; + } + if (value && inhibitor == null) { + inhibitor = inhibit_manager.inhibit(this); + } + else if (!value && inhibitor != null) { + inhibitor = null; + } + } + get { + return inhibitor != null; + } + } + + public override void show() { + base.show(); + if(inhibit) { + inhibitor = inhibit_manager.inhibit(this); + } + } + + public string namespace { + get { return get_namespace(this); } + set { set_namespace(this, value); } + } + + public int anchor { + set { + if (check("set anchor")) + return; + + set_anchor(this, Edge.TOP, WindowAnchor.TOP in value); + set_anchor(this, Edge.BOTTOM, WindowAnchor.BOTTOM in value); + set_anchor(this, Edge.LEFT, WindowAnchor.LEFT in value); + set_anchor(this, Edge.RIGHT, WindowAnchor.RIGHT in value); + } + get { + var a = WindowAnchor.NONE; + if (get_anchor(this, Edge.TOP)) + a = a | WindowAnchor.TOP; + + if (get_anchor(this, Edge.RIGHT)) + a = a | WindowAnchor.RIGHT; + + if (get_anchor(this, Edge.LEFT)) + a = a | WindowAnchor.LEFT; + + if (get_anchor(this, Edge.BOTTOM)) + a = a | WindowAnchor.BOTTOM; + + return a; + } + } + + public Exclusivity exclusivity { + set { + if (check("set exclusivity")) + return; + + switch (value) { + case Exclusivity.NORMAL: + set_exclusive_zone(this, 0); + break; + case Exclusivity.EXCLUSIVE: + auto_exclusive_zone_enable(this); + break; + case Exclusivity.IGNORE: + set_exclusive_zone(this, -1); + break; + } + } + get { + if (auto_exclusive_zone_is_enabled(this)) + return Exclusivity.EXCLUSIVE; + + if (get_exclusive_zone(this) == -1) + return Exclusivity.IGNORE; + + return Exclusivity.NORMAL; + } + } + + public Layer layer { + get { return (Layer)get_layer(this); } + set { + if (check("set layer")) + return; + + set_layer(this, (GtkLayerShell.Layer)value); + } + } + + public Keymode keymode { + get { return (Keymode)get_keyboard_mode(this); } + set { + if (check("set keymode")) + return; + + set_keyboard_mode(this, (GtkLayerShell.KeyboardMode)value); + } + } + + public Gdk.Monitor gdkmonitor { + get { return get_monitor(this); } + set { + if (check("set gdkmonitor")) + return; + + set_monitor (this, value); + } + } + + public new int margin_top { + get { return GtkLayerShell.get_margin(this, Edge.TOP); } + set { + if (check("set margin_top")) + return; + + GtkLayerShell.set_margin(this, Edge.TOP, value); + } + } + + public new int margin_bottom { + get { return GtkLayerShell.get_margin(this, Edge.BOTTOM); } + set { + if (check("set margin_bottom")) + return; + + GtkLayerShell.set_margin(this, Edge.BOTTOM, value); + } + } + + public new int margin_left { + get { return GtkLayerShell.get_margin(this, Edge.LEFT); } + set { + if (check("set margin_left")) + return; + + GtkLayerShell.set_margin(this, Edge.LEFT, value); + } + } + + public new int margin_right { + get { return GtkLayerShell.get_margin(this, Edge.RIGHT); } + set { + if (check("set margin_right")) + return; + + GtkLayerShell.set_margin(this, Edge.RIGHT, value); + } + } + + public new int margin { + set { + if (check("set margin")) + return; + + margin_top = value; + margin_right = value; + margin_bottom = value; + margin_left = value; + } + } + + /** + * CAUTION: the id might not be the same mapped by the compositor + * to reset and let the compositor map it pass a negative number + */ + public int monitor { + set { + if (check("set monitor")) + return; + + if (value < 0) + set_monitor(this, (Gdk.Monitor)null); + + var m = Gdk.Display.get_default().get_monitor(value); + set_monitor(this, m); + } + get { + var m = get_monitor(this); + var d = Gdk.Display.get_default(); + for (var i = 0; i < d.get_n_monitors(); ++i) { + if (m == d.get_monitor(i)) + return i; + } + + return -1; + } + } +} diff --git a/lib/astal/gtk3/version b/lib/astal/gtk3/version new file mode 100644 index 0000000..4a36342 --- /dev/null +++ b/lib/astal/gtk3/version @@ -0,0 +1 @@ +3.0.0 -- cgit v1.2.3 From 2f71cd4c08bb4514efe43533e6a5d03535204c29 Mon Sep 17 00:00:00 2001 From: Aylur Date: Tue, 15 Oct 2024 01:26:32 +0200 Subject: refactor lua and gjs lib --- lib/astal/gtk3/src/application.vala | 68 +++++++++++++++---------------------- lib/astal/io/application.vala | 12 +++++-- 2 files changed, 36 insertions(+), 44 deletions(-) (limited to 'lib') diff --git a/lib/astal/gtk3/src/application.vala b/lib/astal/gtk3/src/application.vala index 8539aa0..2255333 100644 --- a/lib/astal/gtk3/src/application.vala +++ b/lib/astal/gtk3/src/application.vala @@ -3,8 +3,9 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { private List css_providers = new List(); private SocketService service; private DBusConnection conn; - private string socket_path { get; set; } - private string _instance_name; + private string _instance_name = "astal"; + + public string socket_path { get; private set; } [DBus (visible=false)] public signal void monitor_added(Gdk.Monitor monitor); @@ -34,8 +35,8 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { public string instance_name { owned get { return _instance_name; } construct set { - application_id = "io.Astal." + value; - _instance_name = value; + _instance_name = value != null ? value : "astal"; + application_id = @"io.Astal.$_instance_name"; } } @@ -138,54 +139,39 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { AstalIO.write_sock.begin(conn, @"missing response implementation on $application_id"); } - /** - * should be called before `run()` - * the return value indicates if instance is already running - */ [DBus (visible=false)] - public void acquire_socket() { - try { - service = AstalIO.acquire_socket(this); - - Bus.own_name( - BusType.SESSION, - "io.Astal." + instance_name, - BusNameOwnerFlags.NONE, - (conn) => { - try { - this.conn = conn; - conn.register_object("/io/Astal/Application", this); - } catch (Error err) { - critical(err.message); - } - }, - () => {}, - () => {} - ); - } catch (Error err) { - critical("could not acquire socket %s\n", application_id); - critical(err.message); - } - } + public void acquire_socket() throws Error { + string path; + service = AstalIO.acquire_socket(this, out path); + socket_path = path; - public new void quit() throws DBusError, IOError { - if (service != null) { - if (FileUtils.test(socket_path, GLib.FileTest.EXISTS)){ + Bus.own_name( + BusType.SESSION, + application_id, + BusNameOwnerFlags.NONE, + (conn) => { try { - File.new_for_path(socket_path).delete(null); + this.conn = conn; + conn.register_object("/io/Astal/Application", this); } catch (Error err) { - warning(err.message); + critical(err.message); } - } + }, + () => {}, + () => {} + ); + } + + public new void quit() throws DBusError, IOError { + if (service != null) { + service.stop(); + service.close(); } base.quit(); } construct { - if (instance_name == null) - instance_name = "astal"; - activate.connect(() => { var display = Gdk.Display.get_default(); display.monitor_added.connect((mon) => { diff --git a/lib/astal/io/application.vala b/lib/astal/io/application.vala index 00bef57..60318ed 100644 --- a/lib/astal/io/application.vala +++ b/lib/astal/io/application.vala @@ -14,7 +14,7 @@ public interface Application : Object { public abstract void request(string msg, SocketConnection conn) throws Error; } -public SocketService acquire_socket(Application app) throws Error { +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) { @@ -23,7 +23,13 @@ public SocketService acquire_socket(Application app) throws Error { } var rundir = Environment.get_user_runtime_dir(); - var path = @"$rundir/$name.sock"; + 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 { @@ -123,7 +129,7 @@ public static void toggle_window_by_name(string instance, string window) { 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 socket_path = @"$rundir/astal/$instance_name.sock"; var client = new SocketClient(); try { -- cgit v1.2.3 From dde50e333a237efb4ddaf522398dedf1a69999d1 Mon Sep 17 00:00:00 2001 From: Aylur Date: Tue, 15 Oct 2024 11:47:26 +0000 Subject: docs: update references flake --- lib/astal/gtk3/src/application.vala | 9 +++------ lib/astal/io/application.vala | 4 +++- 2 files changed, 6 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/astal/gtk3/src/application.vala b/lib/astal/gtk3/src/application.vala index 2255333..1210d88 100644 --- a/lib/astal/gtk3/src/application.vala +++ b/lib/astal/gtk3/src/application.vala @@ -4,8 +4,7 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { private SocketService service; private DBusConnection conn; private string _instance_name = "astal"; - - public string socket_path { get; private set; } + private string socket_path { get; private set; } [DBus (visible=false)] public signal void monitor_added(Gdk.Monitor monitor); @@ -45,13 +44,11 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { get { return get_windows(); } } - [DBus (visible=false)] - public Gtk.Settings settings { + private Gtk.Settings settings { get { return Gtk.Settings.get_default(); } } - [DBus (visible=false)] - public Gdk.Screen screen { + private Gdk.Screen screen { get { return Gdk.Screen.get_default(); } } diff --git a/lib/astal/io/application.vala b/lib/astal/io/application.vala index 60318ed..b32de34 100644 --- a/lib/astal/io/application.vala +++ b/lib/astal/io/application.vala @@ -11,7 +11,9 @@ public interface Application : Object { 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 virtual void request(string msg, SocketConnection conn) throws Error { + write_sock.begin(conn, @"missing response implementation on $instance_name"); + } } public SocketService acquire_socket(Application app, out string sock) throws Error { -- cgit v1.2.3 From 0cb8733f31defcf2f7158d23c058fbb92a580215 Mon Sep 17 00:00:00 2001 From: Aylur Date: Tue, 15 Oct 2024 18:58:12 +0000 Subject: docs: apps doc comments docs: apps doc comments --- lib/apps/application.vala | 55 +++++++++++++++++++++++++++++++++-------- lib/apps/apps.vala | 63 ++++++++++++++++++++++++++++++++++++++++++++--- lib/apps/fuzzy.vala | 4 +-- 3 files changed, 105 insertions(+), 17 deletions(-) (limited to 'lib') diff --git a/lib/apps/application.vala b/lib/apps/application.vala index 75ff6b2..7e20b06 100644 --- a/lib/apps/application.vala +++ b/lib/apps/application.vala @@ -1,12 +1,45 @@ -namespace AstalApps { -public class Application : Object { +public class AstalApps.Application : Object { + /** + * The underlying DesktopAppInfo. + */ public DesktopAppInfo app { get; construct set; } + + /** + * The number of times [func@AstalApps.Application.launch] was called on this Application. + */ public int frequency { get; set; default = 0; } + + /** + * The name of this Application. + */ public string name { get { return app.get_name(); } } + + /** + * Name of the .desktop of this Application. + */ public string entry { get { return app.get_id(); } } + + /** + * Description of this Application. + */ public string description { get { return app.get_description(); } } + + /** + * The StartupWMClass field from the desktop file. + * This represents the WM_CLASS property of the main window of the application. + */ public string wm_class { get { return app.get_startup_wm_class(); } } + + /** + * `Exec` field from the desktop file. + * Note that if you want to launch this Application you should use the [func@AstalApps.Application.launch] method. + */ public string executable { owned get { return app.get_string("Exec"); } } + + /** + * `Icon` field from the desktop file. + * This is usually a named icon or a path to a file. + */ public string icon_name { owned get { return app.get_string("Icon"); } } internal Application(string id, int? frequency = 0) { @@ -14,10 +47,17 @@ public class Application : Object { this.frequency = frequency; } + /** + * Get a value from the .desktop file by its key. + */ public string get_key(string key) { return app.get_string(key); } + /** + * Launches this application. + * The launched application inherits the environment of the launching process + */ public bool launch() { try { var s = app.launch(null, null); @@ -29,7 +69,7 @@ public class Application : Object { } } - public Score fuzzy_match(string term) { + internal Score fuzzy_match(string term) { var score = Score(); if (name != null) score.name = fuzzy_match_string(term, name); @@ -43,7 +83,7 @@ public class Application : Object { return score; } - public Score exact_match(string term) { + internal Score exact_match(string term) { var score = Score(); if (name != null) score.name = name.down().contains(term.down()) ? 1 : 0; @@ -71,14 +111,9 @@ public class Application : Object { } } -int min3(int a, int b, int c) { - return (a < b) ? ((a < c) ? a : c) : ((b < c) ? b : c); -} - -public struct Score { +public struct AstalApps.Score { int name; int entry; int executable; int description; } -} diff --git a/lib/apps/apps.vala b/lib/apps/apps.vala index b07961e..c96a2b2 100644 --- a/lib/apps/apps.vala +++ b/lib/apps/apps.vala @@ -1,23 +1,70 @@ -namespace AstalApps { -public class Apps : Object { +public class AstalApps.Apps : Object { private string cache_directory; private string cache_file; private List _list; private HashTable frequents { get; private set; } + /** + * Indicates wether hidden applications should included in queries. + */ public bool show_hidden { get; set; } + + /** + * Full list of available applications. + */ public List list { owned get { return _list.copy(); } } + /** + * The minimum score the application has to meet in order to be included in queries. + */ public int min_score { get; set; default = 0; } + /** + * Extra multiplier to apply when matching the `name` of an application. + * Defaults to `2` + */ public double name_multiplier { get; set; default = 2; } + + /** + * Extra multiplier to apply when matching the entry of an application. + * Defaults to `1` + */ public double entry_multiplier { get; set; default = 1; } + + /** + * Extra multiplier to apply when matching the executable of an application. + * Defaults to `1` + */ public double executable_multiplier { get; set; default = 1; } + + /** + * Extra multiplier to apply when matching the description of an application. + * Defaults to `0.5` + */ public double description_multiplier { get; set; default = 0.5; } + /** + * Consider the name of an application during queries. + * Defaults to `true` + */ public bool include_name { get; set; default = true; } + + /** + * Consider the entry of an application during queries. + * Defaults to `false` + */ public bool include_entry { get; set; default = false; } + + /** + * Consider the executable of an application during queries. + * Defaults to `false` + */ public bool include_executable { get; set; default = false; } + + /** + * Consider the description of an application during queries. + * Defaults to `false` + */ public bool include_description { get; set; default = false; } construct { @@ -65,7 +112,7 @@ public class Apps : Object { return r; } - public List query(string? search = "", bool exact = false) { + internal List query(string? search = "", bool exact = false) { if (search == null) search = ""; @@ -114,14 +161,23 @@ public class Apps : Object { return arr; } + /** + * Query the `list` of applications with a fuzzy matching algorithm. + */ public List fuzzy_query(string? search = "") { return query(search, false); } + /** + * Query the `list` of applications with a simple string matching algorithm. + */ public List exact_query(string? search = "") { return query(search, true); } + /** + * Reload the `list` of Applications. + */ public void reload() { var arr = AppInfo.get_all(); @@ -169,4 +225,3 @@ public class Apps : Object { } } } -} diff --git a/lib/apps/fuzzy.vala b/lib/apps/fuzzy.vala index f93b2eb..97277bd 100644 --- a/lib/apps/fuzzy.vala +++ b/lib/apps/fuzzy.vala @@ -1,11 +1,9 @@ - namespace AstalApps { - private int max(int a, int b) { return a > b ? a : b; } -public int fuzzy_match_string(string pattern, string str) { +private int fuzzy_match_string(string pattern, string str) { const int unmatched_letter_penalty = -1; int score = 100; -- cgit v1.2.3 From ecfbf082bfab22e34d8036896f51069ce0c18302 Mon Sep 17 00:00:00 2001 From: Aylur Date: Tue, 15 Oct 2024 20:58:49 +0000 Subject: docs: astal-io doc comments --- lib/astal/io/application.vala | 36 ++++++++++++++++++++- lib/astal/io/cli.vala | 16 ++++----- lib/astal/io/file.vala | 17 ++++++++++ lib/astal/io/process.vala | 75 ++++++++++++++++++++++++++++++++++++------- lib/astal/io/time.vala | 44 +++++++++++++++++++++++-- lib/astal/io/variable.vala | 4 +++ 6 files changed, 170 insertions(+), 22 deletions(-) (limited to 'lib') diff --git a/lib/astal/io/application.vala b/lib/astal/io/application.vala index b32de34..60a7d3e 100644 --- a/lib/astal/io/application.vala +++ b/lib/astal/io/application.vala @@ -4,6 +4,10 @@ public errordomain AppError { 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; @@ -16,6 +20,10 @@ public interface Application : Object { } } +/** + * Starts a [class@GLib.SocketService] and binds `XDG_RUNTIME_DIR/astal/.sock`. + * This socket is then used by the astal cli. Not meant for public usage, but for [func@AstalIO.Application.acquire_socket]. + */ public SocketService acquire_socket(Application app, out string sock) throws Error { var name = app.instance_name; foreach (var instance in get_instances()) { @@ -65,6 +73,10 @@ public SocketService acquire_socket(Application app, out string sock) throws Err return service; } +/** + * Get a list of running Astal.Application instances. + * It is the equivalent of `astal --list`. + */ public static List get_instances() { var list = new List(); var prefix = "io.Astal."; @@ -87,6 +99,10 @@ public static List get_instances() { return list; } +/** + * Quit an an Astal instances. + * It is the equivalent of `astal --quit -i instance`. + */ public static void quit_instance(string instance) { try { IApplication proxy = Bus.get_proxy_sync( @@ -101,6 +117,10 @@ public static void quit_instance(string instance) { } } +/** + * 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) { try { IApplication proxy = Bus.get_proxy_sync( @@ -115,6 +135,10 @@ public static void open_inspector(string instance) { } } +/** + * 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) { try { IApplication proxy = Bus.get_proxy_sync( @@ -129,7 +153,11 @@ public static void toggle_window_by_name(string instance, string window) { } } -public static string send_message(string instance_name, string msg) { +/** + * 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) { var rundir = Environment.get_user_runtime_dir(); var socket_path = @"$rundir/astal/$instance_name.sock"; var client = new SocketClient(); @@ -146,11 +174,17 @@ public static string send_message(string instance_name, string msg) { } } +/** + * 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.concat("\x04").data, Priority.DEFAULT); } diff --git a/lib/astal/io/cli.vala b/lib/astal/io/cli.vala index 1db0b2e..8fc0523 100644 --- a/lib/astal/io/cli.vala +++ b/lib/astal/io/cli.vala @@ -1,12 +1,12 @@ -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; +static bool version; +static bool help; +static bool list; +static bool quit; +static bool inspector; +static string? toggle_window; +static string? instance_name; -private const OptionEntry[] options = { +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 }, diff --git a/lib/astal/io/file.vala b/lib/astal/io/file.vala index b2d480c..e57f449 100644 --- a/lib/astal/io/file.vala +++ b/lib/astal/io/file.vala @@ -1,4 +1,7 @@ namespace AstalIO { +/** + * Read the contents of a file synchronously. + */ public string read_file(string path) { var str = ""; try { @@ -9,12 +12,18 @@ public string read_file(string path) { 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); @@ -23,6 +32,9 @@ public void write_file(string path, string content) { } } +/** + * 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, @@ -33,6 +45,11 @@ public async void write_file_async(string path, string content) throws Error { 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 [enum@GLib.FileMonitorEvent] indicating the reason. + */ public FileMonitor? monitor_file(string path, Closure callback) { try { var file = File.new_for_path(path); diff --git a/lib/astal/io/process.vala b/lib/astal/io/process.vala index e8637ab..8767012 100644 --- a/lib/astal/io/process.vala +++ b/lib/astal/io/process.vala @@ -1,3 +1,6 @@ +/** + * `Process` provides shortcuts for [class@GLib.Subprocess] 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) => { @@ -23,34 +26,56 @@ public class AstalIO.Process : Object { 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); } - 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); - } - } - ); + /** + * 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, @@ -65,12 +90,22 @@ public class AstalIO.Process : Object { read_stream(err_stream, false); } + /** + * Start a new subprocess with the given command + * which is parsed using [func@Shell.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, @@ -88,12 +123,24 @@ public class AstalIO.Process : Object { throw new IOError.FAILED(err_str.strip()); } + /** + * Execute a command synchronously. + * The command is parsed using [func@Shell.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, @@ -111,6 +158,12 @@ public class AstalIO.Process : Object { throw new IOError.FAILED(err_str.strip()); } + /** + * Execute a command asynchronously. + * The command is parsed using [func@Shell.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); diff --git a/lib/astal/io/time.vala b/lib/astal/io/time.vala index 1446441..29e7e1f 100644 --- a/lib/astal/io/time.vala +++ b/lib/astal/io/time.vala @@ -1,10 +1,21 @@ +/** + * `Time` provides shortcuts for GLib timeout functions. + */ public class AstalIO.Time : Object { - public signal void now (); - public signal void cancelled (); 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(() => { @@ -26,6 +37,9 @@ public class AstalIO.Time : Object { }); } + /** + * Start an interval timer with a [enum@GLib.Priority]. + */ public Time.interval_prio(uint interval, int prio = Priority.DEFAULT, Closure? fn) { connect_closure(fn); Idle.add_once(() => now()); @@ -35,6 +49,9 @@ public class AstalIO.Time : Object { }, prio); } + /** + * Start a timeout timer with a [enum@GLib.Priority]. + */ public Time.timeout_prio(uint timeout, int prio = Priority.DEFAULT, Closure? fn) { connect_closure(fn); timeout_id = Timeout.add(timeout, () => { @@ -44,6 +61,9 @@ public class AstalIO.Time : Object { }, prio); } + /** + * Start an idle timer with a [enum@GLib.Priority]. + */ public Time.idle_prio(int prio = Priority.DEFAULT_IDLE, Closure? fn) { connect_closure(fn); timeout_id = Idle.add(() => { @@ -53,18 +73,38 @@ public class AstalIO.Time : Object { }, 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 [signal@AstalIO.Time::cancelled] + */ public void cancel() { cancellable.cancel(); } diff --git a/lib/astal/io/variable.vala b/lib/astal/io/variable.vala index 2a395b4..312a27a 100644 --- a/lib/astal/io/variable.vala +++ b/lib/astal/io/variable.vala @@ -1,3 +1,7 @@ +/* + * Base class for [class@AstalIO.Variable] 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 (); -- cgit v1.2.3 From 2ad95e05d83a455bb30503ca4ca0aa8356ea5ff7 Mon Sep 17 00:00:00 2001 From: kotontrion Date: Wed, 16 Oct 2024 09:49:37 +0200 Subject: apps: fuzzy: reduce panalty for not matching --- lib/apps/apps.vala | 3 +-- lib/apps/fuzzy.vala | 30 ++++++++++++++++-------------- 2 files changed, 17 insertions(+), 16 deletions(-) (limited to 'lib') diff --git a/lib/apps/apps.vala b/lib/apps/apps.vala index b07961e..acab155 100644 --- a/lib/apps/apps.vala +++ b/lib/apps/apps.vala @@ -8,7 +8,7 @@ public class Apps : Object { public bool show_hidden { get; set; } public List list { owned get { return _list.copy(); } } - public int min_score { get; set; default = 0; } + public double min_score { get; set; default = 0; } public double name_multiplier { get; set; default = 2; } public double entry_multiplier { get; set; default = 1; } @@ -61,7 +61,6 @@ public class Apps : Object { r += am.executable * executable_multiplier; if (include_description) r += am.description * description_multiplier; - return r; } diff --git a/lib/apps/fuzzy.vala b/lib/apps/fuzzy.vala index f93b2eb..b77fba7 100644 --- a/lib/apps/fuzzy.vala +++ b/lib/apps/fuzzy.vala @@ -1,10 +1,6 @@ namespace AstalApps { -private int max(int a, int b) { - return a > b ? a : b; -} - public int fuzzy_match_string(string pattern, string str) { const int unmatched_letter_penalty = -1; int score = 100; @@ -12,14 +8,17 @@ public int fuzzy_match_string(string pattern, string str) { if (pattern.length == 0) return score; if (str.length < pattern.length) return int.MIN; + bool found = fuzzy_match_recurse(pattern, str, score, true, out score); score += unmatched_letter_penalty * (str.length - pattern.length); - score = fuzzy_match_recurse(pattern, str, score, true); + + if(!found) score = -10; return score; } -private int fuzzy_match_recurse(string pattern, string str, int score, bool first_char) { - if (pattern.length == 0) return score; +private bool fuzzy_match_recurse(string pattern, string str, int score, bool first_char, out int result) { + result = score; + if (pattern.length == 0) return true; int match_idx = 0; int offset = 0; @@ -28,16 +27,19 @@ private int fuzzy_match_recurse(string pattern, string str, int score, bool firs while ((match_idx = str.casefold().substring(offset).index_of_char(search)) >= 0) { offset += match_idx; - int subscore = fuzzy_match_recurse( + int subscore; + bool found = fuzzy_match_recurse( pattern.substring(1), str.substring(offset + 1), - compute_score(offset, first_char, str, offset), false); - best_score = max(best_score, subscore); + compute_score(offset, first_char, str, offset), false, out subscore); + if(!found) break; + best_score = int.max(best_score, subscore); offset++; } - - if (best_score == int.MIN) return int.MIN; - return score + best_score; + + if (best_score == int.MIN) return false; + result += best_score; + return true; } private int compute_score(int jump, bool first_char, string match, int idx) { @@ -65,7 +67,7 @@ private int compute_score(int jump, bool first_char, string match, int idx) { score += first_letter_bonus; } if (first_char) { - score += max(leading_letter_penalty * jump, max_leading_letter_penalty); + score += int.max(leading_letter_penalty * jump, max_leading_letter_penalty); } return score; -- cgit v1.2.3 From 8368b20eda96674e0f5a502b92d3afb936b71b0b Mon Sep 17 00:00:00 2001 From: Aylur Date: Wed, 16 Oct 2024 10:28:24 +0000 Subject: fix io send_message --- lib/astal/io/application.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/astal/io/application.vala b/lib/astal/io/application.vala index 60a7d3e..a420128 100644 --- a/lib/astal/io/application.vala +++ b/lib/astal/io/application.vala @@ -159,7 +159,7 @@ public static void toggle_window_by_name(string instance, string window) { */ public static string send_message(string instance, string msg) { var rundir = Environment.get_user_runtime_dir(); - var socket_path = @"$rundir/astal/$instance_name.sock"; + var socket_path = @"$rundir/astal/$instance.sock"; var client = new SocketClient(); try { -- cgit v1.2.3 From bfb7e27c1f9d099fcc457379f0317b1cea43fc37 Mon Sep 17 00:00:00 2001 From: Aylur Date: Sun, 20 Oct 2024 22:49:44 +0000 Subject: fix #51 keymode enum --- lib/astal/gtk3/src/widget/window.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/astal/gtk3/src/widget/window.vala b/lib/astal/gtk3/src/widget/window.vala index 946e766..2690365 100644 --- a/lib/astal/gtk3/src/widget/window.vala +++ b/lib/astal/gtk3/src/widget/window.vala @@ -23,8 +23,8 @@ public enum Astal.Layer { public enum Astal.Keymode { NONE = 0, // GtkLayerShell.KeyboardMode.NONE - ON_DEMAND = 1, // GtkLayerShell.KeyboardMode.ON_DEMAND - EXCLUSIVE = 2, // GtkLayerShell.KeyboardMode.EXCLUSIVE + EXCLUSIVE = 1, // GtkLayerShell.KeyboardMode.EXCLUSIVE + ON_DEMAND = 2, // GtkLayerShell.KeyboardMode.ON_DEMAND } public class Astal.Window : Gtk.Window { -- cgit v1.2.3 From 9a6c776f8fb145a602bcfe9046955d0d2f268416 Mon Sep 17 00:00:00 2001 From: Aylur Date: Tue, 22 Oct 2024 18:43:17 +0000 Subject: docs: notifd doc comments --- lib/gir.py | 43 +++++++++++++++++++++ lib/notifd/gir.py | 1 + lib/notifd/meson.build | 90 +++++++++----------------------------------- lib/notifd/notifd.vala | 8 ++-- lib/notifd/notification.vala | 34 +++++++++++------ 5 files changed, 87 insertions(+), 89 deletions(-) create mode 100644 lib/gir.py create mode 120000 lib/notifd/gir.py (limited to 'lib') diff --git a/lib/gir.py b/lib/gir.py new file mode 100644 index 0000000..8ec786f --- /dev/null +++ b/lib/gir.py @@ -0,0 +1,43 @@ +""" +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(gir: 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("", "").replace("", "") + ) + + tree.write(gir, encoding="utf-8", xml_declaration=True) + + +def valadoc(gir: str, args: list[str]): + subprocess.run(["valadoc", "-o", "docs", "--gir", gir, *args]) + + +if __name__ == "__main__": + gir = sys.argv[1] + args = sys.argv[2:] + + valadoc(gir, args) + fix_gir(gir) diff --git a/lib/notifd/gir.py b/lib/notifd/gir.py new file mode 120000 index 0000000..b5b4f1d --- /dev/null +++ b/lib/notifd/gir.py @@ -0,0 +1 @@ +../gir.py \ No newline at end of file diff --git a/lib/notifd/meson.build b/lib/notifd/meson.build index b6ef59a..e09a371 100644 --- a/lib/notifd/meson.build +++ b/lib/notifd/meson.build @@ -3,7 +3,7 @@ project( 'vala', 'c', version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), - meson_version: '>= 0.62.0', + meson_version: '>= 0.63.0', default_options: [ 'warning_level=2', 'werror=false', @@ -53,84 +53,28 @@ if get_option('lib') meson.project_name(), sources, dependencies: deps, - vala_args: ['--vapi-comments', '--ccode'], + vala_args: ['--vapi-comments'], 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], + install_dir: [true, true, true], ) - # import('gnome').generate_gir( - # lib, - # sources: [], - # nsversion: api_version, - # namespace: namespace, - # symbol_prefix: meson.project_name().replace('-', '_'), - # identifier_prefix: namespace, - # includes: ['GObject-2.0'], - # header: meson.project_name() + '.h', - # export_packages: meson.project_name() + '-' + api_version, - # install: true, - # ) + pkgs = [] + foreach dep : deps + pkgs += ['--pkg=' + dep.name()] + endforeach - # custom_target( - # gir, - # command: [ - # find_program('g-ir-scanner'), - # '--namespace=' + namespace, - # '--nsversion=' + api_version, - # '--library=' + meson.project_name(), - # '--include=GObject-2.0', - # '--output=' + gir, - # '--symbol-prefix=' + meson.project_name().replace('-', '_'), - # '--identifier-prefix=' + namespace, - # ] - # + pkgs - # + ['@INPUT@'], - # output: gir, - # depends: lib, - # input: meson.current_build_dir() / meson.project_name() + '.h', - # install: true, - # install_dir: get_option('datadir') / 'gir-1.0', - # ) - - # custom_target( - # gir, - # command: [ - # find_program('g-ir-scanner'), - # '--namespace=' + namespace, - # '--nsversion=' + api_version, - # '--library=' + meson.project_name(), - # '--include=GObject-2.0', - # '--output=' + gir, - # '--symbol-prefix=' + meson.project_name().replace('-', '_'), - # '--identifier-prefix=' + namespace, - # ] - # + pkgs - # + ['@INPUT@'], - # input: lib.extract_all_objects(), - # output: gir, - # depends: lib, - # install: true, - # install_dir: get_option('datadir') / 'gir-1.0', - # ) - - # pkgs = [] - # foreach dep : deps - # pkgs += ['--pkg=' + dep.name()] - # endforeach - # - # gir_tgt = custom_target( - # gir, - # command: [find_program('valadoc'), '-o', 'docs', '--gir', gir] + pkgs + sources, - # input: sources, - # depends: lib, - # output: gir, - # install: true, - # install_dir: get_option('datadir') / 'gir-1.0', - # ) + gir_tgt = custom_target( + gir, + command: [find_program('python3'), files('gir.py'), gir] + pkgs + sources, + input: sources, + depends: lib, + output: gir, + install: true, + install_dir: get_option('datadir') / 'gir-1.0', + ) custom_target( typelib, @@ -142,7 +86,7 @@ if get_option('lib') ], input: lib, output: typelib, - depends: lib, + depends: [lib, gir_tgt], install: true, install_dir: get_option('libdir') / 'girepository-1.0', ) diff --git a/lib/notifd/notifd.vala b/lib/notifd/notifd.vala index 6ca25fa..807e40a 100644 --- a/lib/notifd/notifd.vala +++ b/lib/notifd/notifd.vala @@ -1,5 +1,5 @@ /** - * Get the singleton instance of {@link Notifd} + * Get the singleton instance of [class@AstalNotifd.Notifd] */ namespace AstalNotifd { public Notifd get_default() { @@ -74,7 +74,7 @@ public class AstalNotifd.Notifd : Object { } /** - * Gets the {@link Notification} with id or null if there is no such Notification. + * Gets the [class@AstalNotifd.Notification] with id or null if there is no such Notification. */ public Notification get_notification(uint id) { return proxy != null ? proxy.get_notification(id) : daemon.get_notification(id); @@ -85,7 +85,7 @@ public class AstalNotifd.Notifd : Object { } /** - * Emitted when the daemon receives a {@link Notification}. + * Emitted when the daemon receives a [class@AstalNotifd.Notification]. * * @param id The ID of the Notification. * @param replaced Indicates if an existing Notification was replaced. @@ -93,7 +93,7 @@ public class AstalNotifd.Notifd : Object { public signal void notified(uint id, bool replaced); /** - * Emitted when a {@link Notification} is resolved. + * Emitted when a [class@AstalNotifd.Notification] is resolved. * * @param id The ID of the Notification. * @param reason The reason how the Notification was resolved. diff --git a/lib/notifd/notification.vala b/lib/notifd/notification.vala index 5db3fe2..527a352 100644 --- a/lib/notifd/notification.vala +++ b/lib/notifd/notification.vala @@ -38,19 +38,24 @@ public class AstalNotifd.Notification : Object { public int expire_timeout { internal set; get; } /** - * List of {@link Action} of the notification. + * List of [struct@AstalNotifd.Action] of the notification. * - * Can be invoked by calling {@link Notification.invoke} with the action's id. + * Can be invoked by calling [method@AstalNotifd.Notification.invoke] with the action's id. */ public List actions { get { return _actions; } } /** Path of an image */ public string image { get { return get_str_hint("image-path"); } } - /** Indicates whether {@link Action} identifier should be interpreted as a named icon. */ + /** + * Indicates whether [struct@AstalNotifd.Action] + * identifier should be interpreted as a named icon. + */ public bool action_icons { get { return get_bool_hint("action-icons"); } } - /** [[https://specifications.freedesktop.org/notification-spec/latest/categories.html|Category of the notification.]] */ + /** + * [[https://specifications.freedesktop.org/notification-spec/latest/categories.html]] + */ public string category { get { return get_str_hint("category"); } } /** Specifies the name of the desktop filename representing the calling program. */ @@ -71,13 +76,19 @@ public class AstalNotifd.Notification : Object { /** Indicates that the notification should be excluded from persistency. */ public bool transient { get { return get_bool_hint("transient"); } } - /** Specifies the X location on the screen that the notification should point to. The "y" hint must also be specified. */ + /** + * Specifies the X location on the screen that the notification should point to. + * The "y" hint must also be specified. + */ public int x { get { return get_int_hint("x"); } } - /** Specifies the Y location on the screen that the notification should point to. The "x" hint must also be specified. */ + /** + * Specifies the Y location on the screen that the notification should point to. + * The "x" hint must also be specified. + */ public int y { get { return get_int_hint("y"); } } - /** {@link Urgency} level of the notification. */ + /** [enum@AstalNotifd.Urgency] level of the notification. */ public Urgency urgency { get { return get_byte_hint("urgency"); } } internal Notification( @@ -141,24 +152,23 @@ public class AstalNotifd.Notification : Object { } /** - * Emitted when this {@link Notification} is resolved. + * Emitted when this this notification is resolved. * * @param reason The reason how the Notification was resolved. */ public signal void resolved(ClosedReason reason); /** - * Emitted when the user dismisses this {@link Notification} + * Emitted when the user dismisses this notification. * * @see dismiss */ public signal void dismissed(); /** - * Emitted when an {@link Action} of this {@link Notification} is invoked. + * Emitted when an [struct@AstalNotifd.Action] of this notification is invoked. * * @param action_id id of the invoked action - * @see invoke */ public signal void invoked(string action_id); @@ -171,7 +181,7 @@ public class AstalNotifd.Notification : Object { public void dismiss() { dismissed(); } /** - * Invoke an {@link Action} of this {@link Notification} + * Invoke an [struct@AstalNotifd.Action] of this notification. * * Note that this method just notifies the client that this action was invoked * by the user. If for example this notification persists through the lifetime -- cgit v1.2.3 From 02fdcbe57155bd62c632a75e08087177b7c66eb9 Mon Sep 17 00:00:00 2001 From: Aylur Date: Tue, 22 Oct 2024 22:27:42 +0000 Subject: docs: fixup notifd and apps --- lib/apps/application.vala | 4 ++-- lib/apps/gir.py | 1 + lib/apps/meson.build | 46 +++++++++++++++++++++++++++++++--------------- lib/gir.py | 26 +++++++++++++++++++------- lib/notifd/meson.build | 4 +++- 5 files changed, 56 insertions(+), 25 deletions(-) create mode 120000 lib/apps/gir.py (limited to 'lib') diff --git a/lib/apps/application.vala b/lib/apps/application.vala index 7e20b06..ea22e7a 100644 --- a/lib/apps/application.vala +++ b/lib/apps/application.vala @@ -5,7 +5,7 @@ public class AstalApps.Application : Object { public DesktopAppInfo app { get; construct set; } /** - * The number of times [func@AstalApps.Application.launch] was called on this Application. + * The number of times [method@AstalApps.Application.launch] was called on this Application. */ public int frequency { get; set; default = 0; } @@ -32,7 +32,7 @@ public class AstalApps.Application : Object { /** * `Exec` field from the desktop file. - * Note that if you want to launch this Application you should use the [func@AstalApps.Application.launch] method. + * Note that if you want to launch this Application you should use the [method@AstalApps.Application.launch] method. */ public string executable { owned get { return app.get_string("Exec"); } } diff --git a/lib/apps/gir.py b/lib/apps/gir.py new file mode 120000 index 0000000..b5b4f1d --- /dev/null +++ b/lib/apps/gir.py @@ -0,0 +1 @@ +../gir.py \ No newline at end of file diff --git a/lib/apps/meson.build b/lib/apps/meson.build index b83b216..eb7a90b 100644 --- a/lib/apps/meson.build +++ b/lib/apps/meson.build @@ -39,35 +39,41 @@ deps = [ dependency('json-glib-1.0'), ] -sources = [ - config, - 'apps.vala', +sources = [config] + files( 'application.vala', + 'apps.vala', 'cli.vala', 'fuzzy.vala', -] +) if get_option('lib') 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', - vala_gir: gir, version: meson.project_version(), install: true, - install_dir: [true, true, true, true], + install_dir: [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: get_option('libdir') / 'pkgconfig', + 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( @@ -80,10 +86,20 @@ if get_option('lib') ], input: lib, output: typelib, - depends: lib, + 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', + ) endif if get_option('cli') diff --git a/lib/gir.py b/lib/gir.py index 8ec786f..ccfbbc1 100644 --- a/lib/gir.py +++ b/lib/gir.py @@ -10,7 +10,7 @@ import sys import subprocess -def fix_gir(gir: str): +def fix_gir(name: str, gir: str): namespaces = { "": "http://www.gtk.org/introspection/core/1.0", "c": "http://www.gtk.org/introspection/c/1.0", @@ -28,16 +28,28 @@ def fix_gir(gir: str): html.unescape(doc.text).replace("", "").replace("", "") ) + 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(gir, encoding="utf-8", xml_declaration=True) -def valadoc(gir: str, args: list[str]): - subprocess.run(["valadoc", "-o", "docs", "--gir", gir, *args]) +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__": - gir = sys.argv[1] - args = sys.argv[2:] + name = sys.argv[1] + gir = sys.argv[2] + args = sys.argv[3:] - valadoc(gir, args) - fix_gir(gir) + valadoc(name, gir, args) + fix_gir(name, gir) diff --git a/lib/notifd/meson.build b/lib/notifd/meson.build index e09a371..3d4de95 100644 --- a/lib/notifd/meson.build +++ b/lib/notifd/meson.build @@ -68,7 +68,9 @@ if get_option('lib') gir_tgt = custom_target( gir, - command: [find_program('python3'), files('gir.py'), gir] + pkgs + sources, + command: [find_program('python3'), files('gir.py'), meson.project_name(), gir] + + pkgs + + sources, input: sources, depends: lib, output: gir, -- cgit v1.2.3 From 306e64998c1bf1fb997c1098ae92d6edfef31cd2 Mon Sep 17 00:00:00 2001 From: Aylur Date: Wed, 23 Oct 2024 20:37:32 +0000 Subject: docs: astal3 and io comments --- lib/astal/gtk3/gir.py | 1 + lib/astal/gtk3/meson.build | 1 + lib/astal/gtk3/src/application.vala | 65 ++++++++++++++++++++++++ lib/astal/gtk3/src/meson.build | 57 +++++++++++++++------ lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi | 5 +- lib/astal/gtk3/src/widget/box.vala | 3 ++ lib/astal/gtk3/src/widget/button.vala | 18 +++++-- lib/astal/gtk3/src/widget/centerbox.vala | 3 ++ lib/astal/gtk3/src/widget/circularprogress.vala | 26 ++++++++++ lib/astal/gtk3/src/widget/eventbox.vala | 9 ++++ lib/astal/gtk3/src/widget/icon.vala | 12 ++++- lib/astal/gtk3/src/widget/label.vala | 6 +++ lib/astal/gtk3/src/widget/levelbar.vala | 3 ++ lib/astal/gtk3/src/widget/overlay.vala | 8 +++ lib/astal/gtk3/src/widget/scrollable.vala | 8 +++ lib/astal/gtk3/src/widget/slider.vala | 27 +++++++++- lib/astal/gtk3/src/widget/stack.vala | 14 +++++ lib/astal/gtk3/src/widget/window.vala | 50 +++++++++++++++++- lib/astal/io/application.vala | 6 +-- lib/astal/io/file.vala | 2 +- lib/astal/io/gir.py | 58 +++++++++++++++++++++ lib/astal/io/meson.build | 48 +++++++++++------ lib/astal/io/process.vala | 8 +-- lib/astal/io/time.vala | 6 +-- lib/gir.py | 11 ++-- 25 files changed, 397 insertions(+), 58 deletions(-) create mode 120000 lib/astal/gtk3/gir.py create mode 100644 lib/astal/io/gir.py (limited to 'lib') diff --git a/lib/astal/gtk3/gir.py b/lib/astal/gtk3/gir.py new file mode 120000 index 0000000..16a3a64 --- /dev/null +++ b/lib/astal/gtk3/gir.py @@ -0,0 +1 @@ +../../gir.py \ No newline at end of file diff --git a/lib/astal/gtk3/meson.build b/lib/astal/gtk3/meson.build index b1a0b43..48d3058 100644 --- a/lib/astal/gtk3/meson.build +++ b/lib/astal/gtk3/meson.build @@ -13,5 +13,6 @@ project( libdir = get_option('prefix') / get_option('libdir') pkgdatadir = get_option('prefix') / get_option('datadir') / 'astal' +girpy = files('gir.py') subdir('src') diff --git a/lib/astal/gtk3/src/application.vala b/lib/astal/gtk3/src/application.vala index 1210d88..82ee797 100644 --- a/lib/astal/gtk3/src/application.vala +++ b/lib/astal/gtk3/src/application.vala @@ -6,15 +6,28 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { private string _instance_name = "astal"; private string socket_path { get; private set; } + /** + * Emitted when a new monitor is added to [class@Gdk.Display]. + */ [DBus (visible=false)] public signal void monitor_added(Gdk.Monitor monitor); + /** + * Emitted when a monitor is disconnected from [class@Gdk.Display]. + */ [DBus (visible=false)] public signal void monitor_removed(Gdk.Monitor monitor); + /** + * Emitted when a window that has been added using + * [method@Gtk.Application.add_window] changes its visibility . + */ [DBus (visible=false)] public signal void window_toggled(Gtk.Window window); + /** + * Get all monitors from [class@Gdk.Display]. + */ [DBus (visible=false)] public List monitors { owned get { @@ -30,6 +43,11 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { } } + /** + * A unique instance name. + * + * This is the identifier used by the AstalIO package and the CLI. + */ [DBus (visible=false)] public string instance_name { owned get { return _instance_name; } @@ -39,6 +57,9 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { } } + /** + * Windows that has been added to this app using [method@Gtk.Application.add_window]. + */ [DBus (visible=false)] public List windows { get { return get_windows(); } @@ -52,24 +73,36 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { get { return Gdk.Screen.get_default(); } } + /** + * Shortcut for [property@Gtk.Settings:gtk_theme_name] + */ [DBus (visible=false)] public string gtk_theme { owned get { return settings.gtk_theme_name; } set { settings.gtk_theme_name = value; } } + /** + * Shortcut for [property@Gtk.Settings:gtk_icon_theme_name] + */ [DBus (visible=false)] public string icon_theme { owned get { return settings.gtk_icon_theme_name; } set { settings.gtk_icon_theme_name = value; } } + /** + * Shortcut for [property@Gtk.Settings:gtk_cursor_theme_name] + */ [DBus (visible=false)] public string cursor_theme { owned get { return settings.gtk_cursor_theme_name; } set { settings.gtk_cursor_theme_name = value; } } + /** + * Remove all [class@Gtk.StyleContext] providers. + */ [DBus (visible=false)] public void reset_css() { foreach(var provider in css_providers) { @@ -78,10 +111,17 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { css_providers = new List(); } + /** + * Shortcut for [func@Gtk.Window.set_interactive_debugging]. + */ public void inspector() throws DBusError, IOError { Gtk.Window.set_interactive_debugging(true); } + /** + * Get a window by its [property@Gtk.Widget:name] that has been added to this app + * using [method@Gtk.Application.add_window]. + */ [DBus (visible=false)] public Gtk.Window? get_window(string name) { foreach(var win in windows) { @@ -93,6 +133,10 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { return null; } + /** + * Toggle the visibility of a window by its [property@Gtk.Widget:name] + * that has been added to this app using [method@Gtk.Application.add_window]. + */ public void toggle_window(string window) throws Error { var win = get_window(window); if (win != null) { @@ -102,6 +146,11 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { } } + /** + * Add a new [class@Gtk.StyleContext] provider. + * + * @param style Css string or a path to a css file. + */ [DBus (visible=false)] public void apply_css(string style, bool reset = false) { var provider = new Gtk.CssProvider(); @@ -124,6 +173,9 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { css_providers.append(provider); } + /** + * Shortcut for [method@Gtk.IconTheme.prepend_search_path]. + */ [DBus (visible=false)] public void add_icons(string? path) { if (path != null) { @@ -131,11 +183,21 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { } } + /** + * Handler for an incoming request. + * + * @param msg Body of the message + * @param conn The connection which expects the response. + */ [DBus (visible=false)] public virtual void request(string msg, SocketConnection conn) { AstalIO.write_sock.begin(conn, @"missing response implementation on $application_id"); } + /** + * Attempt to acquire the astal socket for this app identified by its [property@AstalIO.Application:instance_name]. + * If the socket is in use by another app with the same name an [error@AstalIO.AppError.NAME_OCCUPIED] is thrown. + */ [DBus (visible=false)] public void acquire_socket() throws Error { string path; @@ -159,6 +221,9 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { ); } + /** + * Quit and stop the socket if it was acquired. + */ public new void quit() throws DBusError, IOError { if (service != null) { service.stop(); diff --git a/lib/astal/gtk3/src/meson.build b/lib/astal/gtk3/src/meson.build index c8c7df2..bf8f72a 100644 --- a/lib/astal/gtk3/src/meson.build +++ b/lib/astal/gtk3/src/meson.build @@ -60,8 +60,7 @@ foreach protocol : protocols client_protocol_srcs += [client_header, code] endforeach -sources = [ - config, +vala_sources = [config] + files( 'widget/box.vala', 'widget/button.vala', 'widget/centerbox.vala', @@ -77,31 +76,47 @@ sources = [ 'widget/widget.vala', 'widget/window.vala', 'application.vala', - 'idle-inhibit.h', 'idle-inhibit.c', -] + client_protocol_srcs +) + +sources = vala_sources + client_protocol_srcs + files( + 'idle-inhibit.h', +) lib = library( meson.project_name(), sources, dependencies: deps, - vala_args: ['--pkg', 'AstalInhibitManager'], + vala_args: ['--vapi-comments', '--pkg', 'AstalInhibitManager'], 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], + install_dir: [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: pkgconfig_deps, - install_dir: libdir / 'pkgconfig', +pkgs = [] +foreach dep : pkgconfig_deps + pkgs += ['--pkg=' + dep.name()] +endforeach + +gir_tgt = custom_target( + gir, + command: [ + find_program('python3'), + girpy, + meson.project_name(), + gir + ':src/' + gir, + ] + + pkgs + + vala_sources + + [meson.project_source_root() / 'src' / 'vapi' / 'AstalInhibitManager.vapi'], + + input: sources, + depends: lib, + output: gir, + install: true, + install_dir: get_option('datadir') / 'gir-1.0', ) custom_target( @@ -114,7 +129,17 @@ custom_target( ], input: lib, output: typelib, - depends: lib, + depends: [lib, gir_tgt], install: true, install_dir: 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: pkgconfig_deps, + install_dir: libdir / 'pkgconfig', +) diff --git a/lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi b/lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi index 6232a3c..b2b3b34 100644 --- a/lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi +++ b/lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi @@ -5,9 +5,8 @@ namespace Astal { public static unowned InhibitManager? get_default(); public Inhibitor inhibit (Gtk.Window window); } - + [CCode (cheader_filename = "idle-inhibit.h", free_function = "zwp_idle_inhibitor_v1_destroy")] [Compact] - public class Inhibitor { - } + public class Inhibitor { } } diff --git a/lib/astal/gtk3/src/widget/box.vala b/lib/astal/gtk3/src/widget/box.vala index d23a799..d049161 100644 --- a/lib/astal/gtk3/src/widget/box.vala +++ b/lib/astal/gtk3/src/widget/box.vala @@ -1,4 +1,7 @@ public class Astal.Box : Gtk.Box { + /** + * Corresponds to [property@Gtk.Orientable :orientation]. + */ [CCode (notify = false)] public bool vertical { get { return orientation == Gtk.Orientation.VERTICAL; } diff --git a/lib/astal/gtk3/src/widget/button.vala b/lib/astal/gtk3/src/widget/button.vala index bc10577..2d3095a 100644 --- a/lib/astal/gtk3/src/widget/button.vala +++ b/lib/astal/gtk3/src/widget/button.vala @@ -1,3 +1,9 @@ +/** + * This button has no extra functionality on top if its base [class@Gtk.Button] class. + * + * The purpose of this Button subclass is to have a destructable + * struct as the argument in GJS event handlers. + */ public class Astal.Button : Gtk.Button { public signal void hover (HoverEvent event); public signal void hover_lost (HoverEvent event); @@ -39,9 +45,9 @@ public enum Astal.MouseButton { FORWARD = 5, } -// these structs are here because gjs converts every event -// into a union Gdk.Event, which cannot be destructured -// and are not as convinent to work with as a struct +/** + * Struct for [struct@Gdk.EventButton] + */ public struct Astal.ClickEvent { bool release; uint time; @@ -59,6 +65,9 @@ public struct Astal.ClickEvent { } } +/** + * Struct for [struct@Gdk.EventCrossing] + */ public struct Astal.HoverEvent { bool lost; uint time; @@ -78,6 +87,9 @@ public struct Astal.HoverEvent { } } +/** + * Struct for [struct@Gdk.EventScroll] + */ public struct Astal.ScrollEvent { uint time; double x; diff --git a/lib/astal/gtk3/src/widget/centerbox.vala b/lib/astal/gtk3/src/widget/centerbox.vala index 89bf50b..d74a2c4 100644 --- a/lib/astal/gtk3/src/widget/centerbox.vala +++ b/lib/astal/gtk3/src/widget/centerbox.vala @@ -1,4 +1,7 @@ public class Astal.CenterBox : Gtk.Box { + /** + * Corresponds to [property@Gtk.Orientable :orientation]. + */ [CCode (notify = false)] public bool vertical { get { return orientation == Gtk.Orientation.VERTICAL; } diff --git a/lib/astal/gtk3/src/widget/circularprogress.vala b/lib/astal/gtk3/src/widget/circularprogress.vala index dd7c97b..a3ecdf1 100644 --- a/lib/astal/gtk3/src/widget/circularprogress.vala +++ b/lib/astal/gtk3/src/widget/circularprogress.vala @@ -1,8 +1,34 @@ +/** + * CircularProgress is a subclass of [class@Gtk.Bin] which provides a circular progress bar + * with customizable properties such as starting and ending points, + * progress value, and visual features like rounded ends and inversion of progress direction. + */ public class Astal.CircularProgress : Gtk.Bin { + /** + * The starting point of the progress circle, + * where 0 represents 3 o'clock position or 0° degrees and 1 represents 360°. + */ public double start_at { get; set; } + + /** + * The cutoff point of the background color of the progress circle. + */ public double end_at { get; set; } + + /** + * The value which determines the arc of the drawn foreground color. + * Should be a value between 0 and 1. + */ public double value { get; set; } + + /** + * Inverts the progress direction, making it draw counterclockwise. + */ public bool inverted { get; set; } + + /** + * Renders rounded ends at both the start and the end of the progress bar. + */ public bool rounded { get; set; } construct { diff --git a/lib/astal/gtk3/src/widget/eventbox.vala b/lib/astal/gtk3/src/widget/eventbox.vala index 611da2a..0b588e9 100644 --- a/lib/astal/gtk3/src/widget/eventbox.vala +++ b/lib/astal/gtk3/src/widget/eventbox.vala @@ -1,3 +1,9 @@ +/** + * EventBox is a [class@Gtk.EventBox] subclass which is meant to fix an issue with its + * [signal@Gtk.Widget::enter_notify_event] and [signal@Gtk.Widget::leave_notify_event] when nesting EventBoxes + * + * Its css selector is `eventbox`. + */ public class Astal.EventBox : Gtk.EventBox { public signal void hover (HoverEvent event); public signal void hover_lost (HoverEvent event); @@ -49,6 +55,9 @@ public class Astal.EventBox : Gtk.EventBox { } } +/** + * Struct for [struct@Gdk.EventMotion] + */ public struct Astal.MotionEvent { uint time; double x; diff --git a/lib/astal/gtk3/src/widget/icon.vala b/lib/astal/gtk3/src/widget/icon.vala index f2d59a2..9a20359 100644 --- a/lib/astal/gtk3/src/widget/icon.vala +++ b/lib/astal/gtk3/src/widget/icon.vala @@ -1,10 +1,20 @@ +/** + * [class@Gtk.Image] subclass meant to be used only for icons. + * + * It's size is calculated from `font-size` css property. + * Its css selector is `icon`. + */ public class Astal.Icon : Gtk.Image { private IconType type = IconType.NAMED; private double size { get; set; default = 14; } public new Gdk.Pixbuf pixbuf { get; set; } + public GLib.Icon g_icon { get; set; } + + /** + * Either a named icon or a path to a file. + */ public string icon { get; set; default = ""; } - public GLib.Icon g_icon {get; set;} public static Gtk.IconInfo? lookup_icon(string icon) { var theme = Gtk.IconTheme.get_default(); diff --git a/lib/astal/gtk3/src/widget/label.vala b/lib/astal/gtk3/src/widget/label.vala index 4063b6f..899cba9 100644 --- a/lib/astal/gtk3/src/widget/label.vala +++ b/lib/astal/gtk3/src/widget/label.vala @@ -1,11 +1,17 @@ using Pango; public class Astal.Label : Gtk.Label { + /** + * Shortcut for setting [property@Gtk.Label:ellipsize] to [enum@Pango.EllipsizeMode.END] + */ public bool truncate { set { ellipsize = value ? EllipsizeMode.END : EllipsizeMode.NONE; } get { return ellipsize == EllipsizeMode.END; } } + /** + * Shortcut for setting [property@Gtk.Label:justify] to [enum@Gtk.Justification.FILL] + */ public new bool justify_fill { set { justify = value ? Gtk.Justification.FILL : Gtk.Justification.LEFT; } get { return justify == Gtk.Justification.FILL; } diff --git a/lib/astal/gtk3/src/widget/levelbar.vala b/lib/astal/gtk3/src/widget/levelbar.vala index 9b61957..3e98afb 100644 --- a/lib/astal/gtk3/src/widget/levelbar.vala +++ b/lib/astal/gtk3/src/widget/levelbar.vala @@ -1,4 +1,7 @@ public class Astal.LevelBar : Gtk.LevelBar { + /** + * Corresponds to [property@Gtk.Orientable :orientation]. + */ [CCode (notify = false)] public bool vertical { get { return orientation == Gtk.Orientation.VERTICAL; } diff --git a/lib/astal/gtk3/src/widget/overlay.vala b/lib/astal/gtk3/src/widget/overlay.vala index 603ee66..ed5f03b 100644 --- a/lib/astal/gtk3/src/widget/overlay.vala +++ b/lib/astal/gtk3/src/widget/overlay.vala @@ -1,6 +1,11 @@ public class Astal.Overlay : Gtk.Overlay { public bool pass_through { get; set; } + /** + * First [property@Astal.Overlay:overlays] element. + * + * WARNING: setting this value will remove every overlay but the first. + */ public Gtk.Widget? overlay { get { return overlays.nth_data(0); } set { @@ -14,6 +19,9 @@ public class Astal.Overlay : Gtk.Overlay { } } + /** + * Sets the overlays of this Overlay. [method@Gtk.Overlay.add_overlay]. + */ public List overlays { owned get { return get_children(); } set { diff --git a/lib/astal/gtk3/src/widget/scrollable.vala b/lib/astal/gtk3/src/widget/scrollable.vala index 57afb6e..57a440c 100644 --- a/lib/astal/gtk3/src/widget/scrollable.vala +++ b/lib/astal/gtk3/src/widget/scrollable.vala @@ -1,3 +1,11 @@ +/** + * Subclass of [class@Gtk.ScrolledWindow] which has its policy default to + * [enum@Gtk.PolicyType.AUTOMATIC]. + * + * Its css selector is `scrollable`. + * Its child getter returns the child of the inner + * [class@Gtk.Viewport], instead of the viewport. + */ public class Astal.Scrollable : Gtk.ScrolledWindow { private Gtk.PolicyType _hscroll = Gtk.PolicyType.AUTOMATIC; private Gtk.PolicyType _vscroll = Gtk.PolicyType.AUTOMATIC; diff --git a/lib/astal/gtk3/src/widget/slider.vala b/lib/astal/gtk3/src/widget/slider.vala index 466275b..97cfb69 100644 --- a/lib/astal/gtk3/src/widget/slider.vala +++ b/lib/astal/gtk3/src/widget/slider.vala @@ -1,12 +1,20 @@ +/** + * Subclass of [class@Gtk.Scale] which adds a signal and property for the drag state. + */ public class Astal.Slider : Gtk.Scale { + /** + * Corresponds to [property@Gtk.Orientable :orientation]. + */ [CCode (notify = false)] public bool vertical { get { return orientation == Gtk.Orientation.VERTICAL; } set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } } - // emitted when the user drags the slider - public signal void dragged (); + /** + * Emitted when the user drags the slider or uses keyboard arrows and its value changes. + */ + public signal void dragged(); construct { draw_value = false; @@ -45,23 +53,38 @@ public class Astal.Slider : Gtk.Scale { }); } + /** + * `true` when the user drags the slider or uses keyboard arrows. + */ public bool dragging { get; private set; } + /** + * Value of this slider. Defaults to `0`. + */ public double value { get { return adjustment.value; } set { if (!dragging) adjustment.value = value; } } + /** + * Minimum possible value of this slider. Defaults to `0`. + */ public double min { get { return adjustment.lower; } set { adjustment.lower = value; } } + /** + * Maximum possible value of this slider. Defaults to `1`. + */ public double max { get { return adjustment.upper; } set { adjustment.upper = value; } } + /** + * Size of step increments. Defaults to `0.05`. + */ public double step { get { return adjustment.step_increment; } set { adjustment.step_increment = value; } diff --git a/lib/astal/gtk3/src/widget/stack.vala b/lib/astal/gtk3/src/widget/stack.vala index 02f9959..4e856a6 100644 --- a/lib/astal/gtk3/src/widget/stack.vala +++ b/lib/astal/gtk3/src/widget/stack.vala @@ -1,4 +1,12 @@ +/** + * Subclass of [class@Gtk.Stack] that has a children setter which + * invokes [method@Gt.Stack.add_named] with the child's [property@Gtk.Widget:name] property. + */ public class Astal.Stack : Gtk.Stack { + /** + * Same as [property@Gtk.Stack:visible-child-name]. + */ + [CCode (notify = false)] public string shown { get { return visible_child_name; } set { visible_child_name = value; } @@ -23,4 +31,10 @@ public class Astal.Stack : Gtk.Stack { } } } + + construct { + notify["visible_child_name"].connect(() => { + notify_property("shown"); + }); + } } diff --git a/lib/astal/gtk3/src/widget/window.vala b/lib/astal/gtk3/src/widget/window.vala index 2690365..e513242 100644 --- a/lib/astal/gtk3/src/widget/window.vala +++ b/lib/astal/gtk3/src/widget/window.vala @@ -10,7 +10,13 @@ public enum Astal.WindowAnchor { public enum Astal.Exclusivity { NORMAL, + /** + * Request the compositor to allocate space for this window. + */ EXCLUSIVE, + /** + * Request the compositor to stack layers on top of each other. + */ IGNORE, } @@ -22,11 +28,23 @@ public enum Astal.Layer { } public enum Astal.Keymode { + /** + * Window should not receive keyboard events. + */ NONE = 0, // GtkLayerShell.KeyboardMode.NONE + /** + * Window should have exclusive focus if it is on the top or overlay layer. + */ EXCLUSIVE = 1, // GtkLayerShell.KeyboardMode.EXCLUSIVE + /** + * Focus and Unfocues the window as needed. + */ ON_DEMAND = 2, // GtkLayerShell.KeyboardMode.ON_DEMAND } +/** + * Subclass of [class@Gtk.Window] which integrates GtkLayerShell as class fields. + */ public class Astal.Window : Gtk.Window { private static bool check(string action) { if (!is_supported()) { @@ -44,12 +62,18 @@ public class Astal.Window : Gtk.Window { if (check("initialize layer shell")) return; + // If the window has no size allocatoted when it gets mapped. + // It won't show up later either when it size changes by adding children. height_request = 1; width_request = 1; + init_for_window(this); inhibit_manager = InhibitManager.get_default(); } + /** + * When `true` it will permit inhibiting the idle behavior such as screen blanking, locking, and screensaving. + */ public bool inhibit { set { if (inhibit_manager == null) { @@ -74,11 +98,20 @@ public class Astal.Window : Gtk.Window { } } + /** + * Namespace of this window. This can be used to target the layer in compositor rules. + */ public string namespace { get { return get_namespace(this); } set { set_namespace(this, value); } } + /** + * Edges to anchor the window to. + * + * If two perpendicular edges are anchored, the surface will be anchored to that corner. + * If two opposite edges are anchored, the window will be stretched across the screen in that direction. + */ public int anchor { set { if (check("set anchor")) @@ -107,6 +140,9 @@ public class Astal.Window : Gtk.Window { } } + /** + * Exclusivity of this window. + */ public Exclusivity exclusivity { set { if (check("set exclusivity")) @@ -135,6 +171,9 @@ public class Astal.Window : Gtk.Window { } } + /** + * Which layer to appear this window on. + */ public Layer layer { get { return (Layer)get_layer(this); } set { @@ -145,6 +184,9 @@ public class Astal.Window : Gtk.Window { } } + /** + * Keyboard mode of this window. + */ public Keymode keymode { get { return (Keymode)get_keyboard_mode(this); } set { @@ -155,6 +197,9 @@ public class Astal.Window : Gtk.Window { } } + /** + * Which monitor to appear this window on. + */ public Gdk.Monitor gdkmonitor { get { return get_monitor(this); } set { @@ -218,8 +263,9 @@ public class Astal.Window : Gtk.Window { } /** - * CAUTION: the id might not be the same mapped by the compositor - * to reset and let the compositor map it pass a negative number + * Which monitor to appear this window on. + * + * CAUTION: the id might not be the same mapped by the compositor. */ public int monitor { set { diff --git a/lib/astal/io/application.vala b/lib/astal/io/application.vala index a420128..c7bd311 100644 --- a/lib/astal/io/application.vala +++ b/lib/astal/io/application.vala @@ -21,8 +21,8 @@ public interface Application : Object { } /** - * Starts a [class@GLib.SocketService] and binds `XDG_RUNTIME_DIR/astal/.sock`. - * This socket is then used by the astal cli. Not meant for public usage, but for [func@AstalIO.Application.acquire_socket]. + * Starts a [class@Gio.SocketService] and binds `XDG_RUNTIME_DIR/astal/.sock`. + * This socket is then used by the astal cli. Not meant for public usage, but for [method@AstalIO.Application.acquire_socket]. */ public SocketService acquire_socket(Application app, out string sock) throws Error { var name = app.instance_name; @@ -186,7 +186,7 @@ public async string read_sock(SocketConnection conn) throws IOError { * 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.concat("\x04").data, Priority.DEFAULT); + yield conn.output_stream.write_async(@"$response\x04".data, Priority.DEFAULT); } [DBus (name="io.Astal.Application")] diff --git a/lib/astal/io/file.vala b/lib/astal/io/file.vala index e57f449..57b6dc0 100644 --- a/lib/astal/io/file.vala +++ b/lib/astal/io/file.vala @@ -48,7 +48,7 @@ public async void write_file_async(string path, string content) throws Error { /** * 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 [enum@GLib.FileMonitorEvent] indicating the reason. + * that changed and an [enum@Gio.FileMonitorEvent] indicating the reason. */ public FileMonitor? monitor_file(string path, Closure callback) { try { 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("", "").replace("", "") + ) + + 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 index 426a6d6..023dece 100644 --- a/lib/astal/io/meson.build +++ b/lib/astal/io/meson.build @@ -36,35 +36,41 @@ deps = [ dependency('gio-2.0'), ] -sources = [ - config, +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', - vala_gir: gir, version: meson.project_version(), install: true, - install_dir: [true, true, true, true], + install_dir: [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', +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( @@ -72,14 +78,24 @@ custom_target( command: [ find_program('g-ir-compiler'), '--output', '@OUTPUT@', - '--shared-library', libdir / '@PLAINNAME@', + '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@', meson.current_build_dir() / gir, ], input: lib, output: typelib, - depends: lib, + depends: [lib, gir_tgt], install: true, - install_dir: libdir / 'girepository-1.0', + 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( diff --git a/lib/astal/io/process.vala b/lib/astal/io/process.vala index 8767012..cfd05b9 100644 --- a/lib/astal/io/process.vala +++ b/lib/astal/io/process.vala @@ -1,5 +1,5 @@ /** - * `Process` provides shortcuts for [class@GLib.Subprocess] with sane defaults. + * `Process` provides shortcuts for [class@Gio.Subprocess] with sane defaults. */ public class AstalIO.Process : Object { private void read_stream(DataInputStream stream, bool err) { @@ -92,7 +92,7 @@ public class AstalIO.Process : Object { /** * Start a new subprocess with the given command - * which is parsed using [func@Shell.parse_argv]. + * which is parsed using [func@GLib.shell_parse_argv]. */ public static Process subprocess(string cmd) throws Error { string[] argv; @@ -125,7 +125,7 @@ public class AstalIO.Process : Object { /** * Execute a command synchronously. - * The command is parsed using [func@Shell.parse_argv]. + * The command is parsed using [func@GLib.shell_parse_argv]. * * @return stdout of the subprocess */ @@ -160,7 +160,7 @@ public class AstalIO.Process : Object { /** * Execute a command asynchronously. - * The command is parsed using [func@Shell.parse_argv]. + * The command is parsed using [func@GLib.shell_parse_argv]. * * @return stdout of the subprocess */ diff --git a/lib/astal/io/time.vala b/lib/astal/io/time.vala index 29e7e1f..a799f2b 100644 --- a/lib/astal/io/time.vala +++ b/lib/astal/io/time.vala @@ -38,7 +38,7 @@ public class AstalIO.Time : Object { } /** - * Start an interval timer with a [enum@GLib.Priority]. + * Start an interval timer with default Priority. */ public Time.interval_prio(uint interval, int prio = Priority.DEFAULT, Closure? fn) { connect_closure(fn); @@ -50,7 +50,7 @@ public class AstalIO.Time : Object { } /** - * Start a timeout timer with a [enum@GLib.Priority]. + * Start a timeout timer with default Priority. */ public Time.timeout_prio(uint timeout, int prio = Priority.DEFAULT, Closure? fn) { connect_closure(fn); @@ -62,7 +62,7 @@ public class AstalIO.Time : Object { } /** - * Start an idle timer with a [enum@GLib.Priority]. + * Start an idle timer with default priority. */ public Time.idle_prio(int prio = Priority.DEFAULT_IDLE, Closure? fn) { connect_closure(fn); diff --git a/lib/gir.py b/lib/gir.py index ccfbbc1..9ef680f 100644 --- a/lib/gir.py +++ b/lib/gir.py @@ -10,7 +10,7 @@ import sys import subprocess -def fix_gir(name: str, gir: str): +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", @@ -34,7 +34,7 @@ def fix_gir(name: str, gir: str): print("no c:include tag found", file=sys.stderr) exit(1) - tree.write(gir, encoding="utf-8", xml_declaration=True) + tree.write(out, encoding="utf-8", xml_declaration=True) def valadoc(name: str, gir: str, args: list[str]): @@ -48,8 +48,11 @@ def valadoc(name: str, gir: str, args: list[str]): if __name__ == "__main__": name = sys.argv[1] - gir = sys.argv[2] + 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) + fix_gir(name, gir, out) -- cgit v1.2.3 From e6250c7a4366da033d98a67b426ae5e08aa6f712 Mon Sep 17 00:00:00 2001 From: Aylur Date: Thu, 24 Oct 2024 20:25:33 +0000 Subject: lib(apps): make scoring mechanism public resolves #54 --- lib/apps/application.vala | 10 +++++++-- lib/apps/apps.vala | 55 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 46 insertions(+), 19 deletions(-) (limited to 'lib') diff --git a/lib/apps/application.vala b/lib/apps/application.vala index ea22e7a..4b4fc44 100644 --- a/lib/apps/application.vala +++ b/lib/apps/application.vala @@ -69,7 +69,10 @@ public class AstalApps.Application : Object { } } - internal Score fuzzy_match(string term) { + /** + * Calculate a score for an application using fuzzy matching algorithm. + */ + public Score fuzzy_match(string term) { var score = Score(); if (name != null) score.name = fuzzy_match_string(term, name); @@ -83,7 +86,10 @@ public class AstalApps.Application : Object { return score; } - internal Score exact_match(string term) { + /** + * Calculate a score using exact string algorithm. + */ + public Score exact_match(string term) { var score = Score(); if (name != null) score.name = name.down().contains(term.down()) ? 1 : 0; diff --git a/lib/apps/apps.vala b/lib/apps/apps.vala index bbe07c6..ac48121 100644 --- a/lib/apps/apps.vala +++ b/lib/apps/apps.vala @@ -96,22 +96,38 @@ public class AstalApps.Apps : Object { reload(); } - private double score (string search, Application a, bool exact) { - var am = exact ? a.exact_match(search) : a.fuzzy_match(search); + private double score(string search, Application a, SearchAlgorithm alg) { + var s = Score(); double r = 0; - if (include_name) - r += am.name * name_multiplier; - if (include_entry) - r += am.entry * entry_multiplier; - if (include_executable) - r += am.executable * executable_multiplier; - if (include_description) - r += am.description * description_multiplier; + if (alg == FUZZY) s = a.fuzzy_match(search); + if (alg == EXACT) s = a.exact_match(search); + + if (include_name) r += s.name * name_multiplier; + if (include_entry) r += s.entry * entry_multiplier; + if (include_executable) r += s.executable * executable_multiplier; + if (include_description) r += s.description * description_multiplier; + return r; } - internal List query(string? search = "", bool exact = false) { + /** + * Calculate a score for an application using fuzzy matching algorithm. + * Taking this Apps' include settings into consideration . + */ + public double fuzzy_score(string search, Application a) { + return score(search, a, FUZZY); + } + + /** + * Calculate a score for an application using exact string algorithm. + * Taking this Apps' include settings into consideration . + */ + public double exact_score(string search, Application a) { + return score(search, a, EXACT); + } + + internal List query(string? search = "", SearchAlgorithm alg = FUZZY) { if (search == null) search = ""; @@ -129,7 +145,7 @@ public class AstalApps.Apps : Object { // single character, sort by frequency and exact match if (search.length == 1) { foreach (var app in list) { - if (score(search, app, true) == 0) + if (score(search, app, alg) == 0) arr.remove(app); } @@ -142,14 +158,14 @@ public class AstalApps.Apps : Object { // filter foreach (var app in list) { - if (score(search, app, exact) < min_score) + if (score(search, app, alg) < min_score) arr.remove(app); } // sort by score, frequency arr.sort_with_data((a, b) => { - var s1 = score(search, a, exact); - var s2 = score(search, b, exact); + var s1 = score(search, a, alg); + var s2 = score(search, b, alg); if (s1 == s2) return (int)b.frequency - (int)a.frequency; @@ -164,14 +180,14 @@ public class AstalApps.Apps : Object { * Query the `list` of applications with a fuzzy matching algorithm. */ public List fuzzy_query(string? search = "") { - return query(search, false); + return query(search, FUZZY); } /** * Query the `list` of applications with a simple string matching algorithm. */ public List exact_query(string? search = "") { - return query(search, true); + return query(search, EXACT); } /** @@ -224,3 +240,8 @@ public class AstalApps.Apps : Object { } } } + +private enum AstalApps.SearchAlgorithm { + EXACT, + FUZZY, +} -- cgit v1.2.3 From 61727b01ba06f876664addca83e774cb2bad9ce9 Mon Sep 17 00:00:00 2001 From: matteo4375 <160355435+matteo4375@users.noreply.github.com> Date: Thu, 24 Oct 2024 17:39:38 -0400 Subject: apps: add option to include keywords in queries (#56) --- lib/apps/application.vala | 30 +++++++++++++++++++++++++++++- lib/apps/apps.vala | 13 +++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/apps/application.vala b/lib/apps/application.vala index 4b4fc44..c137183 100644 --- a/lib/apps/application.vala +++ b/lib/apps/application.vala @@ -42,6 +42,11 @@ public class AstalApps.Application : Object { */ public string icon_name { owned get { return app.get_string("Icon"); } } + /** + * `Keywords` field from the desktop file. + */ + public string[] keywords { owned get { return app.get_keywords(); } } + internal Application(string id, int? frequency = 0) { Object(app: new DesktopAppInfo(id)); this.frequency = frequency; @@ -74,6 +79,7 @@ public class AstalApps.Application : Object { */ public Score fuzzy_match(string term) { var score = Score(); + if (name != null) score.name = fuzzy_match_string(term, name); if (entry != null) @@ -82,6 +88,12 @@ public class AstalApps.Application : Object { score.executable = fuzzy_match_string(term, executable); if (description != null) score.description = fuzzy_match_string(term, description); + foreach (var keyword in keywords) { + var s = fuzzy_match_string(term, keyword); + if (s > score.keywords) { + score.keywords = s; + } + } return score; } @@ -91,6 +103,7 @@ public class AstalApps.Application : Object { */ public Score exact_match(string term) { var score = Score(); + if (name != null) score.name = name.down().contains(term.down()) ? 1 : 0; if (entry != null) @@ -99,12 +112,17 @@ public class AstalApps.Application : Object { score.executable = executable.down().contains(term.down()) ? 1 : 0; if (description != null) score.description = description.down().contains(term.down()) ? 1 : 0; + foreach (var keyword in keywords) { + if (score.keywords == 0) { + score.keywords = keyword.down().contains(term.down()) ? 1 : 0; + } + } return score; } internal Json.Node to_json() { - return new Json.Builder() + var builder = new Json.Builder() .begin_object() .set_member_name("name").add_string_value(name) .set_member_name("entry").add_string_value(entry) @@ -112,6 +130,15 @@ public class AstalApps.Application : Object { .set_member_name("description").add_string_value(description) .set_member_name("icon_name").add_string_value(icon_name) .set_member_name("frequency").add_int_value(frequency) + .set_member_name("keywords") + .begin_array(); + + foreach (string keyword in keywords) { + builder.add_string_value(keyword); + } + + return builder + .end_array() .end_object() .get_root(); } @@ -122,4 +149,5 @@ public struct AstalApps.Score { int entry; int executable; int description; + int keywords; } diff --git a/lib/apps/apps.vala b/lib/apps/apps.vala index ac48121..dde7d44 100644 --- a/lib/apps/apps.vala +++ b/lib/apps/apps.vala @@ -43,6 +43,12 @@ public class AstalApps.Apps : Object { */ public double description_multiplier { get; set; default = 0.5; } + /** + * Extra multiplier to apply when matching the keywords of an application. + * Defaults to `0.5` + */ + public double keywords_multiplier { get; set; default = 0.5; } + /** * Consider the name of an application during queries. * Defaults to `true` @@ -67,6 +73,12 @@ public class AstalApps.Apps : Object { */ public bool include_description { get; set; default = false; } + /** + * Consider the keywords of an application during queries. + * Defaults to `false` + */ + public bool include_keywords { get; set; default = false; } + construct { cache_directory = Environment.get_user_cache_dir() + "/astal"; cache_file = cache_directory + "/apps-frequents.json"; @@ -107,6 +119,7 @@ public class AstalApps.Apps : Object { if (include_entry) r += s.entry * entry_multiplier; if (include_executable) r += s.executable * executable_multiplier; if (include_description) r += s.description * description_multiplier; + if (include_keywords) r += s.keywords * keywords_multiplier; return r; } -- cgit v1.2.3 From 510ba0b25a680ab53cf353bcaca2950cdd877ff9 Mon Sep 17 00:00:00 2001 From: kotontrion Date: Fri, 25 Oct 2024 08:44:46 +0200 Subject: docs: add tray docs --- lib/tray/gir.py | 1 + lib/tray/meson.build | 64 ++++++++++++++++++++++++++++++----------- lib/tray/tray.vala | 30 ++++++++++++++++---- lib/tray/trayItem.vala | 77 +++++++++++++++++++++++++++++++++++++++++++------- 4 files changed, 140 insertions(+), 32 deletions(-) create mode 120000 lib/tray/gir.py (limited to 'lib') diff --git a/lib/tray/gir.py b/lib/tray/gir.py new file mode 120000 index 0000000..b5b4f1d --- /dev/null +++ b/lib/tray/gir.py @@ -0,0 +1 @@ +../gir.py \ No newline at end of file diff --git a/lib/tray/meson.build b/lib/tray/meson.build index 421f33d..fbb6672 100644 --- a/lib/tray/meson.build +++ b/lib/tray/meson.build @@ -3,7 +3,7 @@ project( 'vala', 'c', version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(), - meson_version: '>= 0.62.0', + meson_version: '>= 0.63.0', default_options: [ 'warning_level=2', 'werror=false', @@ -18,8 +18,9 @@ assert( version_split = meson.project_version().split('.') api_version = version_split[0] + '.' + version_split[1] -gir = 'AstalTray-' + api_version + '.gir' -typelib = 'AstalTray-' + api_version + '.typelib' +namespace = 'AstalTray' +gir = namespace + '-' + api_version + '.gir' +typelib = namespace + '-' + api_version + '.typelib' config = configure_file( input: 'config.vala.in', @@ -61,7 +62,7 @@ dbusmenu_libs = run_command( check: true, ).stdout().strip() -sources = [config, 'tray.vala', 'watcher.vala', 'trayItem.vala'] +sources = [config] + files('tray.vala', 'watcher.vala', 'trayItem.vala') if get_option('lib') lib = library( @@ -70,23 +71,29 @@ if get_option('lib') dependencies: deps, vala_header: meson.project_name() + '.h', vala_vapi: meson.project_name() + '-' + api_version + '.vapi', - vala_gir: gir, - vala_args: ['--pkg', 'DbusmenuGtk3-0.4', '--pkg', 'Dbusmenu-0.4'], + vala_args: ['--vapi-comments', '--pkg', 'DbusmenuGtk3-0.4', '--pkg', 'Dbusmenu-0.4'], version: meson.project_version(), c_args: dbusmenu_cflags.split(' '), link_args: dbusmenu_libs.split(' '), install: true, - install_dir: [true, true, true, true], + install_dir: 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: get_option('libdir') / 'pkgconfig', + pkgs = ['--pkg', 'DbusmenuGtk3-0.4', '--pkg', 'Dbusmenu-0.4'] + 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( @@ -99,10 +106,35 @@ if get_option('lib') ], input: lib, output: typelib, - depends: lib, + 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', + ) + # + # 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, + # install: true, + # install_dir: get_option('libdir') / 'girepository-1.0', + # ) endif if get_option('cli') diff --git a/lib/tray/tray.vala b/lib/tray/tray.vala index 09b0643..4ea6765 100644 --- a/lib/tray/tray.vala +++ b/lib/tray/tray.vala @@ -2,7 +2,7 @@ namespace AstalTray { [DBus (name="org.kde.StatusNotifierWatcher")] internal interface IWatcher : Object { public abstract string[] RegisteredStatusNotifierItems { owned get; } - public abstract int ProtocolVersion { owned get; } + public abstract int ProtocolVersion { get; } public abstract void RegisterStatusNotifierItem(string service, BusName sender) throws DBusError, IOError; public abstract void RegisterStatusNotifierHost(string service) throws DBusError, IOError; @@ -12,13 +12,19 @@ internal interface IWatcher : Object { public signal void StatusNotifierHostRegistered(); public signal void StatusNotifierHostUnregistered(); } - +/** + * Get the singleton instance of [class@AstalTray.Tray] + */ public Tray get_default() { return Tray.get_default(); } public class Tray : Object { private static Tray? instance; + + /** + * Get the singleton instance of [class@AstalTray.Tray] + */ public static unowned Tray get_default() { if (instance == null) instance = new Tray(); @@ -32,13 +38,22 @@ public class Tray : Object { private HashTable _items = new HashTable(str_hash, str_equal); + /** + * List of currently registered tray items + */ public List items { owned get { return _items.get_values(); }} - public signal void item_added(string service) { + /** + * emitted when a new tray item was added. + */ + public signal void item_added(string item_id) { notify_property("items"); } - public signal void item_removed(string service) { + /** + * emitted when a tray item was removed. + */ + public signal void item_removed(string item_id) { notify_property("items"); } @@ -128,8 +143,11 @@ public class Tray : Object { item_removed(service); } - public TrayItem get_item(string service) { - return _items.get(service); + /** + * gets the TrayItem with the given item-id. + */ + public TrayItem get_item(string item_id) { + return _items.get(item_id); } } } diff --git a/lib/tray/trayItem.vala b/lib/tray/trayItem.vala index d5e8603..16bc05b 100644 --- a/lib/tray/trayItem.vala +++ b/lib/tray/trayItem.vala @@ -57,12 +57,12 @@ public enum Status { [DBus (name="org.kde.StatusNotifierItem")] internal interface IItem : DBusProxy { public abstract string Title { owned get; } - public abstract Category Category { owned get; } - public abstract Status Status { owned get; } + public abstract Category Category { get; } + public abstract Status Status { get; } public abstract Tooltip? ToolTip { owned get; } public abstract string Id { owned get; } public abstract string? IconThemePath { owned get; } - public abstract bool ItemIsMenu { owned get; } + public abstract bool ItemIsMenu { get; } public abstract ObjectPath? Menu { owned get; } public abstract string IconName { owned get; } public abstract Pixmap[] IconPixmap { owned get; } @@ -88,11 +88,23 @@ public class TrayItem : Object { private IItem proxy; private List connection_ids; + + /** The Title of the TrayItem */ public string title { owned get { return proxy.Title; } } + + /** The category this item belongs to */ public Category category { get { return proxy.Category; } } + + /** the current status of this item */ public Status status { get { return proxy.Status; } } + + /** the tooltip of this item */ public Tooltip? tooltip { owned get { return proxy.ToolTip; } } - + + /** + * a markup representation of the tooltip. This is basically equvivalent + * to `tooltip.title \n tooltip.description` + */ public string tooltip_markup { owned get { if (proxy.ToolTip == null) @@ -106,10 +118,30 @@ public class TrayItem : Object { } } + /** the id of the item. This id is specified by the tray app.*/ public string id { owned get { return proxy.Id ;} } - public string icon_theme_path { owned get { return proxy.IconThemePath ;} } + + /** + * If set, this only supports the menu, so showing the menu should be prefered + * over calling [method@AstalTray.TrayItem.activate]. + */ public bool is_menu { get { return proxy.ItemIsMenu ;} } - + + /** + * the icon theme path, where to look for the [property@AstalTray.TrayItem:icon-name]. + * + * It is recommended to use the [property@AstalTray.TrayItem:gicon] property, + * which does the icon lookups for you. + */ + public string icon_theme_path { owned get { return proxy.IconThemePath ;} } + + /** + * the name of the icon. This should be looked up in the [property@AstalTray.TrayItem:icon-theme-path] + * if set or in the currently used icon theme otherwise. + * + * It is recommended to use the [property@AstalTray.TrayItem:gicon] property, + * which does the icon lookups for you. + */ public string icon_name { owned get { return proxy.Status == Status.NEEDS_ATTENTION @@ -117,17 +149,30 @@ public class TrayItem : Object { : proxy.IconName; } } - + + /** + * a pixbuf containing the icon. + * + * It is recommended to use the [property@AstalTray.TrayItem:gicon] property, + * which does the icon lookups for you. + */ public Gdk.Pixbuf icon_pixbuf { owned get { return _get_icon_pixbuf(); } } + /** + * contains the items icon. This property is intended to be used with the gicon property + * of the Icon widget and the recommended way to display the icon. + * This property unifies the [property@AstalTray.TrayItem:icon-name], + * [property@AstalTray.TrayItem:icon-theme-path] and [property@AstalTray.TrayItem:icon-pixbuf] properties. + */ public GLib.Icon gicon { get; private set; } + /** the id of the item used to uniquely identify the TrayItems by this lib.*/ public string item_id { get; private set; } public signal void changed(); public signal void ready(); - public TrayItem(string service, string path) { + internal TrayItem(string service, string path) { connection_ids = new List(); item_id = service + path; setup_proxy.begin(service, path, (_, res) => setup_proxy.end(res)); @@ -229,7 +274,10 @@ public class TrayItem : Object { } ); } - + + /** + * send an activate request to the tray app. + */ public void activate(int x, int y) { try { proxy.Activate(x, y); @@ -239,6 +287,9 @@ public class TrayItem : Object { } } + /** + * send a secondary activate request to the tray app. + */ public void secondary_activate(int x, int y) { try { proxy.SecondaryActivate(x, y); @@ -248,6 +299,10 @@ public class TrayItem : Object { } } + /** + * send a scroll request to the tray app. + * valid values for the orientation are "horizontal" and "vertical". + */ public void scroll(int delta, string orientation) { try { proxy.Scroll(delta, orientation); @@ -257,7 +312,9 @@ public class TrayItem : Object { } } - + /** + * creates a new Gtk Menu for this item. + */ public Gtk.Menu? create_menu() { if (proxy.Menu == null) return null; -- cgit v1.2.3 From d6bdcff12256ef1ff8c906c90ebf3c72b58209af Mon Sep 17 00:00:00 2001 From: kotontrion Date: Fri, 25 Oct 2024 08:50:50 +0200 Subject: tray: fix method visibility --- lib/tray/trayItem.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/tray/trayItem.vala b/lib/tray/trayItem.vala index 16bc05b..db0e6d4 100644 --- a/lib/tray/trayItem.vala +++ b/lib/tray/trayItem.vala @@ -324,7 +324,7 @@ public class TrayItem : Object { proxy.Menu); } - public Gdk.Pixbuf? _get_icon_pixbuf() { + private Gdk.Pixbuf? _get_icon_pixbuf() { Pixmap[] pixmaps = proxy.Status == Status.NEEDS_ATTENTION ? proxy.AttentionIconPixmap : proxy.IconPixmap; -- cgit v1.2.3 From e8715aec5e05e0438192e611afea2fe6f10cb80f Mon Sep 17 00:00:00 2001 From: Aylur Date: Fri, 25 Oct 2024 14:09:04 +0000 Subject: docs: battery doc comments --- lib/apps/application.vala | 4 +- lib/battery/device.vala | 221 +++++++++++++++++++++++++++++++++++++++++----- lib/battery/gir.py | 1 + lib/battery/ifaces.vala | 40 ++++----- lib/battery/meson.build | 46 ++++++---- lib/battery/upower.vala | 32 +++++-- lib/gir.py | 11 ++- 7 files changed, 290 insertions(+), 65 deletions(-) create mode 120000 lib/battery/gir.py (limited to 'lib') diff --git a/lib/apps/application.vala b/lib/apps/application.vala index c137183..0a2f73c 100644 --- a/lib/apps/application.vala +++ b/lib/apps/application.vala @@ -25,8 +25,8 @@ public class AstalApps.Application : Object { public string description { get { return app.get_description(); } } /** - * The StartupWMClass field from the desktop file. - * This represents the WM_CLASS property of the main window of the application. + * `StartupWMClass` field from the desktop file. + * This represents the `WM_CLASS` property of the main window of the application. */ public string wm_class { get { return app.get_startup_wm_class(); } } diff --git a/lib/battery/device.vala b/lib/battery/device.vala index a39d789..db69574 100644 --- a/lib/battery/device.vala +++ b/lib/battery/device.vala @@ -1,70 +1,250 @@ namespace AstalBattery { -public Device get_default() { - return Device.get_default(); + /** Get the DisplayDevice. */ + public Device get_default() { + return Device.get_default(); + } } -public class Device : Object { +/** + * Client for a UPower [[https://upower.freedesktop.org/docs/Device.html|device]]. + */ +public class AstalBattery.Device : Object { private static Device display_device; + + /** Get the DisplayDevice. */ public static Device? get_default() { - if (display_device != null) + if (display_device != null) { return display_device; + } try { - display_device = new Device("/org/freedesktop/UPower/devices/DisplayDevice"); - + display_device = new Device((ObjectPath)"/org/freedesktop/UPower/devices/DisplayDevice"); return display_device; } catch (Error error) { critical(error.message); } + return null; } private IUPowerDevice proxy; - public Device(string path) throws Error { + public Device(ObjectPath path) throws Error { proxy = Bus.get_proxy_sync(BusType.SYSTEM, "org.freedesktop.UPower", path); proxy.g_properties_changed.connect(sync); sync(); } + /** + * If it is [enum@AstalBattery.Type.BATTERY], you will need to verify that the + * property power-supply has the value `true` before considering it as a laptop battery. + * Otherwise it will likely be the battery for a device of an unknown type. + */ public Type device_type { get; private set; } + + /** + * Native path of the power source. This is the sysfs path, + * for example /sys/devices/LNXSYSTM:00/device:00/PNP0C0A:00/power_supply/BAT0. + * It is blank if the device is being driven by a user space driver. + */ public string native_path { owned get; private set; } + + /** Name of the vendor of the battery. */ public string vendor { owned get; private set; } + + /** Name of the model of this battery. */ public string model { owned get; private set; } + + /** Unique serial number of the battery. */ public string serial { owned get; private set; } + + /** + * The point in time (seconds since the Epoch) + * that data was read from the power source. + */ public uint64 update_time { get; private set; } + + /** + * If the power device is used to supply the system. + * This would be set `true` for laptop batteries and UPS devices, + * but set to `false` for wireless mice or PDAs. + */ public bool power_supply { get; private set; } - public bool has_history { get; private set; } - public bool has_statistics { get; private set; } + + /** If the power device has history. */ + // TODO: public bool has_history { get; private set; } + + /** If the power device has statistics. */ + // TODO: public bool has_statistics { get; private set; } + + /** + * Whether power is currently being provided through line power. + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.LINE_POWER]. + */ public bool online { get; private set; } + + /** + * Amount of energy (measured in Wh) currently available in the power source. + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.BATTERY]. + */ public double energy { get; private set; } + + /** + * Amount of energy (measured in Wh) in the power source when it's considered to be empty. + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.BATTERY]. + */ public double energy_empty { get; private set; } + + /** + * Amount of energy (measured in Wh) in the power source when it's considered full. + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.BATTERY]. + */ public double energy_full { get; private set; } + + /** + * Amount of energy (measured in Wh) the power source is designed to hold when it's considered full. + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.BATTERY]. + */ public double energy_full_design { get; private set; } + + /** + * Amount of energy being drained from the source, measured in W. + * If positive, the source is being discharged, if negative it's being charged. + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.BATTERY]. + */ public double energy_rate { get; private set; } + + /** Voltage in the Cell or being recorded by the meter. */ public double voltage { get; private set; } + + /** + * The number of charge cycles as defined by the TCO certification, + * or -1 if that value is unknown or not applicable. + */ public int charge_cycles { get; private set; } + + /** Luminosity being recorded by the meter. */ public double luminosity { get; private set; } + + /** + * Number of seconds until the power source is considered empty. Is set to 0 if unknown. + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.BATTERY]. + */ public int64 time_to_empty { get; private set; } + + /** + * Number of seconds until the power source is considered full. Is set to 0 if unknown. + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.BATTERY]. + */ public int64 time_to_full { get; private set;} + + /** + * The amount of energy left in the power source expressed as a percentage between 0 and 1. + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.BATTERY]. + * The percentage will be an approximation if [property@AstalBattery.Device:battery_level] + * is set to something other than None. + */ public double percentage { get; private set; } + + /** + * The temperature of the device in degrees Celsius. + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.BATTERY]. + */ public double temperature { get; private set; } + + /** + * If the power source is present in the bay. + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.BATTERY]. + */ public bool is_present { get; private set; } + + /** + * The battery power state. + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.BATTERY]. + */ public State state { get; private set; } + + /** + * If the power source is rechargeable. + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.BATTERY]. + */ public bool is_rechargable { get; private set; } + + /** + * The capacity of the power source expressed as a percentage between 0 and 1. + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.BATTERY]. + */ public double capacity { get; private set; } + + /** + * Technology used in the battery: + * + * This property is only valid if [property@AstalBattery.Device:device_type] is [enum@AstalBattery.Type.BATTERY]. + */ public Technology technology { get; private set; } + + /** Warning level of the battery. */ public WarningLevel warning_level { get; private set; } + + /** + * The level of the battery for devices which do not report a percentage + * but rather a coarse battery level. If the value is None. + * then the device does not support coarse battery reporting, + * and the [property@AstalBattery.Device:percentage] should be used instead. + */ public BatteryLevel battery_level { get; private set; } + + /** + * An icon name representing this Device. + * + * NOTE: [property@AstalBattery.Device:battery_icon_name] might be a better fit + * as it is calculated from percentage. + */ public string icon_name { owned get; private set; } + /** + * Indicates if [property@AstalBattery.Device:state] is charging or fully charged. + */ public bool charging { get; private set; } + + /** + * Indicates if [property@AstalBattery.Device:device_type] is not line power or unknown. + */ public bool is_battery { get; private set; } + + /** + * An icon name in the form of "battery-level-$percentage-$state-symbolic". + */ public string battery_icon_name { get; private set; } + + /** + * A string representation of this device's [property@AstalBattery.Device:device_type]. + */ public string device_type_name { get; private set; } + + /** + * An icon name that can be used to represent this device's [property@AstalBattery.Device:device_type]. + */ public string device_type_icon { get; private set; } - public void sync() { + // TODO: get_history + // TODO: get_statistics + + private void sync() { device_type = (Type)proxy.Type; native_path = proxy.native_path; vendor = proxy.vendor; @@ -72,8 +252,8 @@ public class Device : Object { serial = proxy.serial; update_time = proxy.update_time; power_supply = proxy.power_supply; - has_history = proxy.has_history; - has_statistics = proxy.has_statistics; + // TODO: has_history = proxy.has_history; + // TODO: has_statistics = proxy.has_statistics; online = proxy.online; energy = proxy.energy; energy_empty = proxy.energy_empty; @@ -90,7 +270,7 @@ public class Device : Object { is_present = proxy.is_present; state = (State)proxy.state; is_rechargable = proxy.is_rechargable; - capacity = proxy.capacity; + capacity = proxy.capacity / 100; technology = (Technology)proxy.technology; warning_level = (WarningLevel)proxy.warning_level; battery_level = (BatteryLevel)proxy.battery_level; @@ -115,7 +295,7 @@ public class Device : Object { } [CCode (type_signature = "u")] -public enum State { +public enum AstalBattery.State { UNKNOWN, CHARGING, DISCHARGING, @@ -126,7 +306,7 @@ public enum State { } [CCode (type_signature = "u")] -public enum Technology { +public enum AstalBattery.Technology { UNKNOWN, LITHIUM_ION, LITHIUM_POLYMER, @@ -137,7 +317,7 @@ public enum Technology { } [CCode (type_signature = "u")] -public enum WarningLevel { +public enum AstalBattery.WarningLevel { UNKNOWN, NONE, DISCHARGING, @@ -147,7 +327,7 @@ public enum WarningLevel { } [CCode (type_signature = "u")] -public enum BatteryLevel { +public enum AstalBattery.BatteryLevel { UNKNOWN, NONE, LOW, @@ -158,7 +338,7 @@ public enum BatteryLevel { } [CCode (type_signature = "u")] -public enum Type { +public enum AstalBattery.Type { UNKNOWN, LINE_POWER, BATTERY, @@ -190,7 +370,7 @@ public enum Type { BLUETOOTH_GENERIC; // TODO: add more icon names - public string? get_icon_name () { + internal string? get_icon_name () { switch (this) { case UPS: return "uninterruptible-power-supply"; @@ -213,7 +393,7 @@ public enum Type { } } - public unowned string? get_name () { + internal unowned string? get_name () { switch (this) { case LINE_POWER: return "Plugged In"; @@ -276,4 +456,3 @@ public enum Type { } } } -} diff --git a/lib/battery/gir.py b/lib/battery/gir.py new file mode 120000 index 0000000..b5b4f1d --- /dev/null +++ b/lib/battery/gir.py @@ -0,0 +1 @@ +../gir.py \ No newline at end of file diff --git a/lib/battery/ifaces.vala b/lib/battery/ifaces.vala index e6eb849..e2d21fe 100644 --- a/lib/battery/ifaces.vala +++ b/lib/battery/ifaces.vala @@ -1,12 +1,11 @@ -namespace AstalBattery { [DBus (name = "org.freedesktop.UPower")] -interface IUPower : DBusProxy { - public abstract string[] enumerate_devices() throws Error; - public abstract string get_display_device() throws Error; +private interface AstalBattery.IUPower : DBusProxy { + public abstract ObjectPath[] enumerate_devices() throws Error; + public abstract ObjectPath get_display_device() throws Error; public abstract string get_critical_action() throws Error; - public signal void device_added(string object_path); - public signal void device_removed(string object_path); + public signal void device_added(ObjectPath object_path); + public signal void device_removed(ObjectPath object_path); public abstract string daemon_version { owned get; } public abstract bool on_battery { get; } @@ -15,10 +14,10 @@ interface IUPower : DBusProxy { } [DBus (name = "org.freedesktop.UPower.Device")] -public interface IUPowerDevice : DBusProxy { - public abstract HistoryDataPoint[] get_history (string type, uint32 timespan, uint32 resolution) throws GLib.Error; - public abstract StatisticsDataPoint[] get_statistics (string type) throws GLib.Error; - public abstract void refresh () throws GLib.Error; +private interface AstalBattery.IUPowerDevice : DBusProxy { + // public abstract HistoryDataPoint[] get_history (string type, uint32 timespan, uint32 resolution) throws GLib.Error; + // public abstract StatisticsDataPoint[] get_statistics (string type) throws GLib.Error; + // public abstract void refresh () throws GLib.Error; public abstract uint Type { get; } public abstract string native_path { owned get; } @@ -52,14 +51,13 @@ public interface IUPowerDevice : DBusProxy { public abstract string icon_name { owned get; } } -public struct HistoryDataPoint { - uint32 time; - double value; - uint32 state; -} - -public struct StatisticsDataPoint { - double value; - double accuracy; -} -} +// private struct AstalBattery.HistoryDataPoint { +// uint32 time; +// double value; +// uint32 state; +// } +// +// private struct AstalBattery.StatisticsDataPoint { +// double value; +// double accuracy; +// } diff --git a/lib/battery/meson.build b/lib/battery/meson.build index 584f66d..054e9db 100644 --- a/lib/battery/meson.build +++ b/lib/battery/meson.build @@ -41,34 +41,40 @@ pkgconfig_deps = [ deps = pkgconfig_deps + meson.get_compiler('c').find_library('m') -sources = [ - config, - 'ifaces.vala', +sources = [config] + files( 'device.vala', + 'ifaces.vala', 'upower.vala', -] +) if get_option('lib') 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', - vala_gir: gir, version: meson.project_version(), install: true, - install_dir: [true, true, true, true], + install_dir: [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: pkgconfig_deps, - install_dir: get_option('libdir') / 'pkgconfig', + pkgs = [] + foreach dep : pkgconfig_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( @@ -81,10 +87,20 @@ if get_option('lib') ], input: lib, output: typelib, - depends: lib, + 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: pkgconfig_deps, + install_dir: get_option('libdir') / 'pkgconfig', + ) endif if get_option('cli') diff --git a/lib/battery/upower.vala b/lib/battery/upower.vala index 9c18ffd..223633e 100644 --- a/lib/battery/upower.vala +++ b/lib/battery/upower.vala @@ -1,23 +1,40 @@ -namespace AstalBattery { -public class UPower : Object { +/** + * Client for the UPower [[https://upower.freedesktop.org/docs/UPower.html|dbus interface]]. + */ +public class AstalBattery.UPower : Object { private IUPower proxy; private HashTable _devices = new HashTable(str_hash, str_equal); + /** List of UPower devices. */ public List devices { owned get { return _devices.get_values(); } } + /** Emitted when a new device is connected. */ public signal void device_added(Device device); + + /** Emitted a new device is disconnected. */ public signal void device_removed(Device device); + /** A composite device that represents the battery status. */ public Device display_device { owned get { return Device.get_default(); }} public string daemon_version { owned get { return proxy.daemon_version; } } + + /** Indicates whether the system is running on battery power. */ public bool on_battery { get { return proxy.on_battery; } } + + /** Indicates if the laptop lid is closed where the display cannot be seen. */ public bool lid_is_closed { get { return proxy.lid_is_closed; } } + + /** Indicates if the system has a lid device. */ public bool lis_is_present { get { return proxy.lid_is_closed; } } + /** + * When the system's power supply is critical (critically low batteries or UPS), + * the system will take this action. + */ public string critical_action { owned get { try { @@ -41,8 +58,14 @@ public class UPower : Object { _devices.set(path, new Device(path)); proxy.device_added.connect((path) => { - _devices.set(path, new Device(path)); - notify_property("devices"); + try { + var d = new Device(path); + _devices.set(path, d); + device_added(d); + notify_property("devices"); + } catch (Error err) { + critical(err.message); + } }); proxy.device_removed.connect((path) => { @@ -55,4 +78,3 @@ public class UPower : Object { } } } -} diff --git a/lib/gir.py b/lib/gir.py index 9ef680f..a0a81dc 100644 --- a/lib/gir.py +++ b/lib/gir.py @@ -8,6 +8,15 @@ import xml.etree.ElementTree as ET import html import sys import subprocess +import re + + +# valac fails on gi-docgen compliant markdown +# gi-docgen removes valac compliant ulink +# so we use vala notation and turn it into markdown +def ulink_to_markdown(text: str): + pattern = r'(.*?)' + return re.sub(pattern, r"[\2](\1)", text) def fix_gir(name: str, gir: str, out: str): @@ -24,7 +33,7 @@ def fix_gir(name: str, gir: str, out: str): for doc in root.findall(".//doc", namespaces): if doc.text: - doc.text = ( + doc.text = ulink_to_markdown( html.unescape(doc.text).replace("", "").replace("", "") ) -- cgit v1.2.3 From 2cd3b8a2a1c040ea643c5b08e63e2c26addc5ee3 Mon Sep 17 00:00:00 2001 From: Aylur Date: Sun, 27 Oct 2024 01:17:28 +0200 Subject: docs(bluetooth): doc comments --- lib/bluetooth/adapter.vala | 117 +++++++++++++++++++++-------- lib/bluetooth/bluetooth.vala | 50 ++++++++++++- lib/bluetooth/device.vala | 169 ++++++++++++++++++++++++++++++------------ lib/bluetooth/gir.py | 1 + lib/bluetooth/interfaces.vala | 46 ++++++++++++ lib/bluetooth/meson.build | 49 ++++++++---- 6 files changed, 336 insertions(+), 96 deletions(-) create mode 120000 lib/bluetooth/gir.py create mode 100644 lib/bluetooth/interfaces.vala (limited to 'lib') diff --git a/lib/bluetooth/adapter.vala b/lib/bluetooth/adapter.vala index 0c9d00e..99a59fb 100644 --- a/lib/bluetooth/adapter.vala +++ b/lib/bluetooth/adapter.vala @@ -1,27 +1,10 @@ -namespace AstalBluetooth { -[DBus (name = "org.bluez.Adapter1")] -internal interface IAdapter : DBusProxy { - public abstract void remove_device(ObjectPath device) throws Error; - public abstract void start_discovery() throws Error; - public abstract void stop_discovery() throws Error; - - public abstract string[] uuids { owned get; } - public abstract bool discoverable { get; set; } - public abstract bool discovering { get; } - public abstract bool pairable { get; set; } - public abstract bool powered { get; set; } - public abstract string address { owned get; } - public abstract string alias { owned get; set; } - public abstract string modalias { owned get; } - public abstract string name { owned get; } - public abstract uint class { get; } - public abstract uint discoverable_timeout { get; set; } - public abstract uint pairable_timeout { get; set; } -} - -public class Adapter : Object { +/** + * Object representing an [[https://github.com/RadiusNetworks/bluez/blob/master/doc/adapter-api.txt|adapter]]. + */ +public class AstalBluetooth.Adapter : Object { private IAdapter proxy; - public string object_path { owned get; construct set; } + + internal string object_path { owned get; private set; } internal Adapter(IAdapter proxy) { this.proxy = proxy; @@ -37,53 +20,127 @@ public class Adapter : Object { }); } + /** + * List of 128-bit UUIDs that represents the available local services. + */ public string[] uuids { owned get { return proxy.uuids; } } + + /** + * Indicates that a device discovery procedure is active. + */ public bool discovering { get { return proxy.discovering; } } + + /** + * Local Device ID information in modalias format used by the kernel and udev. + */ public string modalias { owned get { return proxy.modalias; } } + + /** + * The Bluetooth system name (pretty hostname). + */ public string name { owned get { return proxy.name; } } + + /** + * The Bluetooth class of device. + */ public uint class { get { return proxy.class; } } + + /** + * The Bluetooth device address. + */ public string address { owned get { return proxy.address; } } + + /** + * Switch an adapter to discoverable or non-discoverable + * to either make it visible or hide it. + */ public bool discoverable { get { return proxy.discoverable; } set { proxy.discoverable = value; } } + + /** + * Switch an adapter to pairable or non-pairable. + */ public bool pairable { get { return proxy.pairable; } set { proxy.pairable = value; } } + + /** + * Switch an adapter on or off. + */ public bool powered { get { return proxy.powered; } set { proxy.powered = value; } } + + /** + * The Bluetooth friendly name. + * + * In case no alias is set, it will return [property@AstalBluetooth.Adapter:name]. + */ public string alias { owned get { return proxy.alias; } set { proxy.alias = value; } } + + /** + * The discoverable timeout in seconds. + * A value of zero means that the timeout is disabled + * and it will stay in discoverable/limited mode forever + * until [method@AstalBluetooth.Adapter.stop_discovery] is invoked. + * The default value for the discoverable timeout should be `180`. + */ public uint discoverable_timeout { get { return proxy.discoverable_timeout; } set { proxy.discoverable_timeout = value; } } + + /** + * The pairable timeout in seconds. + * + * A value of zero means that the timeout is disabled and it will stay in pairable mode forever. + * The default value for pairable timeout should be disabled `0`. + */ public uint pairable_timeout { get { return proxy.pairable_timeout; } set { proxy.pairable_timeout = value; } } - public void remove_device(Device device) { - try { proxy.remove_device((ObjectPath)device.object_path); } catch (Error err) { critical(err.message); } + + /** + * This removes the remote device and the pairing information. + * + * Possible errors: `InvalidArguments`, `Failed`. + */ + public void remove_device(Device device) throws Error { + proxy.remove_device(device.object_path); } - public void start_discovery() { - try { proxy.start_discovery(); } catch (Error err) { critical(err.message); } + + /** + * This method starts the device discovery procedure. + * + * Possible errors: `NotReady`, `Failed`. + */ + public void start_discovery() throws Error { + proxy.start_discovery(); } - public void stop_discovery() { - try { proxy.stop_discovery(); } catch (Error err) { critical(err.message); } + + /** + * This method will cancel any previous [method@AstalBluetooth.Adapter.start_discovery] procedure. + * + * Possible errors: `NotReady`, `Failed`, `NotAuthorized`. + */ + public void stop_discovery() throws Error { + proxy.stop_discovery(); } } -} diff --git a/lib/bluetooth/bluetooth.vala b/lib/bluetooth/bluetooth.vala index ce086ba..6eb6b76 100644 --- a/lib/bluetooth/bluetooth.vala +++ b/lib/bluetooth/bluetooth.vala @@ -1,11 +1,21 @@ namespace AstalBluetooth { -public Bluetooth get_default() { - return Bluetooth.get_default(); + /** + * Gets the default singleton Bluetooth object. + */ + public Bluetooth get_default() { + return Bluetooth.get_default(); + } } -public class Bluetooth : Object { +/** + * Manager object for `org.bluez`. + */ +public class AstalBluetooth.Bluetooth : Object { private static Bluetooth _instance; + /** + * Gets the default singleton Bluetooth object. + */ public static Bluetooth get_default() { if (_instance == null) _instance = new Bluetooth(); @@ -21,30 +31,59 @@ public class Bluetooth : Object { private HashTable _devices = new HashTable(str_hash, str_equal); + /** + * Emitted when a new device is registered on the `org.bluez` bus. + */ public signal void device_added (Device device) { notify_property("devices"); } + /** + * Emitted when a device is unregistered on the `org.bluez` bus. + */ public signal void device_removed (Device device) { notify_property("devices"); } + /** + * Emitted when an adapter is registered on the `org.bluez` bus. + */ public signal void adapter_added (Adapter adapter) { notify_property("adapters"); } + /** + * Emitted when an adapter is unregistered on the `org.bluez` bus. + */ public signal void adapter_removed (Adapter adapter) { notify_property("adapters"); } + /** + * `true` if any of the [property@AstalBluetooth.Bluetooth:adapters] are powered. + */ public bool is_powered { get; private set; default = false; } + + /** + * `true` if any of the [property@AstalBluetooth.Bluetooth:devices] is connected. + */ public bool is_connected { get; private set; default = false; } + + /** + * The first registered adapter which is usually the only adapter. + */ public Adapter? adapter { get { return adapters.nth_data(0); } } + /** + * List of adapters available on the host device. + */ public List adapters { owned get { return _adapters.get_values(); } } + /** + * List of registered devices on the `org.bluez` bus. + */ public List devices { owned get { return _devices.get_values(); } } @@ -85,6 +124,10 @@ public class Bluetooth : Object { } } + /** + * Toggle the [property@AstalBluetooth.Adapter:powered] + * property of the [property@AstalBluetooth.Bluetooth:adapter]. + */ public void toggle() { adapter.powered = !adapter.powered; } @@ -178,4 +221,3 @@ public class Bluetooth : Object { return false; } } -} diff --git a/lib/bluetooth/device.vala b/lib/bluetooth/device.vala index 8fe086f..3f00cd9 100644 --- a/lib/bluetooth/device.vala +++ b/lib/bluetooth/device.vala @@ -1,37 +1,14 @@ -namespace AstalBluetooth { -[DBus (name = "org.bluez.Device1")] -internal interface IDevice : DBusProxy { - public abstract void cancel_pairing() throws Error; - public abstract async void connect() throws Error; - public abstract void connect_profile(string uuid) throws Error; - public abstract async void disconnect() throws Error; - public abstract void disconnect_profile(string uuid) throws Error; - public abstract void pair() throws Error; - - public abstract string[] uuids { owned get; } - public abstract bool blocked { get; set; } - public abstract bool connected { get; } - public abstract bool legacy_pairing { get; } - public abstract bool paired { get; } - public abstract bool trusted { get; set; } - public abstract int16 rssi { get; } - public abstract ObjectPath adapter { owned get; } - public abstract string address { owned get; } - public abstract string alias { owned get; set; } - public abstract string icon { owned get; } - public abstract string modalias { owned get; } - public abstract string name { owned get; } - public abstract uint16 appearance { get; } - public abstract uint32 class { get; } -} - -public class Device : Object { +/** + * Object representing a [[https://github.com/luetzel/bluez/blob/master/doc/device-api.txt|device]]. + */ +public class AstalBluetooth.Device : Object { private IDevice proxy; - public string object_path { owned get; construct set; } + + internal ObjectPath object_path { owned get; private set; } internal Device(IDevice proxy) { this.proxy = proxy; - this.object_path = proxy.g_object_path; + this.object_path = (ObjectPath)proxy.g_object_path; proxy.g_properties_changed.connect((props) => { var map = (HashTable)props; foreach (var key in map.get_keys()) { @@ -43,64 +20,164 @@ public class Device : Object { }); } + /** + * List of 128-bit UUIDs that represents the available remote services. + */ public string[] uuids { owned get { return proxy.uuids; } } + + /** + * Indicates if the remote device is currently connected. + */ public bool connected { get { return proxy.connected; } } + + /** + * `true` if the device only supports the pre-2.1 pairing mechanism. + */ public bool legacy_pairing { get { return proxy.legacy_pairing; } } + + /** + * Indicates if the remote device is paired. + */ public bool paired { get { return proxy.paired; } } + + /** + * Received Signal Strength Indicator of the remote device (inquiry or advertising). + */ public int16 rssi { get { return proxy.rssi; } } + + /** + * The object path of the adapter the device belongs to. + */ public ObjectPath adapter { owned get { return proxy.adapter; } } + + /** + * The Bluetooth device address of the remote device. + */ public string address { owned get { return proxy.address; } } + + /** + * Proposed icon name. + */ public string icon { owned get { return proxy.icon; } } + + /** + * Remote Device ID information in modalias format used by the kernel and udev. + */ public string modalias { owned get { return proxy.modalias; } } + + /** + * The Bluetooth remote name. + * + * It is always better to use [property@AstalBluetooth.Device:alias]. + */ public string name { owned get { return proxy.name; } } + + /** + * External appearance of device, as found on GAP service. + */ public uint16 appearance { get { return proxy.appearance; } } + + /** + * The Bluetooth class of device of the remote device. + */ public uint32 class { get { return proxy.class; } } + + /** + * Indicates if this device is currently trying to be connected. + */ public bool connecting { get; private set; } + /** + * If set to `true` any incoming connections from the device will be immediately rejected. + */ public bool blocked { get { return proxy.blocked; } set { proxy.blocked = value; } } + /** + * Indicates if the remote is seen as trusted. + */ public bool trusted { get { return proxy.trusted; } set { proxy.trusted = value; } } + /** + * The name alias for the remote device. + * + * In case no alias is set, it will return the remote device [property@AstalBluetooth.Device:name]. + */ public string alias { owned get { return proxy.alias; } set { proxy.alias = value; } } - public void cancel_pairing() { - try { proxy.cancel_pairing(); } catch (Error err) { critical(err.message); } - } - - public async void connect_device() { + /** + * This is a generic method to connect any profiles + * the remote device supports that can be connected to. + * + * Possible errors: `NotReady`, `Failed`, `InProgress`, `AlreadyConnected`. + */ + public async void connect_device() throws Error { try { connecting = true; yield proxy.connect(); - } catch (Error err) { - critical(err.message); } finally { connecting = false; } } - public async void disconnect_device() { - try { yield proxy.disconnect(); } catch (Error err) { critical(err.message); } + /** + * This method gracefully disconnects all connected profiles. + * + * Possible errors: `NotConnected`. + */ + public async void disconnect_device() throws Error { + yield proxy.disconnect(); } - public void connect_profile(string uuid) { - try { proxy.connect_profile(uuid); } catch (Error err) { critical(err.message); } + /** + * This method connects a specific profile of this device. + * The UUID provided is the remote service UUID for the profile. + * + * Possible errors: `Failed`, `InProgress`, `InvalidArguments`, `NotAvailable`, `NotReady`. + * + * @param uuid the remote service UUID. + */ + public void connect_profile(string uuid) throws Error { + proxy.connect_profile(uuid); } - public void disconnect_profile(string uuid) { - try { proxy.disconnect_profile(uuid); } catch (Error err) { critical(err.message); } + /** + * This method disconnects a specific profile of this device. + * + * Possible errors: `Failed`, `InProgress`, `InvalidArguments`, `NotSupported`. + * + * @param uuid the remote service UUID. + */ + public void disconnect_profile(string uuid) throws Error { + proxy.disconnect_profile(uuid); } - public void pair() { - try { proxy.pair(); } catch (Error err) { critical(err.message); } + /** + * This method will connect to the remote device and initiate pairing. + * + * Possible errors: `InvalidArguments`, `Failed`, `AlreadyExists`, + * `AuthenticationCanceled`, `AuthenticationFailed`, `AuthenticationRejected`, + * `AuthenticationTimeout`, `ConnectionAttemptFailed`. + */ + public void pair() throws Error { + proxy.pair(); + } + + /** + * This method can be used to cancel a pairing operation + * initiated by [method@AstalBluetooth.Device.pair]. + * + * Possible errors: `DoesNotExist`, `Failed`. + */ + public void cancel_pairing() throws Error { + proxy.cancel_pairing(); } -} } diff --git a/lib/bluetooth/gir.py b/lib/bluetooth/gir.py new file mode 120000 index 0000000..b5b4f1d --- /dev/null +++ b/lib/bluetooth/gir.py @@ -0,0 +1 @@ +../gir.py \ No newline at end of file diff --git a/lib/bluetooth/interfaces.vala b/lib/bluetooth/interfaces.vala new file mode 100644 index 0000000..dcb1c4b --- /dev/null +++ b/lib/bluetooth/interfaces.vala @@ -0,0 +1,46 @@ +[DBus (name = "org.bluez.Adapter1")] +private interface AstalBluetooth.IAdapter : DBusProxy { + public abstract void remove_device(ObjectPath device) throws Error; + public abstract void start_discovery() throws Error; + public abstract void stop_discovery() throws Error; + + public abstract string[] uuids { owned get; } + public abstract bool discoverable { get; set; } + public abstract bool discovering { get; } + public abstract bool pairable { get; set; } + public abstract bool powered { get; set; } + public abstract string address { owned get; } + public abstract string alias { owned get; set; } + public abstract string modalias { owned get; } + public abstract string name { owned get; } + public abstract uint class { get; } + public abstract uint discoverable_timeout { get; set; } + public abstract uint pairable_timeout { get; set; } +} + +[DBus (name = "org.bluez.Device1")] +private interface AstalBluetooth.IDevice : DBusProxy { + public abstract void cancel_pairing() throws Error; + public abstract async void connect() throws Error; + public abstract void connect_profile(string uuid) throws Error; + public abstract async void disconnect() throws Error; + public abstract void disconnect_profile(string uuid) throws Error; + public abstract void pair() throws Error; + + public abstract string[] uuids { owned get; } + public abstract bool blocked { get; set; } + public abstract bool connected { get; } + public abstract bool legacy_pairing { get; } + public abstract bool paired { get; } + public abstract bool trusted { get; set; } + public abstract int16 rssi { get; } + public abstract ObjectPath adapter { owned get; } + public abstract string address { owned get; } + public abstract string alias { owned get; set; } + public abstract string icon { owned get; } + public abstract string modalias { owned get; } + public abstract string name { owned get; } + public abstract uint16 appearance { get; } + public abstract uint32 class { get; } +} + diff --git a/lib/bluetooth/meson.build b/lib/bluetooth/meson.build index 934d380..347b463 100644 --- a/lib/bluetooth/meson.build +++ b/lib/bluetooth/meson.build @@ -33,34 +33,41 @@ deps = [ dependency('gio-2.0'), ] -sources = [ - config, - 'utils.vala', - 'device.vala', +sources = [config] + files( 'adapter.vala', 'bluetooth.vala', -] + 'device.vala', + 'interfaces.vala', + 'utils.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', - vala_gir: gir, version: meson.project_version(), install: true, - install_dir: [true, true, true, true], + install_dir: [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: get_option('libdir') / 'pkgconfig', +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( @@ -73,7 +80,17 @@ custom_target( ], input: lib, output: typelib, - depends: lib, + 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', +) -- cgit v1.2.3 From 197bffbf05d62aeefceb58011b0f031cd8836ca6 Mon Sep 17 00:00:00 2001 From: Aylur Date: Sun, 27 Oct 2024 20:35:37 +0000 Subject: docs(mpris): doc comments --- lib/mpris/gir.py | 1 + lib/mpris/ifaces.vala | 40 +++---- lib/mpris/meson.build | 46 +++++--- lib/mpris/mpris.vala | 45 +++++-- lib/mpris/player.vala | 317 +++++++++++++++++++++++++++++++++++++++++++------- 5 files changed, 357 insertions(+), 92 deletions(-) create mode 120000 lib/mpris/gir.py (limited to 'lib') diff --git a/lib/mpris/gir.py b/lib/mpris/gir.py new file mode 120000 index 0000000..b5b4f1d --- /dev/null +++ b/lib/mpris/gir.py @@ -0,0 +1 @@ +../gir.py \ No newline at end of file diff --git a/lib/mpris/ifaces.vala b/lib/mpris/ifaces.vala index 4a9d715..298a288 100644 --- a/lib/mpris/ifaces.vala +++ b/lib/mpris/ifaces.vala @@ -1,19 +1,18 @@ -namespace AstalMpris { [DBus (name="org.freedesktop.DBus")] -internal interface DBusImpl : DBusProxy { - public abstract string[] list_names () throws GLib.Error; - public signal void name_owner_changed (string name, string old_owner, string new_owner); +private interface AstalMpris.DBusImpl : DBusProxy { + public abstract string[] list_names() throws GLib.Error; + public signal void name_owner_changed(string name, string old_owner, string new_owner); } [DBus (name="org.freedesktop.DBus.Properties")] -internal interface PropsIface : DBusProxy { - public abstract HashTable get_all (string iface); +private interface AstalMpris.PropsIface : DBusProxy { + public abstract HashTable get_all(string iface); } [DBus (name="org.mpris.MediaPlayer2")] -internal interface IMpris : PropsIface { - public abstract void raise () throws GLib.Error; - public abstract void quit () throws GLib.Error; +private interface AstalMpris.IMpris : PropsIface { + public abstract void raise() throws GLib.Error; + public abstract void quit() throws GLib.Error; public abstract bool can_quit { get; } public abstract bool fullscreen { get; set; } @@ -27,18 +26,18 @@ internal interface IMpris : PropsIface { } [DBus (name="org.mpris.MediaPlayer2.Player")] -internal interface IPlayer : IMpris { - public abstract void next () throws GLib.Error; - public abstract void previous () throws GLib.Error; - public abstract void pause () throws GLib.Error; - public abstract void play_pause () throws GLib.Error; - public abstract void stop () throws GLib.Error; - public abstract void play () throws GLib.Error; - public abstract void seek (int64 offset) throws GLib.Error; - public abstract void set_position (ObjectPath track_id, int64 position) throws GLib.Error; - public abstract void open_uri (string uri) throws GLib.Error; +private interface AstalMpris.IPlayer : IMpris { + public abstract void next() throws GLib.Error; + public abstract void previous() throws GLib.Error; + public abstract void pause() throws GLib.Error; + public abstract void play_pause() throws GLib.Error; + public abstract void stop() throws GLib.Error; + public abstract void play() throws GLib.Error; + public abstract void seek(int64 offset) throws GLib.Error; + public abstract void set_position(ObjectPath track_id, int64 position) throws GLib.Error; + public abstract void open_uri(string uri) throws GLib.Error; - public signal void seeked (int64 position); + public signal void seeked(int64 position); public abstract string playback_status { owned get; } public abstract string loop_status { owned get; set; } @@ -57,4 +56,3 @@ internal interface IPlayer : IMpris { public abstract bool can_seek { get; } public abstract bool can_control { get; } } -} diff --git a/lib/mpris/meson.build b/lib/mpris/meson.build index c9a5c53..bf215c9 100644 --- a/lib/mpris/meson.build +++ b/lib/mpris/meson.build @@ -38,34 +38,40 @@ deps = [ dependency('json-glib-1.0'), ] -sources = [ - config, +sources = [config] + files( 'ifaces.vala', - 'player.vala', 'mpris.vala', -] + 'player.vala', +) if get_option('lib') 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', - vala_gir: gir, version: meson.project_version(), install: true, - install_dir: [true, true, true, true], + install_dir: [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: get_option('libdir') / 'pkgconfig', + 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( @@ -78,10 +84,20 @@ if get_option('lib') ], input: lib, output: typelib, - depends: lib, + 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', + ) endif if get_option('cli') diff --git a/lib/mpris/mpris.vala b/lib/mpris/mpris.vala index 0e55a2e..8eaffa5 100644 --- a/lib/mpris/mpris.vala +++ b/lib/mpris/mpris.vala @@ -1,12 +1,24 @@ namespace AstalMpris { -public Mpris get_default() { - return Mpris.get_default(); + /** + * Gets the default singleton Mpris instance. + */ + public Mpris get_default() { + return Mpris.get_default(); + } } -public class Mpris : Object { +/** + * Object that monitors dbus for players to appear and disappear. + */ +public class AstalMpris.Mpris : Object { internal static string PREFIX = "org.mpris.MediaPlayer2."; private static Mpris instance; + private DBusImpl proxy; + + /** + * Gets the default singleton Mpris instance. + */ public static Mpris get_default() { if (instance == null) instance = new Mpris(); @@ -14,15 +26,23 @@ public class Mpris : Object { return instance; } - private DBusImpl proxy; - private HashTable _players = new HashTable (str_hash, str_equal); + /** + * List of currently available players. + */ public List players { owned get { return _players.get_values(); } } - public signal void player_added (Player player); - public signal void player_closed (Player player); + /** + * Emitted when a new mpris Player appears. + */ + public signal void player_added(Player player); + + /** + * Emitted when a Player disappears. + */ + public signal void player_closed(Player player); construct { try { @@ -53,14 +73,15 @@ public class Mpris : Object { var p = new Player(busname); _players.set(busname, p); - p.closed.connect(() => { - player_closed(p); - _players.remove(busname); - notify_property("players"); + p.notify["available"].connect(() => { + if (!p.available) { + player_closed(p); + _players.remove(busname); + notify_property("players"); + } }); player_added(p); notify_property("players"); } } -} diff --git a/lib/mpris/player.vala b/lib/mpris/player.vala index 6764d2b..2776047 100644 --- a/lib/mpris/player.vala +++ b/lib/mpris/player.vala @@ -1,75 +1,185 @@ -namespace AstalMpris { -public class Player : Object { +/** + * Object which tracks players through their mpris dbus interface. + * The most simple way is to use [class@AstalMpris.Mpris] which tracks every player, + * but [class@AstalMpris.Player] can be constructed for a dedicated players too. + */ +public class AstalMpris.Player : Object { private static string COVER_CACHE = Environment.get_user_cache_dir() + "/astal/mpris"; private IPlayer proxy; - - public signal void appeared () { available = true; } - public signal void closed () { available = false; } + private uint pollid; // periodically notify position // identifiers - public string bus_name { owned get; construct set; } - public bool available { get; private set; } + public string bus_name { owned get; private set; } - // periodically notify position - private uint pollid; + /** + * Indicates if [property@AstalMpris.Player:bus_name] is available on dbus. + */ + public bool available { get; private set; } // mpris + + /** + * Brings the player's user interface to the front + * using any appropriate mechanism available. + * + * The media player may be unable to control how its user interface is displayed, + * or it may not have a graphical user interface at all. + * In this case, the [property@AstalMpris.Player:can_raise] is `false` and this method does nothing. + */ public void raise() { - try { proxy.raise(); } catch (Error error) { critical(error.message); } + try { proxy.raise(); } catch (Error err) { critical(err.message); } } - public void quit() { - try { proxy.quit(); } catch (Error error) { critical(error.message); } + /** + * Causes the media player to stop running. + * + * The media player may refuse to allow clients to shut it down. + * In this case, the [property@AstalMpris.Player:can_quit] property is false and this method does nothing. + */ + public void quit() throws Error { + try { proxy.quit(); } catch (Error err) { critical(err.message); } } + /** + * Indicates if [method@AstalMpris.Player.quit] has any effect. + */ public bool can_quit { get; private set; } + + /** + * Indicates if the player is occupying the fullscreen. This is typically used for videos. + * Use [method@AstalMpris.Player.toggle_fullscreen] to toggle fullscreen state. + */ public bool fullscreen { get; private set; } + + /** + * Indicates if [method@AstalMpris.Player.toggle_fullscreen] has any effect. + */ public bool can_set_fullscreen { get; private set; } + + /** + * Indicates if [method@AstalMpris.Player.raise] has any effect. + */ public bool can_raise { get; private set; } - public bool has_track_list { get; private set; } + + // TODO: Tracklist interface + // public bool has_track_list { get; private set; } + + /** + * A human friendly name to identify the player. + */ public string identity { owned get; private set; } + + /** + * The base name of a .desktop file + */ public string entry { owned get; private set; } + + /** + * The URI schemes supported by the media player. + * + * This can be viewed as protocols supported by the player in almost all cases. + * Almost every media player will include support for the "file" scheme. + * Other common schemes are "http" and "rtsp". + */ public string[] supported_uri_schemas { owned get; private set; } + + /** + * The mime-types supported by the player. + */ public string[] supported_mime_types { owned get; private set; } + /** + * Toggle [property@AstalMpris.Player:fullscreen] state. + */ public void toggle_fullscreen() { if (!can_set_fullscreen) - critical("can not set fullscreen on " + bus_name); + critical(@"can not set fullscreen on $bus_name"); proxy.fullscreen = !fullscreen; } - // player + /** + * Skips to the next track in the tracklist. + * + * If there is no next track (and endless playback and track repeat are both off), stop playback. + * If [property@AstalMpris.Player:can_go_next] is `false` this method has no effect. + */ public void next() { try { proxy.next(); } catch (Error error) { critical(error.message); } } + /** + * Skips to the previous track in the tracklist. + * + * If there is no previous track (and endless playback and track repeat are both off), stop playback. + * If [property@AstalMpris.Player:can_go_previous] is `false` this method has no effect. + */ public void previous() { try { proxy.previous(); } catch (Error error) { critical(error.message); } } + /** + * Pauses playback. + * + * If playback is already paused, this has no effect. + * If [property@AstalMpris.Player:can_pause] is `false` this method has no effect. + */ public void pause() { try { proxy.pause(); } catch (Error error) { critical(error.message); } } + /** + * Pauses playback. + * + * If playback is already paused, resumes playback. + * If playback is stopped, starts playback. + */ public void play_pause() { try { proxy.play_pause(); } catch (Error error) { critical(error.message); } } + /** + * Stops playback. + * + * If playback is already stopped, this has no effect. + * If [property@AstalMpris.Player:can_control] is `false` this method has no effect. + */ public void stop() { try { proxy.stop(); } catch (Error error) { critical(error.message); } } + /** + * Starts or resumes playback. + * + * If already playing, this has no effect. + * If paused, playback resumes from the current position. + * If [property@AstalMpris.Player:can_play] is `false` this method has no effect. + */ public void play() { try { proxy.play(); } catch (Error error) { critical(error.message); } } + /** + * uri scheme should be an element of [property@AstalMpris.Player:supported_uri_schemas] + * and the mime-type should match one of the elements of [property@AstalMpris.Player:supported_mime_types]. + * + * @param uri Uri of the track to load. + */ public void open_uri(string uri) { try { proxy.open_uri(uri); } catch (Error error) { critical(error.message); } } + /** + * Change [property@AstalMpris.Player:loop_status] from none to track, + * from track to playlist, from playlist to none. + */ public void loop() { + if (loop_status == Loop.UNSUPPORTED) { + critical(@"loop is unsupported by $bus_name"); + return; + } + switch (loop_status) { case Loop.NONE: loop_status = Loop.TRACK; @@ -85,15 +195,21 @@ public class Player : Object { } } + /** + * Toggle [property@AstalMpris.Player:shuffle_status]. + */ public void shuffle() { + if (shuffle_status == Shuffle.UNSUPPORTED) { + critical(@"shuffle is unsupported by $bus_name"); + return; + } + shuffle_status = shuffle_status == Shuffle.ON ? Shuffle.OFF : Shuffle.ON; } - public signal void seeked (int64 position); - - public double _get_position() { + private double _get_position() { try { var reply = proxy.call_sync( "org.freedesktop.DBus.Properties.Get", @@ -130,60 +246,172 @@ public class Player : Object { private Shuffle _shuffle_status = Shuffle.UNSUPPORTED; private double _volume = -1; + /** + * The current loop/repeat status. + */ public Loop loop_status { get { return _loop_status; } set { proxy.loop_status = value.to_string(); } } + /** + * The current playback rate. + */ public double rate { get { return _rate; } set { proxy.rate = value; } } + /** + * The current shuffle status. + */ public Shuffle shuffle_status { get { return _shuffle_status; } set { proxy.shuffle = value == Shuffle.ON; } } + /** + * The current volume level between 0 and 1. + */ public double volume { get { return _volume; } set { proxy.volume = value; } } + /** + * The current position of the track in seconds. + * To get a progress percentage simply divide this with [property@AstalMpris.Player:length]. + */ public double position { get { return _get_position(); } set { _set_position(value); } } + /** + * The current playback status. + */ public PlaybackStatus playback_status { get; private set; } + + /** + * The minimum value which the [property@AstalMpris.Player:rate] can take. + */ public double minimum_rate { get; private set; } + + /** + * The maximum value which the [property@AstalMpris.Player:rate] can take. + */ public double maximum_rate { get; private set; } + + /** + * Indicates if invoking [method@AstalMpris.Player.next] has effect. + */ public bool can_go_next { get; private set; } + + /** + * Indicates if invoking [method@AstalMpris.Player.previous] has effect. + */ public bool can_go_previous { get; private set; } + + /** + * Indicates if invoking [method@AstalMpris.Player.play] has effect. + */ public bool can_play { get; private set; } + + /** + * Indicates if invoking [method@AstalMpris.Player.pause] has effect. + */ public bool can_pause { get; private set; } + + /** + * Indicates if setting [property@AstalMpris.Player:position] has effect. + */ public bool can_seek { get; private set; } - public bool can_control { get; private set; } - // metadata - [CCode (notify = false)] - public HashTable metadata { owned get; private set; } + /** + * Indicates if the player can be controlled with + * methods such as [method@AstalMpris.Player.play_pause]. + */ + public bool can_control { get; private set; } + /** + * Metadata hashtable of this player. + * In languages that cannot introspect this + * use [method@AstalMpris.Player.get_meta]. + */ + [CCode (notify = false)] // notified manually in sync + public HashTable metadata { owned get; private set; } + + /** + * Currently playing track's id. + */ public string trackid { owned get; private set; } + + /** + * Length of the currently playing track in seconds. + */ public double length { get; private set; } + + /** + * The location of an image representing the track or album. + * You should always prefer to use [property@AstalMpris.Player:cover_art]. + */ public string art_url { owned get; private set; } + /** + * Title of the currently playing album. + */ public string album { owned get; private set; } + + /** + * Artists of the currently playing album. + */ public string album_artist { owned get; private set; } + + /** + * Artists of the currently playing track. + */ public string artist { owned get; private set; } + + /** + * Lyrics of the currently playing track. + */ public string lyrics { owned get; private set; } + + /** + * Title of the currently playing track. + */ public string title { owned get; private set; } + + /** + * Composers of the currently playing track. + */ public string composer { owned get; private set; } + + /** + * Comments of the currently playing track. + */ public string comments { owned get; private set; } - // cached cover art + /** + * Path of the cached [property@AstalMpris.Player:art_url]. + */ public string cover_art { owned get; private set; } + /** + * Lookup a key from [property@AstalMpris.Player:metadata]. + * This method is useful for languages that fail to introspect hashtables. + */ + public Variant? get_meta(string key) { + return metadata.lookup(key); + } + + /** + * Construct a Player that tracks a dbus name. For example "org.mpris.MediaPlayer2.spotify". + * The "org.mpris.MediaPlayer2." prefix can be leftout so simply "spotify" would mean the same. + * [property@AstalMpris.Player:available] indicates whether the player is actually running or not. + * + * @param name dbus name of the player. + */ public Player(string name) { Object(bus_name: name.has_prefix("org.mpris.MediaPlayer2.") ? name : "org.mpris.MediaPlayer2." + name); @@ -195,7 +423,7 @@ public class Player : Object { fullscreen = proxy.fullscreen; can_set_fullscreen = proxy.can_set_fullscreen; can_raise = proxy.can_raise; - has_track_list = proxy.has_track_list; + // has_track_list = proxy.has_track_list; identity = proxy.identity; entry = proxy.desktop_entry; supported_uri_schemas = proxy.supported_uri_schemas; @@ -310,10 +538,6 @@ public class Player : Object { } } - public Variant? get_meta(string key) { - return metadata.lookup(key); - } - private string get_str(string key) { if (metadata.get(key) == null) return ""; @@ -349,7 +573,7 @@ public class Player : Object { } } - public void try_proxy() throws Error { + private void try_proxy() throws Error { if (proxy != null) return; @@ -360,13 +584,14 @@ public class Player : Object { ); if (proxy.g_name_owner != null) - appeared(); + available = false; proxy.notify["g-name-owner"].connect(() => { - if (proxy.g_name_owner != null) - appeared(); - else - closed(); + if (proxy.g_name_owner != null) { + available = true; + } else { + available = false; + } }); proxy.g_properties_changed.connect(sync); @@ -387,12 +612,12 @@ public class Player : Object { } } -public enum PlaybackStatus { +public enum AstalMpris.PlaybackStatus { PLAYING, PAUSED, STOPPED; - public static PlaybackStatus from_string(string? str) { + internal static PlaybackStatus from_string(string? str) { switch (str) { case "Playing": return PLAYING; @@ -404,7 +629,7 @@ public enum PlaybackStatus { } } - public string to_string() { + internal string to_string() { switch (this) { case PLAYING: return "Playing"; @@ -417,13 +642,16 @@ public enum PlaybackStatus { } } -public enum Loop { +public enum AstalMpris.Loop { UNSUPPORTED, + /** The playback will stop when there are no more tracks to play. */ NONE, + /** The current track will start again from the begining once it has finished playing. */ TRACK, + /** The playback loops through a list of tracks. */ PLAYLIST; - public static Loop from_string(string? str) { + internal static Loop from_string(string? str) { switch (str) { case "None": return NONE; @@ -436,7 +664,7 @@ public enum Loop { } } - public string? to_string() { + internal string? to_string() { switch (this) { case NONE: return "None"; @@ -450,16 +678,18 @@ public enum Loop { } } -public enum Shuffle { +public enum AstalMpris.Shuffle { UNSUPPORTED, + /** Playback is progressing through a playlist in some other order. */ ON, + /** Playback is progressing linearly through a playlist. */ OFF; - public static Shuffle from_bool(bool b) { + internal static Shuffle from_bool(bool b) { return b ? Shuffle.ON : Shuffle.OFF; } - public string? to_string() { + internal string? to_string() { switch (this) { case OFF: return "Off"; @@ -470,4 +700,3 @@ public enum Shuffle { } } } -} -- cgit v1.2.3 From 439571abec531d9b50d22f647335112645e43d86 Mon Sep 17 00:00:00 2001 From: Aylur Date: Sun, 27 Oct 2024 22:12:35 +0000 Subject: cli: better err logs --- lib/astal/io/application.vala | 69 ++++++++++++++++--------------------------- lib/astal/io/cli.vala | 53 ++++++++++++++++++++++----------- lib/mpris/player.vala | 6 ---- 3 files changed, 61 insertions(+), 67 deletions(-) (limited to 'lib') diff --git a/lib/astal/io/application.vala b/lib/astal/io/application.vala index c7bd311..09b61b5 100644 --- a/lib/astal/io/application.vala +++ b/lib/astal/io/application.vala @@ -103,75 +103,58 @@ public static List get_instances() { * Quit an an Astal instances. * It is the equivalent of `astal --quit -i instance`. */ -public static void quit_instance(string instance) { - try { - IApplication proxy = Bus.get_proxy_sync( - BusType.SESSION, - "io.Astal." + instance, - "/io/Astal/Application" - ); +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(); - } catch (Error err) { - critical(err.message); - } + 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) { - try { - IApplication proxy = Bus.get_proxy_sync( - BusType.SESSION, - "io.Astal." + instance, - "/io/Astal/Application" - ); +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(); - } catch (Error err) { - critical(err.message); - } + 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) { - try { - IApplication proxy = Bus.get_proxy_sync( - BusType.SESSION, - "io.Astal." + instance, - "/io/Astal/Application" - ); +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); - } catch (Error err) { - critical(err.message); - } + 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) { +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(); - try { - var conn = client.connect(new UnixSocketAddress(socket_path), null); - conn.output_stream.write(msg.concat("\x04").data); + 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 ""; - } + var stream = new DataInputStream(conn.input_stream); + return stream.read_upto("\x04", -1, null, null); } /** diff --git a/lib/astal/io/cli.vala b/lib/astal/io/cli.vala index 8fc0523..f69cf0b 100644 --- a/lib/astal/io/cli.vala +++ b/lib/astal/io/cli.vala @@ -11,13 +11,19 @@ const OptionEntry[] options = { { "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 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(); @@ -25,9 +31,8 @@ int main(string[] argv) { opts.set_help_enabled(false); opts.set_ignore_unknown_options(false); opts.parse(ref argv); - } catch (OptionError err) { - printerr (err.message); - return 1; + } catch (OptionError e) { + return err(e.message); } if (help) { @@ -55,24 +60,30 @@ int main(string[] argv) { if (list) { foreach (var name in AstalIO.get_instances()) - stdout.printf("%s\n", name); + print(@"$name\n"); return 0; } - if (quit) { - AstalIO.quit_instance(instance_name); - return 0; - } + try { + if (quit) { + AstalIO.quit_instance(instance_name); + return 0; + } - if (inspector) { - AstalIO.open_inspector(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; + 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 = ""; @@ -80,8 +91,14 @@ int main(string[] argv) { request = request.concat(" ", argv[i]); } - var reply = AstalIO.send_message(instance_name, request); - print("%s\n", reply); + 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/mpris/player.vala b/lib/mpris/player.vala index 2776047..9b03f0b 100644 --- a/lib/mpris/player.vala +++ b/lib/mpris/player.vala @@ -101,7 +101,6 @@ public class AstalMpris.Player : Object { /** * Skips to the next track in the tracklist. - * * If there is no next track (and endless playback and track repeat are both off), stop playback. * If [property@AstalMpris.Player:can_go_next] is `false` this method has no effect. */ @@ -111,7 +110,6 @@ public class AstalMpris.Player : Object { /** * Skips to the previous track in the tracklist. - * * If there is no previous track (and endless playback and track repeat are both off), stop playback. * If [property@AstalMpris.Player:can_go_previous] is `false` this method has no effect. */ @@ -121,7 +119,6 @@ public class AstalMpris.Player : Object { /** * Pauses playback. - * * If playback is already paused, this has no effect. * If [property@AstalMpris.Player:can_pause] is `false` this method has no effect. */ @@ -131,7 +128,6 @@ public class AstalMpris.Player : Object { /** * Pauses playback. - * * If playback is already paused, resumes playback. * If playback is stopped, starts playback. */ @@ -141,7 +137,6 @@ public class AstalMpris.Player : Object { /** * Stops playback. - * * If playback is already stopped, this has no effect. * If [property@AstalMpris.Player:can_control] is `false` this method has no effect. */ @@ -151,7 +146,6 @@ public class AstalMpris.Player : Object { /** * Starts or resumes playback. - * * If already playing, this has no effect. * If paused, playback resumes from the current position. * If [property@AstalMpris.Player:can_play] is `false` this method has no effect. -- cgit v1.2.3 From 5e227f6aca0e591c608297c6e6fc0dc2ee4e9757 Mon Sep 17 00:00:00 2001 From: Aylur Date: Sun, 27 Oct 2024 23:35:49 +0000 Subject: fix: mpris constructor --- lib/mpris/player.vala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/mpris/player.vala b/lib/mpris/player.vala index 9b03f0b..b72c15e 100644 --- a/lib/mpris/player.vala +++ b/lib/mpris/player.vala @@ -407,8 +407,8 @@ public class AstalMpris.Player : Object { * @param name dbus name of the player. */ public Player(string name) { - Object(bus_name: name.has_prefix("org.mpris.MediaPlayer2.") - ? name : "org.mpris.MediaPlayer2." + name); + bus_name = name.has_prefix("org.mpris.MediaPlayer2.") + ? name : @"org.mpris.MediaPlayer2.$name"; } private void sync() { -- cgit v1.2.3 From b56e169214b93fbb866401b60744a5152e278220 Mon Sep 17 00:00:00 2001 From: Aylur Date: Sun, 27 Oct 2024 23:36:06 +0000 Subject: fix(astal3): windowanchor annotation --- lib/astal/gtk3/src/widget/window.vala | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/astal/gtk3/src/widget/window.vala b/lib/astal/gtk3/src/widget/window.vala index e513242..9287200 100644 --- a/lib/astal/gtk3/src/widget/window.vala +++ b/lib/astal/gtk3/src/widget/window.vala @@ -1,11 +1,12 @@ using GtkLayerShell; +[Flags] public enum Astal.WindowAnchor { - NONE = 0, - TOP = 1, - RIGHT = 2, - LEFT = 4, - BOTTOM = 8, + NONE, + TOP, + RIGHT, + LEFT, + BOTTOM, } public enum Astal.Exclusivity { @@ -112,7 +113,7 @@ public class Astal.Window : Gtk.Window { * If two perpendicular edges are anchored, the surface will be anchored to that corner. * If two opposite edges are anchored, the window will be stretched across the screen in that direction. */ - public int anchor { + public WindowAnchor anchor { set { if (check("set anchor")) return; -- cgit v1.2.3 From 7e484188e7492ac7945c854bcc3f26cec1863c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pereira?= Date: Mon, 28 Oct 2024 02:15:21 +0100 Subject: fix: #57 hyprland fullscreen enum (#58) --- lib/hyprland/client.vala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/hyprland/client.vala b/lib/hyprland/client.vala index 3df644b..3f2d0fb 100644 --- a/lib/hyprland/client.vala +++ b/lib/hyprland/client.vala @@ -73,10 +73,11 @@ public class Client : Object { } } +[Flags] public enum Fullscreen { CURRENT = -1, NONE = 0, - FULLSCREEN = 1, - MAXIMIZED = 2, + MAXIMIZED = 1, + FULLSCREEN = 2, } } -- cgit v1.2.3