├── .github
└── FUNDING.yml
├── LICENSE
├── Makefile
├── README.md
├── example.gif
└── src
├── dbus.vala
├── notification.vala
└── tiramisu.vala
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [Sweets]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Sweets
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | TARGET = tiramisu
2 | SRC := src/notification.vala src/dbus.vala src/tiramisu.vala
3 |
4 | PREFIX ?= /usr/local
5 | INSTALL = install -Dm755
6 | RM ?= rm -f
7 | PKG_CONFIG ?= pkg-config
8 |
9 | VALAC ?= valac
10 | CFLAGS += -Wall -Wno-unused-value
11 | IFLAGS = --pkg gio-2.0
12 | LFLAGS = `$(PKG_CONFIG) --libs glib-2.0 gio-2.0`
13 |
14 | all: $(TARGET)
15 |
16 | $(TARGET): $(SRC)
17 | $(VALAC) $(IFLAGS) $(SRC) -o $(TARGET)
18 | # $(CC) $(CFLAGS) $(IFLAGS) $(SRC) $(LFLAGS) $(LDFLAGS) -o $(TARGET)
19 |
20 | install: $(TARGET)
21 | mkdir -p $(DESTDIR)$(PREFIX)/bin
22 | $(INSTALL) $(TARGET) $(DESTDIR)$(PREFIX)/bin/$(TARGET)
23 |
24 | clean:
25 | $(RM) ./tiramisu
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | tiramisu
3 | desktop notifications, the UNIX way
4 |
5 |
6 | ---
7 |
8 | tiramisu is a notification daemon for \*nix desktops that implement notifications using dbus.
9 |
10 | Unlike other daemons, tiramisu does not have any sort of window or pop-up, but rather sends all notifications to STDOUT. Doing so enables endless customization from the end-user.
11 |
12 | ---
13 |
14 |
15 | Crafted with ♡
16 |
17 |
18 | - [anufrievroman/polytiramisu](https://github.com/anufrievroman/polytiramisu)
19 |
20 | ---
21 |
22 |
23 | Installation
24 |
25 |
26 | Tiramisu depends upon Vala, gio, and glib.
27 |
28 | |Distribution|Repository|Package name|
29 | |-|-|-|
30 | |Arch Linux|AUR|`tiramisu-git`|
31 | |Alpine Linux|v3.15+|`tiramisu`|
32 | |NixOS|stable|`nixos.tiramisu`|
33 |
34 | Don't see your distribution? Check to make sure it wasn't forgotten at [repology](https://repology.org/projects/?search=tiramisu).
35 | Alternatively, build from source.
36 |
37 | ```sh
38 | $ git clone https://github.com/Sweets/tiramisu
39 | $ cd ./tiramisu
40 | $ make && make install
41 | ```
42 |
43 | ---
44 |
45 |
46 | Usage
47 |
48 |
49 | By default, tiramisu outputs all information from a notification to standard output. You can change this with `-o`, or if you wish to use JSON format, `-j`. If you need the output format to be sanitized (quotes to be escaped), you can do so with `-s`.
50 |
51 | Using `-o` will interpolate your desired format.
52 |
53 | Appropriate keys are `#source`, `#icon`, `#id`, `#summary`, `#body`, `#actions`, `#hints`, and `#timeout`.
54 |
55 | Using `-j` implies `-s`.
56 |
57 | Below is an example of the default output of tiramisu with no flags.
58 |
59 | ```
60 | evolution-mail-notification
61 | evolution
62 | 0
63 | New email in Evolution
64 | You have received 4 new messages.
65 | desktop-entry=org.gnome.Evolution|urgency=1
66 | Show INBOX=default
67 | -1
68 | ```
69 |
--------------------------------------------------------------------------------
/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sweets/tiramisu/5dddd83abd695bfa15640047a97a08ff0a8d9f9b/example.gif
--------------------------------------------------------------------------------
/src/dbus.vala:
--------------------------------------------------------------------------------
1 | [DBus (name = "org.freedesktop.Notifications")]
2 | public class NotificationDaemon : Object {
3 | public static uint notification_id = 1;
4 |
5 | [DBus (name = "GetServerInformation")]
6 | public void get_server_information(out string name,
7 | out string vendor, out string version, out string spec_version)
8 | throws DBusError, IOError {
9 | name = "tiramisu";
10 | vendor = "Sweets";
11 | version = "2.0";
12 | spec_version = "1.2";
13 | }
14 |
15 | [DBus (name = "GetCapabilities")]
16 | public string[] get_capabilities() throws DBusError, IOError {
17 | return {"body", "actions", "icon-static"};
18 | }
19 |
20 | [DBus (name = "Notify")]
21 | public uint Notify(string app_name, uint replaces_id, string app_icon,
22 | string summary, string body, string[] actions,
23 | GLib.HashTable hints,
24 | int expire_timeout) throws DBusError, IOError {
25 |
26 | Notification.output(app_name, replaces_id, app_icon, summary,
27 | body, actions, hints, expire_timeout);
28 |
29 | if (replaces_id == 0)
30 | return notification_id++;
31 |
32 | return replaces_id;
33 | }
34 |
35 | [DBus (name = "CloseNotification")]
36 | public void close_notification(uint id) throws DBusError, IOError {
37 | // close notification
38 | }
39 |
40 | [DBus (name = "NotificationClosed")]
41 | public void notification_closed(uint id,
42 | uint reason) throws DBusError, IOError {
43 | // notification was closed
44 | }
45 |
46 |
47 | // action_invoked
48 | }
49 |
50 |
--------------------------------------------------------------------------------
/src/notification.vala:
--------------------------------------------------------------------------------
1 | string sanitize(string subj) {
2 | return subj
3 | .replace("\"", "\\\"")
4 | .replace("\r", "\\r")
5 | .replace("\n", "\\n"); // ideally all control sequences \u0000 to \u001f
6 | }
7 |
8 | public class Notification : Object {
9 | public static string image_get_base64_representation(GLib.Variant image) {
10 | uint width = 0, height = 0, row_stride = 0, bits_per_sample = 0,
11 | channels = 0;
12 | bool alpha = false;
13 |
14 | uint8[] pixels = {};
15 | GLib.Variant pixel_data = null;
16 |
17 | image.get("(iiibii@ay)",
18 | out width, out height, out row_stride, out alpha,
19 | out bits_per_sample, out channels, out pixel_data);
20 |
21 | pixels = pixel_data.get_data_as_bytes().get_data();
22 | string encoded_image = GLib.Base64.encode((uchar[])pixels);
23 |
24 | return "".concat(@"$(width):$(height):$(row_stride):",
25 | @"$(alpha):$(bits_per_sample):$(channels):", encoded_image);
26 | }
27 |
28 | public static string create_hint_json_string(
29 | GLib.HashTable hints) {
30 | // Only if Tiramisu.json is true. Implies Tiramisu.sanitize.
31 |
32 | string output = "";
33 | string buffer = "";
34 | string separator = "";
35 |
36 | GLib.VariantType image_type = new GLib.VariantType("(iiibiiay)");
37 |
38 | hints.foreach((key, value) => {
39 | string _key = key;
40 | string _value = "";
41 |
42 | _key = sanitize(_key);
43 | buffer = buffer.concat(separator, @"\"$(_key)\": ");
44 |
45 | if (value.is_of_type(GLib.VariantType.STRING)) {
46 | _value = value.print(false);
47 | _value = _value.substring(1, _value.length - 2);
48 | _value = sanitize(_value);
49 | _value = @"\"$(_value)\"";
50 | } else if (value.is_of_type(image_type)) {
51 | _value = @"\"$(image_get_base64_representation(value))\"";
52 | } else {
53 | _value = @"\"$(value.print(false))\"";
54 | }
55 |
56 | buffer = buffer.concat(@"$(_value)");
57 | output = output.concat(buffer);
58 |
59 | buffer = "";
60 | separator = ", ";
61 | });
62 |
63 | return @"{$(output)}";
64 | }
65 |
66 | public static string create_hint_csv_string(
67 | GLib.HashTable hints) {
68 |
69 | string output = "";
70 | string buffer = "";
71 | string separator = "";
72 |
73 | GLib.VariantType image_type = new GLib.VariantType("(iiibiiay)");
74 |
75 | hints.foreach((key, value) => {
76 | string _key = key;
77 | string _value = "";
78 |
79 | if (value.is_of_type(GLib.VariantType.STRING)) {
80 | _value = value.print(false);
81 | _value = _value.substring(1, _value.length - 2);
82 | _value = sanitize(_value);
83 | _value = @"'$(_value)'";
84 | } else if (value.is_of_type(image_type)) {
85 | _value = image_get_base64_representation(value);
86 | } else {
87 | _value = value.print(false);
88 | }
89 |
90 | if (Tiramisu.sanitize) {
91 | _key = sanitize(_key);
92 | _value = sanitize(_value);
93 | }
94 |
95 | buffer = buffer.concat(separator, @"$(_key)=$(_value)");
96 | output = output.concat(buffer);
97 |
98 | buffer = "";
99 | separator = ",";
100 | });
101 |
102 | return output;
103 | }
104 |
105 | public static void output(string source, uint replaces_id, string icon,
106 | string summary, string body, string[] actions,
107 | GLib.HashTable hints, int timeout) {
108 |
109 | string app_name = source;
110 | string app_icon = icon;
111 | string _summary = summary;
112 | string _body = body;
113 |
114 | string hint_string = "";
115 | string fmt = Tiramisu.format;
116 |
117 | if (Tiramisu.json) {
118 | fmt = Tiramisu.json_format;
119 | hint_string = create_hint_json_string(hints);
120 | } else {
121 | hint_string = create_hint_csv_string(hints);
122 | }
123 |
124 | if (Tiramisu.sanitize) {
125 | app_name = sanitize(app_name);
126 | app_icon = sanitize(app_icon);
127 | _summary = sanitize(_summary);
128 | _body = sanitize(_body);
129 | }
130 |
131 | fmt = fmt
132 | .replace("#source", app_name)
133 | .replace("#id", replaces_id.to_string())
134 | .replace("#icon", app_icon)
135 | .replace("#summary", _summary)
136 | .replace("#body", _body)
137 | .replace("#actions", string.joinv(",", actions))
138 | .replace("#hints", hint_string)
139 | .replace("#timeout", timeout.to_string());
140 |
141 | stdout.printf(fmt + "\n");
142 | stdout.flush();
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/src/tiramisu.vala:
--------------------------------------------------------------------------------
1 | public class Tiramisu : Application {
2 | public static string format = "#source\n#icon\n#id\n#summary\n" +
3 | "#body\n#actions\n#hints\n#timeout";
4 | public static string json_format = "{\"source\": \"#source\", " +
5 | "\"id\": #id, \"summary\": \"#summary\", \"body\": \"#body\", " +
6 | "\"icon\": \"#icon\", \"actions\": \"#actions\", \"hints\": #hints, " +
7 | "\"timeout\": #timeout}";
8 |
9 | public static bool sanitize = false;
10 | public static bool json = false;
11 |
12 | private const OptionEntry[] options = {
13 | {"format", 'o', OptionFlags.NONE, OptionArg.STRING, ref format,
14 | "Output format specifier", "FORMAT"},
15 |
16 | {"json", 'j', OptionFlags.NONE, OptionArg.NONE, ref json,
17 | "Output using JSON (implies --sanitize)", null},
18 |
19 | {"sanitize", 's', OptionFlags.NONE, OptionArg.NONE, ref sanitize,
20 | "Sanitize output; escapes quotes", null},
21 | {null}
22 | };
23 |
24 | private Tiramisu() {
25 | this.add_main_option_entries(options);
26 | }
27 |
28 | public override void activate() {
29 | this.hold();
30 |
31 | if (json)
32 | sanitize = true;
33 |
34 | Bus.own_name(BusType.SESSION, "org.freedesktop.Notifications",
35 | BusNameOwnerFlags.DO_NOT_QUEUE,
36 | (connection) => {
37 | try {
38 | connection.register_object("/org/freedesktop/Notifications",
39 | new NotificationDaemon());
40 | } catch (IOError _error) {
41 | error("Unable to register object path.");
42 | }
43 | }, // Bus acquired
44 | () => {
45 | }, // Name acquired
46 | () => {
47 | error("Unable to acquired DBus name.");
48 | } // Unable to acquire
49 | );
50 | }
51 |
52 | public static int main(string[] arguments) {
53 | Tiramisu tiramisu = new Tiramisu();
54 | return tiramisu.run(arguments);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------