fixes, features

* cli dnd-toggle
* save dnd and ignore_timeout state
* block contstructor until dbus acqusition
* fix proxy notify signals
This commit is contained in:
Aylur
2024-07-31 14:01:04 +02:00
parent 7757661ac7
commit e0c70f0972
5 changed files with 142 additions and 98 deletions
+29 -25
View File
@@ -5,6 +5,7 @@ static bool list;
static string invoke;
static int close_n;
static int get_n;
static bool toggle_dnd;
const OptionEntry[] options = {
{ "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null },
@@ -14,6 +15,7 @@ const OptionEntry[] options = {
{ "invoke", 'i', OptionFlags.NONE, OptionArg.STRING, ref invoke, null, null },
{ "close", 'c', OptionFlags.NONE, OptionArg.INT, ref close_n, null, null },
{ "get", 'g', OptionFlags.NONE, OptionArg.INT, ref get_n, null, null },
{ "toggle-dnd", 't', OptionFlags.NONE, OptionArg.NONE, ref toggle_dnd, null, null },
{ null },
};
@@ -34,17 +36,17 @@ int main(string[] argv) {
print("Usage:\n");
print(" %s [flags]\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 Print every notification and exit\n");
print(" -d, --daemonize Watch for new notifications\n");
print(" -i, --invoke Invoke a notification action\n");
print(" -c, --close Close a notification by its id\n");
print(" -g, --get Print a notification by its id\n");
print(" -h, --help Print this help and exit\n");
print(" -v, --version Print version number and exit\n");
print(" -l, --list Print every notification and exit\n");
print(" -d, --daemonize Watch for new notifications\n");
print(" -i, --invoke Invoke a notification action\n");
print(" -c, --close Close a notification by its id\n");
print(" -g, --get Print a notification by its id\n");
print(" -t, --toggle-dnd Toggle do not disturb\n");
return 0;
}
var loop = new MainLoop();
var notifd = new AstalNotifd.Notifd();
if (version) {
@@ -53,16 +55,27 @@ int main(string[] argv) {
}
if (list) {
var cache = Environment.get_user_cache_dir() + "/astal/notifd/notifications.json";
if (FileUtils.test(cache, FileTest.EXISTS)) {
var state = Environment.get_user_state_dir() + "/astal/notifd/notifications.json";
if (FileUtils.test(state, FileTest.EXISTS)) {
try {
uint8[] json;
File.new_for_path(cache).load_contents(null, out json, null);
stdout.printf("%s", (string)json);
File.new_for_path(state).load_contents(null, out json, null);
var obj = Json.from_string((string)json);
var list = obj.get_object().get_member("notifications");
stdout.printf("%s\n", Json.to_string(list, true));
return 0;
} catch (Error err) {
stderr.printf("failed to load cache: %s", err.message);
}
}
stdout.printf("[]\n");
return 0;
}
if (toggle_dnd) {
notifd.dont_disturb = !notifd.dont_disturb;
return 0;
}
@@ -71,6 +84,7 @@ int main(string[] argv) {
stdout.printf("%s\n", notifd.get_notification_json(id));
stdout.flush();
});
new MainLoop().run();
}
if (invoke != null) {
@@ -83,29 +97,19 @@ int main(string[] argv) {
var n_id = int.parse(split[0]);
var a_id = split[1];
notifd.active.connect(() => {
notifd.get_notification(n_id).invoke(a_id);
loop.quit();
});
notifd.get_notification(n_id).invoke(a_id);
}
if (close_n > 0) {
notifd.active.connect(() => {
notifd.get_notification(close_n).dismiss();
loop.quit();
});
notifd.get_notification(close_n).dismiss();
}
if (get_n > 0) {
notifd.active.connect(() => {
stdout.printf("%s", notifd.get_notification(get_n).to_json_string());
loop.quit();
});
stdout.printf("%s", notifd.get_notification(get_n).to_json_string());
}
if (!daemonize && invoke == null && close_n == 0 && get_n == 0)
return 1;
loop.run();
return 0;
}
+57 -29
View File
@@ -1,30 +1,39 @@
namespace AstalNotifd {
public enum ClosedReason {
EXPIRED = 1,
DISMISSED_BY_USER = 2,
CLOSED = 3,
UNDEFINED = 4,
}
[DBus (name = "org.freedesktop.Notifications")]
internal class Daemon : Object {
internal class AstalNotifd.Daemon : Object {
public static string name = "notifd";
public static string vendor = "astal";
public static string version = "0.1";
private string cache_file;
private string state_file;
private string state_directory;
private string cache_directory;
private uint n_id = 1;
private HashTable<uint, Notification> notifs =
new HashTable<uint, Notification>((i) => i, (a, b) => a == b);
public bool ignore_timeout { get; set; }
public bool dont_disturb { get; set; }
private bool _ignore_timeout;
public bool ignore_timeout {
get { return _ignore_timeout; }
set {
_ignore_timeout = value;
write_state();
}
}
private bool _dont_disturb;
public bool dont_disturb {
get { return _dont_disturb; }
set {
_dont_disturb = value;
write_state();
}
}
public signal void notified(uint id, bool replaced);
public signal void resolved(uint id, ClosedReason reason);
public signal void action_invoked(uint id, string action);
public signal void prop_changed(string prop);
// emitting an event from proxy doesn't seem to work
public void emit_resolved(uint id, ClosedReason reason) { resolved(id, reason); }
@@ -32,24 +41,31 @@ internal class Daemon : Object {
construct {
cache_directory = Environment.get_user_cache_dir() + "/astal/notifd";
cache_file = cache_directory + "/notifications.json";
state_directory = Environment.get_user_state_dir() + "/astal/notifd";
state_file = state_directory + "/notifications.json";
if (FileUtils.test(cache_file, FileTest.EXISTS)) {
if (FileUtils.test(state_file, FileTest.EXISTS)) {
try {
uint8[] json;
File.new_for_path(cache_file).load_contents(null, out json, null);
var parser = new Json.Parser();
parser.load_from_data((string)json);
var list = parser.get_root().get_array();
File.new_for_path(state_file).load_contents(null, out json, null);
var obj = Json.from_string((string)json);
var list = obj.get_object().get_array_member("notifications");
for (var i = 0; i < list.get_length(); ++i) {
add_notification(new Notification.from_json(list.get_object_element(i)));
}
n_id = list.get_length() + 1;
_dont_disturb = obj.get_object().get_boolean_member("dont_disturb");
_ignore_timeout = obj.get_object().get_boolean_member("ignore_timeout");
} catch (Error err) {
warning("failed to load cache: %s", err.message);
}
}
notify.connect((prop) => prop_changed(prop.name));
notified.connect(() => {
notify_property("notifications");
});
@@ -57,7 +73,7 @@ internal class Daemon : Object {
resolved.connect((id, reason) => {
notifs.get(id).resolved(reason);
notifs.remove(id);
cache();
write_state();
notify_property("notifications");
notification_closed(id, reason);
});
@@ -123,7 +139,7 @@ internal class Daemon : Object {
notified(id, replaced);
cache();
write_state();
return id;
}
@@ -135,21 +151,25 @@ internal class Daemon : Object {
return replaced;
}
private void cache() {
private void write_state() {
var list = new Json.Builder().begin_array();
foreach (var n in notifications) {
list.add_value(n.to_json());
}
list.end_array();
var generator = new Json.Generator();
generator.set_root(list.get_root());
var json = generator.to_data(null);
var obj = new Json.Builder()
.begin_object()
.set_member_name("notifications").add_value(list.get_root())
.set_member_name("ignore_timeout").add_boolean_value(ignore_timeout)
.set_member_name("dont_disturb").add_boolean_value(dont_disturb)
.end_object();
try {
if (!FileUtils.test(cache_directory, FileTest.EXISTS))
File.new_for_path(cache_directory).make_directory_with_parents(null);
if (!FileUtils.test(state_directory, FileTest.EXISTS))
File.new_for_path(state_directory).make_directory_with_parents(null);
FileUtils.set_contents_full(cache_file, json);
FileUtils.set_contents_full(state_file, Json.to_string(obj.get_root(), false));
} catch (Error err) {
warning("failed to cache notifications: %s", err.message);
}
@@ -201,6 +221,9 @@ internal class Daemon : Object {
var file_name = cache_directory + "/" + data.hash().to_string("%u.png");
try {
if (!FileUtils.test(cache_directory, FileTest.EXISTS))
File.new_for_path(cache_directory).make_directory_with_parents(null);
var output_stream = File.new_for_path(file_name)
.replace(null, false, FileCreateFlags.NONE, null);
@@ -214,8 +237,7 @@ internal class Daemon : Object {
return file_name;
}
[DBus (visible = false)]
public Daemon register(DBusConnection conn) {
internal Daemon register(DBusConnection conn) {
try {
conn.register_object("/org/freedesktop/Notifications", this);
} catch (Error err) {
@@ -224,4 +246,10 @@ internal class Daemon : Object {
return this;
}
}
public enum AstalNotifd.ClosedReason {
EXPIRED = 1,
DISMISSED_BY_USER = 2,
CLOSED = 3,
UNDEFINED = 4,
}
+29 -15
View File
@@ -1,9 +1,10 @@
namespace AstalNotifd {
public Notifd get_default() {
return Notifd.get_default();
public Notifd get_default() {
return Notifd.get_default();
}
}
public class Notifd : Object {
public class AstalNotifd.Notifd : Object {
private static Notifd _instance;
public static Notifd get_default() {
if (_instance == null)
@@ -62,7 +63,13 @@ public class Notifd : Object {
construct {
// hack to make it synchronous
var loop = new MainLoop();
MainLoop? loop = null;
if (!MainContext.default().is_owner()) {
loop = new MainLoop();
}
bool done = false;
Bus.own_name(
BusType.SESSION,
@@ -74,22 +81,23 @@ public class Notifd : Object {
);
active.connect(() => {
if (loop.is_running())
done = true;
if (loop != null && loop.is_running()) {
loop.quit();
}
});
loop.run();
if (loop != null) {
loop.run();
} else {
while (!done) {
MainContext.default().iteration(false);
}
}
}
private void acquire_daemon(DBusConnection conn) {
daemon = new Daemon().register(conn);
daemon.notified.connect((id, replaced) => notified(id, replaced));
daemon.resolved.connect((id, reason) => resolved(id, reason));
daemon.notify.connect((prop) => {
if (get_class().find_property(prop.name) != null) {
notify_property(prop.name);
}
});
}
private void on_daemon_acquired() {
@@ -97,6 +105,13 @@ public class Notifd : Object {
proxy.stop();
proxy = null;
}
daemon.notified.connect((id, replaced) => notified(id, replaced));
daemon.resolved.connect((id, reason) => resolved(id, reason));
daemon.notify.connect((prop) => {
if (get_class().find_property(prop.name) != null) {
notify_property(prop.name);
}
});
active(ActiveType.DAEMON);
}
@@ -119,8 +134,7 @@ public class Notifd : Object {
}
}
public enum ActiveType {
public enum AstalNotifd.ActiveType {
DAEMON,
PROXY,
}
}
+15 -18
View File
@@ -1,21 +1,16 @@
namespace AstalNotifd {
public enum Urgency {
public enum AstalNotifd.Urgency {
LOW = 0,
NORMAL = 1,
CRITICAL = 2,
}
public class Action {
public Action(string id, string label) {
this.id = id;
this.label = label;
}
public struct AstalNotifd.Action {
public string id;
public string label;
}
public class Notification : Object {
private List<Action> _actions;
public class AstalNotifd.Notification : Object {
private List<Action?> _actions;
private HashTable<string, Variant> hints;
public int64 time { construct set; get; }
@@ -25,7 +20,7 @@ public class Notification : Object {
public string body { construct set; get; }
public uint id { construct set; get; }
public int expire_timeout { construct set; get; }
public List<Action> actions { get { return _actions; } }
public List<Action?> actions { get { return _actions; } }
public string image { get { return get_str_hint("image-path"); } }
public bool action_icons { get { return get_bool_hint("action-icons"); } }
@@ -61,9 +56,12 @@ public class Notification : Object {
);
this.hints = hints;
_actions = new List<Action>();
_actions = new List<Action?>();
for (var i = 0; i < actions.length; i += 2) {
_actions.append(new Action(actions[i], actions[i + 1]));
_actions.append(Action() {
id = actions[i],
label = actions[i + 1]
});
}
}
@@ -109,13 +107,13 @@ public class Notification : Object {
}
break;
case "actions":
_actions = new List<Action>();
_actions = new List<Action?>();
for (var i = 0; i < node.get_array().get_length(); ++i) {
var o = node.get_array().get_object_element(i);
_actions.append(new Action(
o.get_member("id").get_string(),
o.get_member("label").get_string()
));
_actions.append(Action() {
id = o.get_member("id").get_string(),
label = o.get_member("label").get_string()
});
}
break;
default: break;
@@ -160,4 +158,3 @@ public class Notification : Object {
.get_root();
}
}
}
+12 -11
View File
@@ -1,6 +1,5 @@
namespace AstalNotifd {
[DBus (name = "org.freedesktop.Notifications")]
internal interface IDaemon : Object {
internal interface AstalNotifd.IDaemon : DBusProxy {
public abstract bool ignore_timeout { get; set; }
public abstract bool dont_disturb { get; set; }
@@ -9,12 +8,13 @@ internal interface IDaemon : Object {
public signal void notified(uint id, bool replaced);
public signal void resolved(uint id, ClosedReason reason);
public signal void prop_changed(string prop);
public abstract void emit_resolved(uint id, ClosedReason reason);
public abstract void emit_action_invoked(uint id, string action);
}
internal class DaemonProxy : Object {
internal class AstalNotifd.DaemonProxy : Object {
private HashTable<uint, Notification> notifs =
new HashTable<uint, Notification>((i) => i, (a, b) => a == b);
@@ -43,8 +43,8 @@ internal class DaemonProxy : Object {
public signal void notified(uint id, bool replaced);
public signal void resolved(uint id, ClosedReason reason);
IDaemon proxy;
List<ulong> ids = new List<ulong>();
private IDaemon proxy;
private List<ulong> ids = new List<ulong>();
public void stop() {
if (ids.length() > 0) {
@@ -72,8 +72,8 @@ internal class DaemonProxy : Object {
var version = variant.get_child_value(2).get_string();
var running = name == Daemon.name
&& vendor == Daemon.vendor
&& version == Daemon.version;
&& vendor == Daemon.vendor
&& version == Daemon.version;
if (running) {
setup_proxy();
@@ -97,19 +97,21 @@ internal class DaemonProxy : Object {
foreach (var id in proxy.notification_ids())
add_notification(id);
ids.append(proxy.notify.connect((pspec) => {
if (get_class().find_property(pspec.name) != null)
notify_property(pspec.name);
ids.append(proxy.prop_changed.connect((prop) => {
if (prop == "ignore-timeout" || prop == "dont-disturb")
notify_property(prop);
}));
ids.append(proxy.notified.connect((id, replaced) => {
add_notification(id);
notified(id, replaced);
notify_property("notifications");
}));
ids.append(proxy.resolved.connect((id, reason) => {
notifs.remove(id);
resolved(id, reason);
notify_property("notifications");
}));
}
@@ -125,4 +127,3 @@ internal class DaemonProxy : Object {
}
}
}
}