├── .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 | --------------------------------------------------------------------------------