Files
astal/lib/notifd/daemon.vala
2024-09-09 01:01:48 +00:00

257 lines
8.3 KiB
Vala

[DBus (name = "org.freedesktop.Notifications")]
internal class AstalNotifd.Daemon : Object {
public static string name = "notifd";
public static string vendor = "astal";
public static string version = "0.1";
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);
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); }
public void emit_action_invoked(uint id, string action) { action_invoked(id, action); }
construct {
cache_directory = Environment.get_user_cache_dir() + "/astal/notifd";
state_directory = Environment.get_user_state_dir() + "/astal/notifd";
state_file = state_directory + "/notifications.json";
if (FileUtils.test(state_file, FileTest.EXISTS)) {
try {
uint8[] json;
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");
});
resolved.connect((id, reason) => {
notifs.get(id).resolved(reason);
notifs.remove(id);
write_state();
notify_property("notifications");
notification_closed(id, reason);
});
}
public uint[] notification_ids() throws DBusError, IOError {
var keys = notifs.get_keys();
uint[] id = new uint[keys.length()];
for (var i = 0; i < keys.length(); ++i)
id[i] = keys.nth_data(i);
return id;
}
[DBus (visible = false)]
public List<weak Notification> notifications {
owned get { return notifs.get_values(); }
}
[DBus (visible = false)]
public Notification get_notification(uint id) {
return notifs.get(id);
}
public string get_notification_json(uint id) throws DBusError, IOError {
return notifs.get(id).to_json_string();
}
[DBus (name = "Notify")]
public uint Notify(
string app_name,
uint replaces_id,
string app_icon,
string summary,
string body,
string[] actions,
HashTable<string, Variant> hints,
int expire_timeout
) throws DBusError, IOError {
if (hints.get("image-data") != null) {
var file = cache_image(hints.get("image-data"), app_name);
if (file != null) {
hints.set("image-path", new Variant.string(file));
hints.remove("image-data");
}
}
// deprecated hints
hints.remove("image_data");
hints.remove("icon_data");
var id = notifs.contains(replaces_id) ? replaces_id : n_id++;
// TODO: update existing Notification object when replaced
var replaced = add_notification(new Notification(
app_name, id, app_icon, summary, body, actions, hints, expire_timeout
));
if (!ignore_timeout && expire_timeout > 0) {
Timeout.add(expire_timeout, () => {
resolved(id, ClosedReason.EXPIRED);
return Source.REMOVE;
}, Priority.DEFAULT);
}
notified(id, replaced);
write_state();
return id;
}
private bool add_notification(Notification n) {
n.dismissed.connect(() => resolved(n.id, ClosedReason.DISMISSED_BY_USER));
n.invoked.connect((action) => action_invoked(n.id, action));
var replaced = notifs.contains(n.id);
notifs.set(n.id, n);
return replaced;
}
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 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(state_directory, FileTest.EXISTS))
File.new_for_path(state_directory).make_directory_with_parents(null);
FileUtils.set_contents_full(state_file, Json.to_string(obj.get_root(), false));
} catch (Error err) {
warning("failed to cache notifications: %s", err.message);
}
}
public signal void notification_closed(uint id, uint reason);
public signal void activation_token(uint id, string token);
public void close_notification(uint id) throws DBusError, IOError {
resolved(id, ClosedReason.CLOSED);
}
public void get_server_information(
out string name,
out string vendor,
out string version,
out string spec_version
) throws DBusError, IOError {
name = Daemon.name;
vendor = Daemon.vendor;
version = Daemon.version;
spec_version = "1.2";
}
public string[] get_capabilities() throws DBusError, IOError {
return {"action-icons", "actions", "body", "icon-static", "persistence", "sound"};
}
private string? cache_image(Variant image, string app_name) {
int w = image.get_child_value(0).get_int32();
int h = image.get_child_value(1).get_int32();
int rs = image.get_child_value(2).get_int32();
bool alpha = image.get_child_value(3).get_boolean();
int bps = image.get_child_value(4).get_int32();
Bytes data = image.get_child_value(6).get_data_as_bytes();
if (bps != 8) {
warning("Can not cache image from %s. %s", app_name,
"Currently only RGB images with 8 bits per sample are supported.");
return null;
}
var pixbuf = new Gdk.Pixbuf.from_bytes(
data, Gdk.Colorspace.RGB, alpha, bps, w, h, rs);
if (pixbuf == null)
return null;
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);
pixbuf.save_to_streamv(output_stream, "png", null, null, null);
output_stream.close(null);
} catch (Error err) {
warning("could not cache image %s", err.message);
return null;
}
return file_name;
}
internal Daemon register(DBusConnection conn) {
try {
conn.register_object("/org/freedesktop/Notifications", this);
} catch (Error err) {
critical(err.message);
}
return this;
}
}
public enum AstalNotifd.ClosedReason {
EXPIRED = 1,
DISMISSED_BY_USER = 2,
CLOSED = 3,
UNDEFINED = 4,
}