summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorkotontrion <[email protected]>2024-10-29 13:50:41 +0100
committerkotontrion <[email protected]>2024-10-29 13:50:41 +0100
commit57f20666e716fde56579b8aa638eed1264f793de (patch)
tree59b2ebbd770c80049cea4df82109d28f617675fe /lib
parent4d9ae88b0bab75779876d465f986791d052414ca (diff)
parent7e484188e7492ac7945c854bcc3f26cec1863c91 (diff)
Merge branch 'main' into feat/cava
Diffstat (limited to 'lib')
-rw-r--r--lib/apps/application.vala135
-rw-r--r--lib/apps/apps.vala128
-rw-r--r--lib/apps/fuzzy.vala73
l---------lib/apps/gir.py1
-rw-r--r--lib/apps/meson.build47
l---------lib/astal/gtk3/gir.py1
-rw-r--r--lib/astal/gtk3/meson.build18
-rw-r--r--lib/astal/gtk3/src/application.vala265
-rw-r--r--lib/astal/gtk3/src/config.vala.in6
-rw-r--r--lib/astal/gtk3/src/idle-inhibit.c114
-rw-r--r--lib/astal/gtk3/src/idle-inhibit.h22
-rw-r--r--lib/astal/gtk3/src/meson.build145
-rw-r--r--lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi12
-rw-r--r--lib/astal/gtk3/src/widget/box.vala53
-rw-r--r--lib/astal/gtk3/src/widget/button.vala111
-rw-r--r--lib/astal/gtk3/src/widget/centerbox.vala55
-rw-r--r--lib/astal/gtk3/src/widget/circularprogress.vala206
-rw-r--r--lib/astal/gtk3/src/widget/eventbox.vala73
-rw-r--r--lib/astal/gtk3/src/widget/icon.vala115
-rw-r--r--lib/astal/gtk3/src/widget/label.vala24
-rw-r--r--lib/astal/gtk3/src/widget/levelbar.vala16
-rw-r--r--lib/astal/gtk3/src/widget/overlay.vala65
-rw-r--r--lib/astal/gtk3/src/widget/scrollable.vala48
-rw-r--r--lib/astal/gtk3/src/widget/slider.vala94
-rw-r--r--lib/astal/gtk3/src/widget/stack.vala40
-rw-r--r--lib/astal/gtk3/src/widget/widget.vala157
-rw-r--r--lib/astal/gtk3/src/widget/window.vala293
-rw-r--r--lib/astal/gtk3/version1
-rw-r--r--lib/astal/io/application.vala186
-rw-r--r--lib/astal/io/cli.vala104
-rw-r--r--lib/astal/io/config.vala.in6
-rw-r--r--lib/astal/io/file.vala98
-rw-r--r--lib/astal/io/gir.py58
-rw-r--r--lib/astal/io/meson.build106
-rw-r--r--lib/astal/io/process.vala172
-rw-r--r--lib/astal/io/time.vala111
-rw-r--r--lib/astal/io/variable.vala198
-rw-r--r--lib/astal/io/version1
-rw-r--r--lib/battery/device.vala221
l---------lib/battery/gir.py1
-rw-r--r--lib/battery/ifaces.vala40
-rw-r--r--lib/battery/meson.build46
-rw-r--r--lib/battery/upower.vala32
-rw-r--r--lib/bluetooth/adapter.vala117
-rw-r--r--lib/bluetooth/bluetooth.vala50
-rw-r--r--lib/bluetooth/device.vala169
l---------lib/bluetooth/gir.py1
-rw-r--r--lib/bluetooth/interfaces.vala46
-rw-r--r--lib/bluetooth/meson.build49
-rw-r--r--lib/gir.py67
-rw-r--r--lib/hyprland/client.vala5
-rw-r--r--lib/hyprland/hyprland.vala42
l---------lib/mpris/gir.py1
-rw-r--r--lib/mpris/ifaces.vala40
-rw-r--r--lib/mpris/meson.build46
-rw-r--r--lib/mpris/mpris.vala45
-rw-r--r--lib/mpris/player.vala315
l---------lib/notifd/gir.py1
-rw-r--r--lib/notifd/meson.build92
-rw-r--r--lib/notifd/notifd.vala8
-rw-r--r--lib/notifd/notification.vala34
l---------lib/tray/gir.py1
-rw-r--r--lib/tray/meson.build64
-rw-r--r--lib/tray/tray.vala30
-rw-r--r--lib/tray/trayItem.vala79
65 files changed, 4518 insertions, 482 deletions
diff --git a/lib/apps/application.vala b/lib/apps/application.vala
index 5748fc6..0a2f73c 100644
--- a/lib/apps/application.vala
+++ b/lib/apps/application.vala
@@ -1,23 +1,68 @@
-namespace AstalApps {
-public class Application : Object {
+public class AstalApps.Application : Object {
+ /**
+ * The underlying DesktopAppInfo.
+ */
public DesktopAppInfo app { get; construct set; }
+
+ /**
+ * The number of times [[email protected]] 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(); } }
+
+ /**
+ * `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 [[email protected]] 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"); } }
+ /**
+ * `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;
}
+ /**
+ * 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,22 +74,36 @@ public class Application : Object {
}
}
+ /**
+ * Calculate a score for an application using fuzzy matching algorithm.
+ */
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);
+ foreach (var keyword in keywords) {
+ var s = fuzzy_match_string(term, keyword);
+ if (s > score.keywords) {
+ score.keywords = s;
+ }
+ }
return score;
}
+ /**
+ * 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;
if (entry != null)
@@ -53,12 +112,17 @@ public class 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)
@@ -66,53 +130,24 @@ public class 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)
- .end_object()
- .get_root();
- }
-}
-
-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;
- }
+ .set_member_name("keywords")
+ .begin_array();
- 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
- );
+ foreach (string keyword in keywords) {
+ builder.add_string_value(keyword);
}
- }
- var distance = d[len1, len2];
- int max_len = len1 > len2 ? len1 : len2;
-
- if (max_len == 0) {
- return 1.0;
+ return builder
+ .end_array()
+ .end_object()
+ .get_root();
}
-
- return 1.0 - ((double)distance / max_len);
}
-public struct Score {
- double name;
- double entry;
- double executable;
- double description;
-}
+public struct AstalApps.Score {
+ int name;
+ int entry;
+ int executable;
+ int description;
+ int keywords;
}
diff --git a/lib/apps/apps.vala b/lib/apps/apps.vala
index 2a0d507..dde7d44 100644
--- a/lib/apps/apps.vala
+++ b/lib/apps/apps.vala
@@ -1,25 +1,84 @@
-namespace AstalApps {
-public class Apps : Object {
+public class AstalApps.Apps : Object {
private string cache_directory;
private string cache_file;
private List<Application> _list;
private HashTable<string, int> 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<weak Application> list { owned get { return _list.copy(); } }
- public double min_score { get; set; default = 0.5; }
+ /**
+ * The minimum score the application has to meet in order to be included in queries.
+ */
+ public double 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; }
+ /**
+ * 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`
+ */
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; }
+ /**
+ * 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";
@@ -49,23 +108,39 @@ public class 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;
+ if (include_keywords) r += s.keywords * keywords_multiplier;
return r;
}
- public List<weak Application> 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<weak Application> query(string? search = "", SearchAlgorithm alg = FUZZY) {
if (search == null)
search = "";
@@ -83,7 +158,7 @@ public class 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);
}
@@ -96,14 +171,14 @@ public class 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;
@@ -114,14 +189,23 @@ public class Apps : Object {
return arr;
}
+ /**
+ * Query the `list` of applications with a fuzzy matching algorithm.
+ */
public List<weak Application> 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<weak Application> exact_query(string? search = "") {
- return query(search, true);
+ return query(search, EXACT);
}
+ /**
+ * Reload the `list` of Applications.
+ */
public void reload() {
var arr = AppInfo.get_all();
@@ -169,4 +253,8 @@ public class Apps : Object {
}
}
}
+
+private enum AstalApps.SearchAlgorithm {
+ EXACT,
+ FUZZY,
}
diff --git a/lib/apps/fuzzy.vala b/lib/apps/fuzzy.vala
new file mode 100644
index 0000000..7745320
--- /dev/null
+++ b/lib/apps/fuzzy.vala
@@ -0,0 +1,73 @@
+namespace AstalApps {
+private 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;
+
+ bool found = fuzzy_match_recurse(pattern, str, score, true, out score);
+ score += unmatched_letter_penalty * (str.length - pattern.length);
+
+ if(!found) score = -10;
+
+ 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;
+ 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;
+ bool found = fuzzy_match_recurse(
+ pattern.substring(1),
+ str.substring(offset + 1),
+ 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 false;
+ result += best_score;
+ return true;
+}
+
+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 += int.max(leading_letter_penalty * jump, max_leading_letter_penalty);
+ }
+
+ return score;
+}
+}
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 fb87e22..eb7a90b 100644
--- a/lib/apps/meson.build
+++ b/lib/apps/meson.build
@@ -39,34 +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(
@@ -79,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/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
new file mode 100644
index 0000000..48d3058
--- /dev/null
+++ b/lib/astal/gtk3/meson.build
@@ -0,0 +1,18 @@
+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'
+girpy = files('gir.py')
+
+subdir('src')
diff --git a/lib/astal/gtk3/src/application.vala b/lib/astal/gtk3/src/application.vala
new file mode 100644
index 0000000..82ee797
--- /dev/null
+++ b/lib/astal/gtk3/src/application.vala
@@ -0,0 +1,265 @@
+[DBus (name="io.Astal.Application")]
+public class Astal.Application : Gtk.Application, AstalIO.Application {
+ private List<Gtk.CssProvider> css_providers = new List<Gtk.CssProvider>();
+ private SocketService service;
+ private DBusConnection conn;
+ private string _instance_name = "astal";
+ private string socket_path { get; private set; }
+
+ /**
+ * Emitted when a new monitor is added to [[email protected]].
+ */
+ [DBus (visible=false)]
+ public signal void monitor_added(Gdk.Monitor monitor);
+
+ /**
+ * Emitted when a monitor is disconnected from [[email protected]].
+ */
+ [DBus (visible=false)]
+ public signal void monitor_removed(Gdk.Monitor monitor);
+
+ /**
+ * Emitted when a window that has been added using
+ * [[email protected]_window] changes its visibility .
+ */
+ [DBus (visible=false)]
+ public signal void window_toggled(Gtk.Window window);
+
+ /**
+ * Get all monitors from [[email protected]].
+ */
+ [DBus (visible=false)]
+ public List<weak Gdk.Monitor> monitors {
+ owned get {
+ var display = Gdk.Display.get_default();
+ var list = new List<weak Gdk.Monitor>();
+ for (var i = 0; i <= display.get_n_monitors(); ++i) {
+ var mon = display.get_monitor(i);
+ if (mon != null) {
+ list.append(mon);
+ }
+ }
+ return list;
+ }
+ }
+
+ /**
+ * 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; }
+ construct set {
+ _instance_name = value != null ? value : "astal";
+ application_id = @"io.Astal.$_instance_name";
+ }
+ }
+
+ /**
+ * Windows that has been added to this app using [[email protected]_window].
+ */
+ [DBus (visible=false)]
+ public List<Gtk.Window> windows {
+ get { return get_windows(); }
+ }
+
+ private Gtk.Settings settings {
+ get { return Gtk.Settings.get_default(); }
+ }
+
+ private Gdk.Screen screen {
+ get { return Gdk.Screen.get_default(); }
+ }
+
+ /**
+ * Shortcut for [[email protected]: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 [[email protected]: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 [[email protected]: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 [[email protected]] providers.
+ */
+ [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<Gtk.CssProvider>();
+ }
+
+ /**
+ * Shortcut for [[email protected]_interactive_debugging].
+ */
+ public void inspector() throws DBusError, IOError {
+ Gtk.Window.set_interactive_debugging(true);
+ }
+
+ /**
+ * Get a window by its [[email protected]:name] that has been added to this app
+ * using [[email protected]_window].
+ */
+ [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;
+ }
+
+ /**
+ * Toggle the visibility of a window by its [[email protected]:name]
+ * that has been added to this app using [[email protected]_window].
+ */
+ 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");
+ }
+ }
+
+ /**
+ * Add a new [[email protected]] 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();
+
+ 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);
+ }
+
+ /**
+ * Shortcut for [[email protected]_search_path].
+ */
+ [DBus (visible=false)]
+ public void add_icons(string? path) {
+ if (path != null) {
+ Gtk.IconTheme.get_default().prepend_search_path(path);
+ }
+ }
+
+ /**
+ * 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 [[email protected]:instance_name].
+ * If the socket is in use by another app with the same name an [[email protected]_OCCUPIED] is thrown.
+ */
+ [DBus (visible=false)]
+ public void acquire_socket() throws Error {
+ string path;
+ service = AstalIO.acquire_socket(this, out path);
+ socket_path = path;
+
+ Bus.own_name(
+ BusType.SESSION,
+ application_id,
+ BusNameOwnerFlags.NONE,
+ (conn) => {
+ try {
+ this.conn = conn;
+ conn.register_object("/io/Astal/Application", this);
+ } catch (Error err) {
+ critical(err.message);
+ }
+ },
+ () => {},
+ () => {}
+ );
+ }
+
+ /**
+ * Quit and stop the socket if it was acquired.
+ */
+ public new void quit() throws DBusError, IOError {
+ if (service != null) {
+ service.stop();
+ service.close();
+ }
+
+ base.quit();
+ }
+
+ construct {
+ 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 <gdk/gdk.h>
+#include <gdk/gdkwayland.h>
+#include <gio/gio.h>
+#include <glib-object.h>
+#include <glib.h>
+#include <gtk/gtk.h>
+#include <wayland-client-protocol.h>
+#include <wayland-client.h>
+
+#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, &registry_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 <glib-object.h>
+#include <gtk/gtk.h>
+
+#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..bf8f72a
--- /dev/null
+++ b/lib/astal/gtk3/src/meson.build
@@ -0,0 +1,145 @@
+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: ['@[email protected]'],
+ arguments: ['-c', 'client-header', '@INPUT@', '@BUILD_DIR@/@[email protected]'],
+)
+
+gen_private_code = generator(
+ wayland_scanner,
+ output: ['@[email protected]'],
+ arguments: ['-c', 'private-code', '@INPUT@', '@BUILD_DIR@/@[email protected]'],
+)
+
+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
+
+vala_sources = [config] + files(
+ '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.c',
+)
+
+sources = vala_sources + client_protocol_srcs + files(
+ 'idle-inhibit.h',
+)
+
+lib = library(
+ meson.project_name(),
+ sources,
+ dependencies: deps,
+ vala_args: ['--vapi-comments', '--pkg', 'AstalInhibitManager'],
+ vala_header: meson.project_name() + '.h',
+ vala_vapi: meson.project_name() + '-' + api_version + '.vapi',
+ version: meson.project_version(),
+ install: true,
+ install_dir: [true, true, true],
+)
+
+pkgs = []
+foreach dep : 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(
+ typelib,
+ command: [
+ find_program('g-ir-compiler'),
+ '--output', '@OUTPUT@',
+ '--shared-library', libdir / '@PLAINNAME@',
+ meson.current_build_dir() / gir,
+ ],
+ input: lib,
+ output: typelib,
+ 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
new file mode 100644
index 0000000..b2b3b34
--- /dev/null
+++ b/lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi
@@ -0,0 +1,12 @@
+[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..d049161
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/box.vala
@@ -0,0 +1,53 @@
+public class Astal.Box : Gtk.Box {
+ /**
+ * Corresponds to [[email protected] :orientation].
+ */
+ [CCode (notify = false)]
+ public bool vertical {
+ get { return orientation == Gtk.Orientation.VERTICAL; }
+ set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; }
+ }
+
+ public List<weak Gtk.Widget> 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<weak Gtk.Widget>();
+ 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<weak Gtk.Widget> arr) {
+ foreach(var child in get_children()) {
+ remove(child);
+ }
+
+ foreach(var child in arr)
+ add(child);
+ }
+
+ public Box(bool vertical, List<weak Gtk.Widget> 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..2d3095a
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/button.vala
@@ -0,0 +1,111 @@
+/**
+ * This button has no extra functionality on top if its base [[email protected]] 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);
+ 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,
+}
+
+/**
+ * Struct for [[email protected]]
+ */
+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;
+ }
+}
+
+/**
+ * Struct for [[email protected]]
+ */
+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;
+ }
+}
+
+/**
+ * Struct for [[email protected]]
+ */
+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..d74a2c4
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/centerbox.vala
@@ -0,0 +1,55 @@
+public class Astal.CenterBox : Gtk.Box {
+ /**
+ * Corresponds to [[email protected] :orientation].
+ */
+ [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..a3ecdf1
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/circularprogress.vala
@@ -0,0 +1,206 @@
+/**
+ * CircularProgress is a subclass of [[email protected]] 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 {
+ 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..0b588e9
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/eventbox.vala
@@ -0,0 +1,73 @@
+/**
+ * EventBox is a [[email protected]] subclass which is meant to fix an issue with its
+ * [[email protected]::enter_notify_event] and [[email protected]::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);
+ 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));
+ });
+ }
+}
+
+/**
+ * Struct for [[email protected]]
+ */
+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..9a20359
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/icon.vala
@@ -0,0 +1,115 @@
+/**
+ * [[email protected]] 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 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..899cba9
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/label.vala
@@ -0,0 +1,24 @@
+using Pango;
+
+public class Astal.Label : Gtk.Label {
+ /**
+ * Shortcut for setting [[email protected]:ellipsize] to [[email protected]]
+ */
+ public bool truncate {
+ set { ellipsize = value ? EllipsizeMode.END : EllipsizeMode.NONE; }
+ get { return ellipsize == EllipsizeMode.END; }
+ }
+
+ /**
+ * Shortcut for setting [[email protected]:justify] to [[email protected]]
+ */
+ 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..3e98afb
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/levelbar.vala
@@ -0,0 +1,16 @@
+public class Astal.LevelBar : Gtk.LevelBar {
+ /**
+ * Corresponds to [[email protected] :orientation].
+ */
+ [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..ed5f03b
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/overlay.vala
@@ -0,0 +1,65 @@
+public class Astal.Overlay : Gtk.Overlay {
+ public bool pass_through { get; set; }
+
+ /**
+ * First [[email protected]:overlays] element.
+ *
+ * WARNING: setting this value will remove every overlay but the first.
+ */
+ 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);
+ }
+ }
+
+ /**
+ * Sets the overlays of this Overlay. [[email protected]_overlay].
+ */
+ public List<weak Gtk.Widget> 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..57a440c
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/scrollable.vala
@@ -0,0 +1,48 @@
+/**
+ * Subclass of [[email protected]] which has its policy default to
+ *
+ * Its css selector is `scrollable`.
+ * Its child getter returns the child of the inner
+ * [[email protected]], instead of the viewport.
+ */
+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..97cfb69
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/slider.vala
@@ -0,0 +1,94 @@
+/**
+ * Subclass of [[email protected]] which adds a signal and property for the drag state.
+ */
+public class Astal.Slider : Gtk.Scale {
+ /**
+ * Corresponds to [[email protected] :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 or uses keyboard arrows and its value changes.
+ */
+ 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();
+ });
+ }
+
+ /**
+ * `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; }
+ }
+
+ // 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..4e856a6
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/stack.vala
@@ -0,0 +1,40 @@
+/**
+ * Subclass of [[email protected]] that has a children setter which
+ * invokes [[email protected]_named] with the child's [[email protected]:name] property.
+ */
+public class Astal.Stack : Gtk.Stack {
+ /**
+ * Same as [[email protected]:visible-child-name].
+ */
+ [CCode (notify = false)]
+ public string shown {
+ get { return visible_child_name; }
+ set { visible_child_name = value; }
+ }
+
+ public List<weak Gtk.Widget> children {
+ set { _set_children(value); }
+ owned get { return get_children(); }
+ }
+
+ private void _set_children(List<weak Gtk.Widget> 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());
+ }
+ }
+ }
+
+ construct {
+ notify["visible_child_name"].connect(() => {
+ notify_property("shown");
+ });
+ }
+}
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<Gtk.Widget, Gtk.CssProvider> _providers;
+ public static HashTable<Gtk.Widget, Gtk.CssProvider> providers {
+ get {
+ if (_providers == null) {
+ _providers = new HashTable<Gtk.Widget, Gtk.CssProvider>(
+ (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<weak string> 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<Gtk.Widget, string> _cursors;
+ public static HashTable<Gtk.Widget, string> cursors {
+ get {
+ if (_cursors == null) {
+ _cursors = new HashTable<Gtk.Widget, string>(
+ (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<Gtk.Widget, bool> _click_through;
+ public static HashTable<Gtk.Widget, bool> click_through {
+ get {
+ if (_click_through == null) {
+ _click_through = new HashTable<Gtk.Widget, bool>(
+ (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..9287200
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/window.vala
@@ -0,0 +1,293 @@
+using GtkLayerShell;
+
+[Flags]
+public enum Astal.WindowAnchor {
+ NONE,
+ TOP,
+ RIGHT,
+ LEFT,
+ BOTTOM,
+}
+
+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,
+}
+
+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 {
+ /**
+ * 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 [[email protected]] which integrates GtkLayerShell as class fields.
+ */
+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;
+
+ // 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) {
+ 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);
+ }
+ }
+
+ /**
+ * 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 WindowAnchor 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;
+ }
+ }
+
+ /**
+ * Exclusivity of this window.
+ */
+ 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;
+ }
+ }
+
+ /**
+ * Which layer to appear this window on.
+ */
+ public Layer layer {
+ get { return (Layer)get_layer(this); }
+ set {
+ if (check("set layer"))
+ return;
+
+ set_layer(this, (GtkLayerShell.Layer)value);
+ }
+ }
+
+ /**
+ * Keyboard mode of this window.
+ */
+ public Keymode keymode {
+ get { return (Keymode)get_keyboard_mode(this); }
+ set {
+ if (check("set keymode"))
+ return;
+
+ set_keyboard_mode(this, (GtkLayerShell.KeyboardMode)value);
+ }
+ }
+
+ /**
+ * Which monitor to appear this window on.
+ */
+ 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;
+ }
+ }
+
+ /**
+ * Which monitor to appear this window on.
+ *
+ * CAUTION: the id might not be the same mapped by the compositor.
+ */
+ 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
diff --git a/lib/astal/io/application.vala b/lib/astal/io/application.vala
new file mode 100644
index 0000000..09b61b5
--- /dev/null
+++ b/lib/astal/io/application.vala
@@ -0,0 +1,186 @@
+namespace AstalIO {
+public errordomain AppError {
+ NAME_OCCUPIED,
+ TAKEOVER_FAILED,
+}
+
+/**
+ * This interface is used as a placeholder for the Astal Application class.
+ * It is not meant to be used by consumers.
+ */
+public interface Application : Object {
+ public abstract void quit() throws Error;
+ public abstract void inspector() throws Error;
+ public abstract void toggle_window(string window) throws Error;
+
+ public abstract string instance_name { owned get; construct set; }
+ public abstract void acquire_socket() throws Error;
+ public virtual void request(string msg, SocketConnection conn) throws Error {
+ write_sock.begin(conn, @"missing response implementation on $instance_name");
+ }
+}
+
+/**
+ * Starts a [[email protected]] and binds `XDG_RUNTIME_DIR/astal/<instance_name>.sock`.
+ * This socket is then used by the astal cli. Not meant for public usage, but for [[email protected]_socket].
+ */
+public SocketService acquire_socket(Application app, out string sock) throws Error {
+ var name = app.instance_name;
+ foreach (var instance in get_instances()) {
+ if (instance == name) {
+ throw new AppError.NAME_OCCUPIED(@"$name is occupied");
+ }
+ }
+
+ var rundir = Environment.get_user_runtime_dir();
+ var dir = @"$rundir/astal";
+ var path = @"$dir/$name.sock";
+ sock = path;
+
+ if (!FileUtils.test(dir, FileTest.IS_DIR)) {
+ File.new_for_path(path).make_directory_with_parents(null);
+ }
+
+ if (FileUtils.test(path, FileTest.EXISTS)) {
+ try {
+ File.new_for_path(path).delete(null);
+ } catch (Error err) {
+ throw new AppError.TAKEOVER_FAILED("could not delete previous socket");
+ }
+ }
+
+ var service = new SocketService();
+ service.add_address(
+ new UnixSocketAddress(path),
+ SocketType.STREAM,
+ SocketProtocol.DEFAULT,
+ null,
+ null
+ );
+
+ service.incoming.connect((conn) => {
+ read_sock.begin(conn, (_, res) => {
+ try {
+ string message = read_sock.end(res);
+ app.request(message != null ? message.strip() : "", conn);
+ } catch (Error err) {
+ critical(err.message);
+ }
+ });
+ return false;
+ });
+
+ return service;
+}
+
+/**
+ * Get a list of running Astal.Application instances.
+ * It is the equivalent of `astal --list`.
+ */
+public static List<string> get_instances() {
+ var list = new List<string>();
+ var prefix = "io.Astal.";
+
+ try {
+ DBusImpl dbus = Bus.get_proxy_sync(
+ BusType.SESSION,
+ "org.freedesktop.DBus",
+ "/org/freedesktop/DBus"
+ );
+
+ foreach (var busname in dbus.list_names()) {
+ if (busname.has_prefix(prefix))
+ list.append(busname.replace(prefix, ""));
+ }
+ } catch (Error err) {
+ critical(err.message);
+ }
+
+ return list;
+}
+
+/**
+ * Quit an an Astal instances.
+ * It is the equivalent of `astal --quit -i instance`.
+ */
+public static void quit_instance(string instance) throws Error {
+ IApplication proxy = Bus.get_proxy_sync(
+ BusType.SESSION,
+ "io.Astal." + instance,
+ "/io/Astal/Application"
+ );
+
+ proxy.quit();
+}
+
+/**
+ * Open the Gtk debug tool of an an Astal instances.
+ * It is the equivalent of `astal --inspector -i instance`.
+ */
+public static void open_inspector(string instance) throws Error {
+ IApplication proxy = Bus.get_proxy_sync(
+ BusType.SESSION,
+ "io.Astal." + instance,
+ "/io/Astal/Application"
+ );
+
+ proxy.inspector();
+}
+
+/**
+ * Toggle a Window of an Astal instances.
+ * It is the equivalent of `astal -i instance --toggle window`.
+ */
+public static void toggle_window_by_name(string instance, string window) throws Error {
+ IApplication proxy = Bus.get_proxy_sync(
+ BusType.SESSION,
+ "io.Astal." + instance,
+ "/io/Astal/Application"
+ );
+
+ proxy.toggle_window(window);
+}
+
+/**
+ * Send a message to an Astal instances.
+ * It is the equivalent of `astal -i instance content of the message`.
+ */
+public static string send_message(string instance, string msg) throws Error {
+ var rundir = Environment.get_user_runtime_dir();
+ var socket_path = @"$rundir/astal/$instance.sock";
+ var client = new SocketClient();
+
+ var conn = client.connect(new UnixSocketAddress(socket_path), null);
+ conn.output_stream.write(msg.concat("\x04").data);
+
+ var stream = new DataInputStream(conn.input_stream);
+ return stream.read_upto("\x04", -1, null, null);
+}
+
+/**
+ * Read the socket of an Astal.Application instance.
+ */
+public async string read_sock(SocketConnection conn) throws IOError {
+ var stream = new DataInputStream(conn.input_stream);
+ return yield stream.read_upto_async("\x04", -1, Priority.DEFAULT, null, null);
+}
+
+/**
+ * Write the socket of an Astal.Application instance.
+ */
+public async void write_sock(SocketConnection conn, string response) throws IOError {
+ yield conn.output_stream.write_async(@"$response\x04".data, Priority.DEFAULT);
+}
+
+[DBus (name="io.Astal.Application")]
+private interface IApplication : DBusProxy {
+ public abstract void quit() throws GLib.Error;
+ public abstract void inspector() throws GLib.Error;
+ public abstract void toggle_window(string window) throws GLib.Error;
+}
+
+[DBus (name="org.freedesktop.DBus")]
+private interface DBusImpl : DBusProxy {
+ public abstract string[] list_names() throws Error;
+}
+}
diff --git a/lib/astal/io/cli.vala b/lib/astal/io/cli.vala
new file mode 100644
index 0000000..f69cf0b
--- /dev/null
+++ b/lib/astal/io/cli.vala
@@ -0,0 +1,104 @@
+static bool version;
+static bool help;
+static bool list;
+static bool quit;
+static bool inspector;
+static string? toggle_window;
+static string? instance_name;
+
+const OptionEntry[] options = {
+ { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null },
+ { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null },
+ { "list", 'l', OptionFlags.NONE, OptionArg.NONE, ref list, null, null },
+ { "quit", 'q', OptionFlags.NONE, OptionArg.NONE, ref quit, null, null },
+ { "inspector", 'I', OptionFlags.NONE, OptionArg.NONE, ref inspector, null, null },
+ { "toggle-window", 't', OptionFlags.NONE, OptionArg.STRING, ref toggle_window, null, null },
+ { "instance", 'i', OptionFlags.NONE, OptionArg.STRING, ref instance_name, null, null },
+ { null },
+};
+
+int err(string msg) {
+ var red = "\x1b[31m";
+ var r = "\x1b[0m";
+ printerr(@"$(red)error: $(r)$msg");
+ return 1;
+}
+
+int main(string[] argv) {
+ try {
+ var opts = new OptionContext();
+ opts.add_main_entries(options, null);
+ opts.set_help_enabled(false);
+ opts.set_ignore_unknown_options(false);
+ opts.parse(ref argv);
+ } catch (OptionError e) {
+ return err(e.message);
+ }
+
+ if (help) {
+ print("Client for Astal.Application instances\n\n");
+ print("Usage:\n");
+ print(" %s [flags] message\n\n", argv[0]);
+ print("Flags:\n");
+ print(" -h, --help Print this help and exit\n");
+ print(" -v, --version Print version number and exit\n");
+ print(" -l, --list List running Astal instances and exit\n");
+ print(" -q, --quit Quit an Astal.Application instance\n");
+ print(" -i, --instance Instance name of the Astal instance\n");
+ print(" -I, --inspector Open up Gtk debug tool\n");
+ print(" -t, --toggle-window Show or hide a window\n");
+ return 0;
+ }
+
+ if (version) {
+ print(AstalIO.VERSION);
+ return 0;
+ }
+
+ if (instance_name == null)
+ instance_name = "astal";
+
+ if (list) {
+ foreach (var name in AstalIO.get_instances())
+ print(@"$name\n");
+
+ return 0;
+ }
+
+ try {
+ if (quit) {
+ AstalIO.quit_instance(instance_name);
+ return 0;
+ }
+
+ if (inspector) {
+ AstalIO.open_inspector(instance_name);
+ return 0;
+ }
+
+ if (toggle_window != null) {
+ AstalIO.toggle_window_by_name(instance_name, toggle_window);
+ return 0;
+ }
+ } catch (DBusError.SERVICE_UNKNOWN e) {
+ return err(@"there is no \"$instance_name\" instance runnning");
+ } catch (Error e) {
+ return err(e.message);
+ }
+
+ var request = "";
+ for (var i = 1; i < argv.length; ++i) {
+ request = request.concat(" ", argv[i]);
+ }
+
+ try {
+ var reply = AstalIO.send_message(instance_name, request);
+ print("%s\n", reply);
+ } catch (IOError.NOT_FOUND e) {
+ return err(@"there is no \"$instance_name\" instance runnning");
+ } catch (Error e) {
+ return err(e.message);
+ }
+
+ return 0;
+}
diff --git a/lib/astal/io/config.vala.in b/lib/astal/io/config.vala.in
new file mode 100644
index 0000000..fe1e450
--- /dev/null
+++ b/lib/astal/io/config.vala.in
@@ -0,0 +1,6 @@
+namespace AstalIO {
+ public const int MAJOR_VERSION = @MAJOR_VERSION@;
+ public const int MINOR_VERSION = @MINOR_VERSION@;
+ public const int MICRO_VERSION = @MICRO_VERSION@;
+ public const string VERSION = "@VERSION@";
+}
diff --git a/lib/astal/io/file.vala b/lib/astal/io/file.vala
new file mode 100644
index 0000000..57b6dc0
--- /dev/null
+++ b/lib/astal/io/file.vala
@@ -0,0 +1,98 @@
+namespace AstalIO {
+/**
+ * Read the contents of a file synchronously.
+ */
+public string read_file(string path) {
+ var str = "";
+ try {
+ FileUtils.get_contents(path, out str, null);
+ } catch (Error error) {
+ critical(error.message);
+ }
+ return str;
+}
+
+/**
+ * Read the contents of a file asynchronously.
+ */
+public async string read_file_async(string path) throws Error {
+ uint8[] content;
+ yield File.new_for_path(path).load_contents_async(null, out content, null);
+ return (string)content;
+}
+
+/**
+ * Write content to a file synchronously.
+ */
+public void write_file(string path, string content) {
+ try {
+ FileUtils.set_contents(path, content);
+ } catch (Error error) {
+ critical(error.message);
+ }
+}
+
+/**
+ * Write content to a file asynchronously.
+ */
+public async void write_file_async(string path, string content) throws Error {
+ yield File.new_for_path(path).replace_contents_async(
+ content.data,
+ null,
+ false,
+ FileCreateFlags.REPLACE_DESTINATION,
+ null,
+ null);
+}
+
+/**
+ * Monitor a file for changes. If the path is a directory, monitor it recursively.
+ * The callback will be called passed two parameters: the path of the file
+ * that changed and an [[email protected]] indicating the reason.
+ */
+public FileMonitor? monitor_file(string path, Closure callback) {
+ try {
+ var file = File.new_for_path(path);
+ var mon = file.monitor(FileMonitorFlags.NONE);
+
+ mon.changed.connect((file, _file, event) => {
+ var f = Value(Type.STRING);
+ var e = Value(Type.INT);
+ var ret = Value(Type.POINTER);
+
+ f.set_string(file.get_path());
+ e.set_int(event);
+
+ callback.invoke(ref ret, { f, e });
+ });
+
+ if (FileUtils.test(path, FileTest.IS_DIR)) {
+ var enumerator = file.enumerate_children("standard::*",
+ FileQueryInfoFlags.NONE, null);
+
+ var i = enumerator.next_file(null);
+ while (i != null) {
+ if (i.get_file_type() == FileType.DIRECTORY) {
+ var filepath = file.get_child(i.get_name()).get_path();
+ if (filepath != null) {
+ var m = monitor_file(path, callback);
+ mon.notify["cancelled"].connect(() => {
+ m.cancel();
+ });
+ }
+ }
+ i = enumerator.next_file(null);
+ }
+ }
+
+ mon.ref();
+ mon.notify["cancelled"].connect(() => {
+ mon.unref();
+ });
+ return mon;
+ } catch (Error error) {
+ critical(error.message);
+ return null;
+ }
+}
+}
diff --git a/lib/astal/io/gir.py b/lib/astal/io/gir.py
new file mode 100644
index 0000000..9ef680f
--- /dev/null
+++ b/lib/astal/io/gir.py
@@ -0,0 +1,58 @@
+"""
+Vala's generated gir does not contain comments,
+so we use valadoc to generate them. However, they are formatted
+for valadoc and not gi-docgen so we need to fix it.
+"""
+
+import xml.etree.ElementTree as ET
+import html
+import sys
+import subprocess
+
+
+def fix_gir(name: str, gir: str, out: str):
+ namespaces = {
+ "": "http://www.gtk.org/introspection/core/1.0",
+ "c": "http://www.gtk.org/introspection/c/1.0",
+ "glib": "http://www.gtk.org/introspection/glib/1.0",
+ }
+ for prefix, uri in namespaces.items():
+ ET.register_namespace(prefix, uri)
+
+ tree = ET.parse(gir)
+ root = tree.getroot()
+
+ for doc in root.findall(".//doc", namespaces):
+ if doc.text:
+ doc.text = (
+ html.unescape(doc.text).replace("<para>", "").replace("</para>", "")
+ )
+
+ if (inc := root.find("c:include", namespaces)) is not None:
+ inc.set("name", f"{name}.h")
+ else:
+ print("no c:include tag found", file=sys.stderr)
+ exit(1)
+
+ tree.write(out, encoding="utf-8", xml_declaration=True)
+
+
+def valadoc(name: str, gir: str, args: list[str]):
+ cmd = ["valadoc", "-o", "docs", "--package-name", name, "--gir", gir, *args]
+ try:
+ subprocess.run(cmd, check=True, text=True, capture_output=True)
+ except subprocess.CalledProcessError as e:
+ print(e.stderr, file=sys.stderr)
+ exit(1)
+
+
+if __name__ == "__main__":
+ name = sys.argv[1]
+ in_out = sys.argv[2].split(":")
+ args = sys.argv[3:]
+
+ gir = in_out[0]
+ out = in_out[1] if len(in_out) > 1 else gir
+
+ valadoc(name, gir, args)
+ fix_gir(name, gir, out)
diff --git a/lib/astal/io/meson.build b/lib/astal/io/meson.build
new file mode 100644
index 0000000..023dece
--- /dev/null
+++ b/lib/astal/io/meson.build
@@ -0,0 +1,106 @@
+project(
+ 'astal-io',
+ 'vala',
+ 'c',
+ version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(),
+ meson_version: '>= 0.62.0',
+ default_options: [
+ 'warning_level=2',
+ 'werror=false',
+ 'c_std=gnu11',
+ ],
+)
+
+version_split = meson.project_version().split('.')
+api_version = version_split[0] + '.' + version_split[1]
+gir = 'AstalIO-' + api_version + '.gir'
+typelib = 'AstalIO-' + api_version + '.typelib'
+libdir = get_option('prefix') / get_option('libdir')
+pkgdatadir = get_option('prefix') / get_option('datadir') / 'astal'
+
+config = configure_file(
+ input: 'config.vala.in',
+ output: 'config.vala',
+ configuration: {
+ 'VERSION': meson.project_version(),
+ 'MAJOR_VERSION': version_split[0],
+ 'MINOR_VERSION': version_split[1],
+ 'MICRO_VERSION': version_split[2],
+ },
+)
+
+deps = [
+ dependency('glib-2.0'),
+ dependency('gio-unix-2.0'),
+ dependency('gobject-2.0'),
+ dependency('gio-2.0'),
+]
+
+sources = [config] + files(
+ 'application.vala',
+ 'file.vala',
+ 'process.vala',
+ 'time.vala',
+ 'variable.vala',
+)
+
+lib = library(
+ meson.project_name(),
+ sources,
+ dependencies: deps,
+ vala_args: ['--vapi-comments'],
+ vala_header: meson.project_name() + '.h',
+ vala_vapi: meson.project_name() + '-' + api_version + '.vapi',
+ version: meson.project_version(),
+ install: true,
+ install_dir: [true, true, true],
+)
+
+pkgs = []
+foreach dep : deps
+ pkgs += ['--pkg=' + dep.name()]
+endforeach
+
+gir_tgt = custom_target(
+ gir,
+ command: [find_program('python3'), files('gir.py'), meson.project_name(), gir]
+ + pkgs
+ + sources,
+ input: sources,
+ depends: lib,
+ output: gir,
+ install: true,
+ install_dir: get_option('datadir') / 'gir-1.0',
+)
+
+custom_target(
+ typelib,
+ command: [
+ find_program('g-ir-compiler'),
+ '--output', '@OUTPUT@',
+ '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@',
+ meson.current_build_dir() / gir,
+ ],
+ input: lib,
+ output: typelib,
+ depends: [lib, gir_tgt],
+ install: true,
+ install_dir: get_option('libdir') / 'girepository-1.0',
+)
+
+import('pkgconfig').generate(
+ lib,
+ name: meson.project_name(),
+ filebase: meson.project_name() + '-' + api_version,
+ version: meson.project_version(),
+ subdirs: meson.project_name(),
+ requires: deps,
+ install_dir: get_option('libdir') / 'pkgconfig',
+)
+
+executable(
+ 'astal',
+ ['cli.vala', sources],
+ dependencies: deps,
+ install: true,
+)
diff --git a/lib/astal/io/process.vala b/lib/astal/io/process.vala
new file mode 100644
index 0000000..cfd05b9
--- /dev/null
+++ b/lib/astal/io/process.vala
@@ -0,0 +1,172 @@
+/**
+ * `Process` provides shortcuts for [[email protected]] with sane defaults.
+ */
+public class AstalIO.Process : Object {
+ private void read_stream(DataInputStream stream, bool err) {
+ stream.read_line_utf8_async.begin(Priority.DEFAULT, null, (_, res) => {
+ try {
+ var output = stream.read_line_utf8_async.end(res);
+ if (output != null) {
+ if (err)
+ stdout(output.strip());
+ else
+ stderr(output.strip());
+
+ read_stream(stream, err);
+ }
+ } catch (Error err) {
+ printerr("%s\n", err.message);
+ }
+ });
+ }
+
+ private DataInputStream out_stream;
+ private DataInputStream err_stream;
+ private DataOutputStream in_stream;
+ private Subprocess process;
+ public string[] argv { construct; get; }
+
+
+ /**
+ * When the underlying subprocess writes to its stdout
+ * this signal is emitted with that line.
+ */
+ public signal void stdout (string out);
+
+ /**
+ * When the underlying subprocess writes to its stderr
+ * this signal is emitted with that line.
+ */
+ public signal void stderr (string err);
+
+ /**
+ * Force quit the subprocess.
+ */
+ public void kill() {
+ process.force_exit();
+ }
+
+ /**
+ * Send a signal to the subprocess.
+ */
+ public void signal(int signal_num) {
+ process.send_signal(signal_num);
+ }
+
+ /**
+ * Write a line to the subprocess' stdin synchronously.
+ */
+ public void write(string in) throws Error {
+ in_stream.put_string(in);
+ }
+
+ /**
+ * Write a line to the subprocess' stdin asynchronously.
+ */
+ public async void write_async(string in) {
+ try {
+ yield in_stream.write_all_async(in.data, in.data.length, null, null);
+ } catch (Error err) {
+ printerr("%s\n", err.message);
+ }
+ }
+
+ /**
+ * Start a new subprocess with the given command.
+ *
+ * The first element of the vector is executed with the remaining elements as the argument list.
+ */
+ public Process.subprocessv(string[] cmd) throws Error {
+ Object(argv: cmd);
+ process = new Subprocess.newv(cmd,
+ SubprocessFlags.STDIN_PIPE |
+ SubprocessFlags.STDERR_PIPE |
+ SubprocessFlags.STDOUT_PIPE
+ );
+ out_stream = new DataInputStream(process.get_stdout_pipe());
+ err_stream = new DataInputStream(process.get_stderr_pipe());
+ in_stream = new DataOutputStream(process.get_stdin_pipe());
+ read_stream(out_stream, true);
+ read_stream(err_stream, false);
+ }
+
+ /**
+ * Start a new subprocess with the given command
+ * which is parsed using [[email protected]_parse_argv].
+ */
+ public static Process subprocess(string cmd) throws Error {
+ string[] argv;
+ Shell.parse_argv(cmd, out argv);
+ return new Process.subprocessv(argv);
+ }
+
+ /**
+ * Execute a command synchronously.
+ * The first element of the vector is executed with the remaining elements as the argument list.
+ *
+ * @return stdout of the subprocess
+ */
+ public static string execv(string[] cmd) throws Error {
+ var process = new Subprocess.newv(
+ cmd,
+ SubprocessFlags.STDERR_PIPE |
+ SubprocessFlags.STDOUT_PIPE
+ );
+
+ string err_str, out_str;
+ process.communicate_utf8(null, null, out out_str, out err_str);
+ var success = process.get_successful();
+ process.dispose();
+ if (success)
+ return out_str.strip();
+ else
+ throw new IOError.FAILED(err_str.strip());
+ }
+
+ /**
+ * Execute a command synchronously.
+ * The command is parsed using [[email protected]_parse_argv].
+ *
+ * @return stdout of the subprocess
+ */
+ public static string exec(string cmd) throws Error {
+ string[] argv;
+ Shell.parse_argv(cmd, out argv);
+ return Process.execv(argv);
+ }
+
+ /**
+ * Execute a command asynchronously.
+ * The first element of the vector is executed with the remaining elements as the argument list.
+ *
+ * @return stdout of the subprocess
+ */
+ public static async string exec_asyncv(string[] cmd) throws Error {
+ var process = new Subprocess.newv(
+ cmd,
+ SubprocessFlags.STDERR_PIPE |
+ SubprocessFlags.STDOUT_PIPE
+ );
+
+ string err_str, out_str;
+ yield process.communicate_utf8_async(null, null, out out_str, out err_str);
+ var success = process.get_successful();
+ process.dispose();
+ if (success)
+ return out_str.strip();
+ else
+ throw new IOError.FAILED(err_str.strip());
+ }
+
+ /**
+ * Execute a command asynchronously.
+ * The command is parsed using [[email protected]_parse_argv].
+ *
+ * @return stdout of the subprocess
+ */
+ public static async string exec_async(string cmd) throws Error {
+ string[] argv;
+ Shell.parse_argv(cmd, out argv);
+ return yield exec_asyncv(argv);
+ }
+}
diff --git a/lib/astal/io/time.vala b/lib/astal/io/time.vala
new file mode 100644
index 0000000..a799f2b
--- /dev/null
+++ b/lib/astal/io/time.vala
@@ -0,0 +1,111 @@
+/**
+ * `Time` provides shortcuts for GLib timeout functions.
+ */
+public class AstalIO.Time : Object {
+ private Cancellable cancellable;
+ private uint timeout_id;
+ private bool fulfilled = false;
+
+ /**
+ * Emitted when the timer ticks.
+ */
+ public signal void now ();
+
+ /**
+ * Emitted when the timere is cancelled.
+ */
+ public signal void cancelled ();
+
+ construct {
+ cancellable = new Cancellable();
+ cancellable.cancelled.connect(() => {
+ if (!fulfilled) {
+ Source.remove(timeout_id);
+ cancelled();
+ dispose();
+ }
+ });
+ }
+
+ private void connect_closure(Closure? closure) {
+ if (closure == null)
+ return;
+
+ now.connect(() => {
+ Value ret = Value(Type.POINTER); // void
+ closure.invoke(ref ret, {});
+ });
+ }
+
+ /**
+ * Start an interval timer with default Priority.
+ */
+ public Time.interval_prio(uint interval, int prio = Priority.DEFAULT, Closure? fn) {
+ connect_closure(fn);
+ Idle.add_once(() => now());
+ timeout_id = Timeout.add(interval, () => {
+ now();
+ return Source.CONTINUE;
+ }, prio);
+ }
+
+ /**
+ * Start a timeout timer with default Priority.
+ */
+ public Time.timeout_prio(uint timeout, int prio = Priority.DEFAULT, Closure? fn) {
+ connect_closure(fn);
+ timeout_id = Timeout.add(timeout, () => {
+ now();
+ fulfilled = true;
+ return Source.REMOVE;
+ }, prio);
+ }
+
+ /**
+ * Start an idle timer with default priority.
+ */
+ public Time.idle_prio(int prio = Priority.DEFAULT_IDLE, Closure? fn) {
+ connect_closure(fn);
+ timeout_id = Idle.add(() => {
+ now();
+ fulfilled = true;
+ return Source.REMOVE;
+ }, prio);
+ }
+
+ /**
+ * Start an interval timer. Ticks immediately then every `interval` milliseconds.
+ *
+ * @param interval Tick every milliseconds.
+ * @param fn Optional callback.
+ */
+ public static Time interval(uint interval, Closure? fn) {
+ return new Time.interval_prio(interval, Priority.DEFAULT, fn);
+ }
+
+ /**
+ * Start a timeout timer which ticks after `timeout` milliseconds.
+ *
+ * @param timeout Tick after milliseconds.
+ * @param fn Optional callback.
+ */
+ public static Time timeout(uint timeout, Closure? fn) {
+ return new Time.timeout_prio(timeout, Priority.DEFAULT, fn);
+ }
+
+ /**
+ * Start a timer which will tick when there are no higher priority tasks pending.
+ *
+ * @param fn Optional callback.
+ */
+ public static Time idle(Closure? fn) {
+ return new Time.idle_prio(Priority.DEFAULT_IDLE, fn);
+ }
+
+ /**
+ * Cancel timer and emit [[email protected]::cancelled]
+ */
+ public void cancel() {
+ cancellable.cancel();
+ }
+}
diff --git a/lib/astal/io/variable.vala b/lib/astal/io/variable.vala
new file mode 100644
index 0000000..312a27a
--- /dev/null
+++ b/lib/astal/io/variable.vala
@@ -0,0 +1,198 @@
+/*
+ * Base class for [[email protected]] mainly meant to be used
+ * in higher level language bindings such as Lua and Gjs.
+ */
+public class AstalIO.VariableBase : Object {
+ public signal void changed ();
+ public signal void dropped ();
+ public signal void error (string err);
+
+ // lua-lgi crashes when using its emitting mechanism
+ public void emit_changed() { changed(); }
+ public void emit_dropped() { dropped(); }
+ public void emit_error(string err) { this.error(err); }
+
+ ~VariableBase() {
+ dropped();
+ }
+}
+
+public class AstalIO.Variable : VariableBase {
+ public Value value { owned get; set; }
+
+ private uint poll_id = 0;
+ private Process? watch_proc;
+
+ private uint poll_interval { get; set; default = 1000; }
+ private string[] poll_exec { get; set; }
+ private Closure? poll_transform { get; set; }
+ private Closure? poll_fn { get; set; }
+
+ private Closure? watch_transform { get; set; }
+ private string[] watch_exec { get; set; }
+
+ public Variable(Value init) {
+ Object(value: init);
+ }
+
+ public Variable poll(
+ uint interval,
+ string exec,
+ Closure? transform
+ ) throws Error {
+ string[] argv;
+ Shell.parse_argv(exec, out argv);
+ return pollv(interval, argv, transform);
+ }
+
+ public Variable pollv(
+ uint interval,
+ string[] execv,
+ Closure? transform
+ ) throws Error {
+ if (is_polling())
+ stop_poll();
+
+ poll_interval = interval;
+ poll_exec = execv;
+ poll_transform = transform;
+ poll_fn = null;
+ start_poll();
+ return this;
+ }
+
+ public Variable pollfn(
+ uint interval,
+ Closure fn
+ ) throws Error {
+ if (is_polling())
+ stop_poll();
+
+ poll_interval = interval;
+ poll_fn = fn;
+ poll_exec = null;
+ start_poll();
+ return this;
+ }
+
+ public Variable watch(
+ string exec,
+ Closure? transform
+ ) throws Error {
+ string[] argv;
+ Shell.parse_argv(exec, out argv);
+ return watchv(argv, transform);
+ }
+
+ public Variable watchv(
+ string[] execv,
+ Closure? transform
+ ) throws Error {
+ if (is_watching())
+ stop_watch();
+
+ watch_exec = execv;
+ watch_transform = transform;
+ start_watch();
+ return this;
+ }
+
+ construct {
+ notify["value"].connect(() => changed());
+ dropped.connect(() => {
+ if (is_polling())
+ stop_poll();
+
+ if (is_watching())
+ stop_watch();
+ });
+ }
+
+ private void set_closure(string val, Closure? transform) {
+ if (transform != null) {
+ var str = Value(typeof(string));
+ str.set_string(val);
+
+ var ret_val = Value(this.value.type());
+ transform.invoke(ref ret_val, { str, this.value });
+ this.value = ret_val;
+ }
+ else {
+ if (this.value.type() == Type.STRING && this.value.get_string() == val)
+ return;
+
+ var str = Value(typeof(string));
+ str.set_string(val);
+ this.value = str;
+ }
+ }
+
+ private void set_fn() {
+ var ret_val = Value(this.value.type());
+ poll_fn.invoke(ref ret_val, { this.value });
+ this.value = ret_val;
+ }
+
+ public void start_poll() throws Error {
+ return_if_fail(poll_id == 0);
+
+ if (poll_fn != null) {
+ set_fn();
+ poll_id = Timeout.add(poll_interval, () => {
+ set_fn();
+ return Source.CONTINUE;
+ }, Priority.DEFAULT);
+ }
+ if (poll_exec != null) {
+ Process.exec_asyncv.begin(poll_exec, (_, res) => {
+ try {
+ var str = Process.exec_asyncv.end(res);
+ set_closure(str, poll_transform);
+ } catch (Error err) {
+ this.error(err.message);
+ }
+ });
+ poll_id = Timeout.add(poll_interval, () => {
+ Process.exec_asyncv.begin(poll_exec, (_, res) => {
+ try {
+ var str = Process.exec_asyncv.end(res);
+ set_closure(str, poll_transform);
+ } catch (Error err) {
+ this.error(err.message);
+ Source.remove(poll_id);
+ poll_id = 0;
+ }
+ });
+ return Source.CONTINUE;
+ }, Priority.DEFAULT);
+ }
+ }
+
+ public void start_watch() throws Error {
+ return_if_fail(watch_proc == null);
+ return_if_fail(watch_exec != null);
+
+ watch_proc = new Process.subprocessv(watch_exec);
+ watch_proc.stdout.connect((str) => set_closure(str, watch_transform));
+ watch_proc.stderr.connect((str) => this.error(str));
+ }
+
+ public void stop_poll() {
+ return_if_fail(poll_id != 0);
+ Source.remove(poll_id);
+ poll_id = 0;
+ }
+
+ public void stop_watch() {
+ return_if_fail(watch_proc != null);
+ watch_proc.kill();
+ watch_proc = null;
+ }
+
+ public bool is_polling() { return poll_id > 0; }
+ public bool is_watching() { return watch_proc != null; }
+
+ ~Variable() {
+ dropped();
+ }
+}
diff --git a/lib/astal/io/version b/lib/astal/io/version
new file mode 100644
index 0000000..6e8bf73
--- /dev/null
+++ b/lib/astal/io/version
@@ -0,0 +1 @@
+0.1.0
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 [[email protected]], 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 [[email protected]:device_type] is [[email protected]_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 [[email protected]:device_type] is [[email protected]].
+ */
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 [[email protected]:device_type] is [[email protected]].
+ */
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 [[email protected]:device_type] is [[email protected]].
+ */
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 [[email protected]:device_type] is [[email protected]].
+ */
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 [[email protected]:device_type] is [[email protected]].
+ */
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 [[email protected]:device_type] is [[email protected]].
+ */
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 [[email protected]:device_type] is [[email protected]].
+ */
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 [[email protected]:device_type] is [[email protected]].
+ * The percentage will be an approximation if [[email protected]: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 [[email protected]:device_type] is [[email protected]].
+ */
public double temperature { get; private set; }
+
+ /**
+ * If the power source is present in the bay.
+ *
+ * This property is only valid if [[email protected]:device_type] is [[email protected]].
+ */
public bool is_present { get; private set; }
+
+ /**
+ * The battery power state.
+ *
+ * This property is only valid if [[email protected]:device_type] is [[email protected]].
+ */
public State state { get; private set; }
+
+ /**
+ * If the power source is rechargeable.
+ *
+ * This property is only valid if [[email protected]:device_type] is [[email protected]].
+ */
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 [[email protected]:device_type] is [[email protected]].
+ */
public double capacity { get; private set; }
+
+ /**
+ * Technology used in the battery:
+ *
+ * This property is only valid if [[email protected]:device_type] is [[email protected]].
+ */
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 [[email protected]:percentage] should be used instead.
+ */
public BatteryLevel battery_level { get; private set; }
+
+ /**
+ * An icon name representing this Device.
+ *
+ * NOTE: [[email protected]:battery_icon_name] might be a better fit
+ * as it is calculated from percentage.
+ */
public string icon_name { owned get; private set; }
+ /**
+ * Indicates if [[email protected]:state] is charging or fully charged.
+ */
public bool charging { get; private set; }
+
+ /**
+ * Indicates if [[email protected]: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 [[email protected]:device_type].
+ */
public string device_type_name { get; private set; }
+
+ /**
+ * An icon name that can be used to represent this device's [[email protected]: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<string, Device> _devices =
new HashTable<string, Device>(str_hash, str_equal);
+ /** List of UPower devices. */
public List<weak Device> 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/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 [[email protected]: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 [[email protected]_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 [[email protected]_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<string, Device> _devices =
new HashTable<string, Device>(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 [[email protected]:adapters] are powered.
+ */
public bool is_powered { get; private set; default = false; }
+
+ /**
+ * `true` if any of the [[email protected]: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<weak Adapter> adapters {
owned get { return _adapters.get_values(); }
}
+ /**
+ * List of registered devices on the `org.bluez` bus.
+ */
public List<weak Device> devices {
owned get { return _devices.get_values(); }
}
@@ -85,6 +124,10 @@ public class Bluetooth : Object {
}
}
+ /**
+ * Toggle the [[email protected]:powered]
+ * property of the [[email protected]: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<string, Variant>)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 [[email protected]: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 [[email protected]: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 [[email protected]].
+ *
+ * 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',
+)
diff --git a/lib/gir.py b/lib/gir.py
new file mode 100644
index 0000000..a0a81dc
--- /dev/null
+++ b/lib/gir.py
@@ -0,0 +1,67 @@
+"""
+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
+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'<ulink url="(.*?)">(.*?)</ulink>'
+ return re.sub(pattern, r"[\2](\1)", text)
+
+
+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 = ulink_to_markdown(
+ html.unescape(doc.text).replace("<para>", "").replace("</para>", "")
+ )
+
+ if (inc := root.find("c:include", namespaces)) is not None:
+ inc.set("name", f"{name}.h")
+ else:
+ print("no c:include tag found", file=sys.stderr)
+ exit(1)
+
+ tree.write(out, encoding="utf-8", xml_declaration=True)
+
+
+def valadoc(name: str, gir: str, args: list[str]):
+ cmd = ["valadoc", "-o", "docs", "--package-name", name, "--gir", gir, *args]
+ try:
+ subprocess.run(cmd, check=True, text=True, capture_output=True)
+ except subprocess.CalledProcessError as e:
+ print(e.stderr, file=sys.stderr)
+ exit(1)
+
+
+if __name__ == "__main__":
+ name = sys.argv[1]
+ in_out = sys.argv[2].split(":")
+ args = sys.argv[3:]
+
+ gir = in_out[0]
+ out = in_out[1] if len(in_out) > 1 else gir
+
+ valadoc(name, gir, args)
+ fix_gir(name, gir, out)
diff --git a/lib/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,
}
}
diff --git a/lib/hyprland/hyprland.vala b/lib/hyprland/hyprland.vala
index 3886486..ea95cab 100644
--- a/lib/hyprland/hyprland.vala
+++ b/lib/hyprland/hyprland.vala
@@ -158,43 +158,43 @@ 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);
+ } 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 "";
}
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<string, Variant> get_all (string iface);
+private interface AstalMpris.PropsIface : DBusProxy {
+ public abstract HashTable<string, Variant> 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<string, Player> _players =
new HashTable<string, Player> (str_hash, str_equal);
+ /**
+ * List of currently available players.
+ */
public List<weak Player> 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..b72c15e 100644
--- a/lib/mpris/player.vala
+++ b/lib/mpris/player.vala
@@ -1,75 +1,179 @@
-namespace AstalMpris {
-public class Player : Object {
+/**
+ * Object which tracks players through their mpris dbus interface.
+ * The most simple way is to use [[email protected]] which tracks every player,
+ * but [[email protected]] 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 [[email protected]: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 [[email protected]: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 [[email protected]: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 [[email protected]] 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 [[email protected]_fullscreen] to toggle fullscreen state.
+ */
public bool fullscreen { get; private set; }
+
+ /**
+ * Indicates if [[email protected]_fullscreen] has any effect.
+ */
public bool can_set_fullscreen { get; private set; }
+
+ /**
+ * Indicates if [[email protected]] 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 [[email protected]: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 [[email protected]: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 [[email protected]: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 [[email protected]: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 [[email protected]: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 [[email protected]: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 [[email protected]:supported_uri_schemas]
+ * and the mime-type should match one of the elements of [[email protected]: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 [[email protected]: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 +189,21 @@ public class Player : Object {
}
}
+ /**
+ * Toggle [[email protected]: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,63 +240,175 @@ 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 [[email protected]: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 [[email protected]:rate] can take.
+ */
public double minimum_rate { get; private set; }
+
+ /**
+ * The maximum value which the [[email protected]:rate] can take.
+ */
public double maximum_rate { get; private set; }
+
+ /**
+ * Indicates if invoking [[email protected]] has effect.
+ */
public bool can_go_next { get; private set; }
+
+ /**
+ * Indicates if invoking [[email protected]] has effect.
+ */
public bool can_go_previous { get; private set; }
+
+ /**
+ * Indicates if invoking [[email protected]] has effect.
+ */
public bool can_play { get; private set; }
+
+ /**
+ * Indicates if invoking [[email protected]] has effect.
+ */
public bool can_pause { get; private set; }
+
+ /**
+ * Indicates if setting [[email protected]:position] has effect.
+ */
public bool can_seek { get; private set; }
- public bool can_control { get; private set; }
- // metadata
- [CCode (notify = false)]
- public HashTable<string,Variant> metadata { owned get; private set; }
+ /**
+ * Indicates if the player can be controlled with
+ * methods such as [[email protected]_pause].
+ */
+ public bool can_control { get; private set; }
+ /**
+ * Metadata hashtable of this player.
+ * In languages that cannot introspect this
+ * use [[email protected]_meta].
+ */
+ [CCode (notify = false)] // notified manually in sync
+ public HashTable<string, Variant> 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 [[email protected]: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 [[email protected]:art_url].
+ */
public string cover_art { owned get; private set; }
+ /**
+ * Lookup a key from [[email protected]: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.
+ * [[email protected]: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);
+ bus_name = name.has_prefix("org.mpris.MediaPlayer2.")
+ ? name : @"org.mpris.MediaPlayer2.$name";
}
private void sync() {
@@ -195,7 +417,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 +532,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 +567,7 @@ public class Player : Object {
}
}
- public void try_proxy() throws Error {
+ private void try_proxy() throws Error {
if (proxy != null)
return;
@@ -360,13 +578,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 +606,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 +623,7 @@ public enum PlaybackStatus {
}
}
- public string to_string() {
+ internal string to_string() {
switch (this) {
case PLAYING:
return "Playing";
@@ -417,13 +636,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 +658,7 @@ public enum Loop {
}
}
- public string? to_string() {
+ internal string? to_string() {
switch (this) {
case NONE:
return "None";
@@ -450,16 +672,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 +694,3 @@ public enum Shuffle {
}
}
}
-}
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..3d4de95 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,30 @@ 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'), meson.project_name(), gir]
+ + pkgs
+ + sources,
+ input: sources,
+ depends: lib,
+ output: gir,
+ install: true,
+ install_dir: get_option('datadir') / 'gir-1.0',
+ )
custom_target(
typelib,
@@ -142,7 +88,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 [[email protected]]
*/
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 [[email protected]] 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 [[email protected]].
*
* @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 [[email protected]] 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 [[email protected]] of the notification.
*
- * Can be invoked by calling {@link Notification.invoke} with the action's id.
+ * Can be invoked by calling [[email protected]] with the action's id.
*/
public List<Action?> 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 [[email protected]]
+ * 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. */
+ /** [[email protected]] 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 [[email protected]] 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 [[email protected]] 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
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 [[email protected]]
+ */
public Tray get_default() {
return Tray.get_default();
}
public class Tray : Object {
private static Tray? instance;
+
+ /**
+ * Get the singleton instance of [[email protected]]
+ */
public static unowned Tray get_default() {
if (instance == null)
instance = new Tray();
@@ -32,13 +38,22 @@ public class Tray : Object {
private HashTable<string, TrayItem> _items =
new HashTable<string, TrayItem>(str_hash, str_equal);
+ /**
+ * List of currently registered tray items
+ */
public List<weak TrayItem> 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..db0e6d4 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<ulong> 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 [[email protected]].
+ */
public bool is_menu { get { return proxy.ItemIsMenu ;} }
-
+
+ /**
+ * the icon theme path, where to look for the [[email protected]:icon-name].
+ *
+ * It is recommended to use the [[email protected]: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 [[email protected]:icon-theme-path]
+ * if set or in the currently used icon theme otherwise.
+ *
+ * It is recommended to use the [[email protected]: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 [[email protected]: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 [[email protected]:icon-name],
+ * [[email protected]:icon-theme-path] and [[email protected]: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<ulong>();
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;
@@ -267,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;