Unverified Commit 84f38b67 authored by Erik Reider's avatar Erik Reider Committed by GitHub

Added Notification action filtering (#654)

* Initial Action matching class for config added * Added filtering logic * Added documentation * Handle config serialization for notification-action-filter
parent 3e83f1f4
......@@ -64,6 +64,7 @@ Post your setup here: [Config flex 💪](https://github.com/ErikReider/SwayNotif
- A panel to view previous notifications
- Show album art for notifications like Spotify
- Do not disturb
- Notification action filtering
- Inhibiting notifications through DBUS or client
- Restores previous Do not disturb value after restart
- Click notification to execute default action
......
......@@ -191,6 +191,51 @@ config file to be able to detect config errors
(ex: "Acer Technologies XV272U V 503023B314202"). If the ++
output is not found, the currently focused one is picked.
*notification-action-filter* ++
type: object ++
visibility object properties: ++
*use-regex*++
type: bool ++
optional: true ++
default: false ++
description: Indicates if all the below fields should use ++
regex or not.++
*app-name*++
type: string ++
optional: true ++
description: The app-name.++
*desktop-entry*++
type: string ++
optional: true ++
description: The desktop-entry.++
*id-matcher*++
type: string ++
optional: true (needs at least one *matcher*) ++
description: Matches the actions identifier. Can be found by ++
reading the output of swaync when run with the ++
*G_MESSAGES_DEBUG=all* environment variable.++
*text-matcher*++
type: string ++
optional: true (needs at least one *matcher*) ++
description: Matches the actions text visible in the notification. ++
description: Hides matching action(s) of matching notifications. ++
If the notification doesn't include one of the properties, that ++
property will be ignored. If all properties match the given ++
notification, the matching actions will be hidden. ++
example:
```
{
"notification-action-filter": {
"hide-chromium-settings": {
"desktop-entry": "chromium-browser",
"use-regex": false,
"id-matcher": "settings",
"text-matcher": "Settings"
}
},
}
```
*notification-visibility* ++
type: object ++
visibility object properties: ++
......
......@@ -80,6 +80,20 @@ namespace SwayNotificationCenter {
}
}
private struct MatchCase {
string ? pattern;
string ? str;
public MatchCase (string ? pattern, string ? str) {
this.pattern = pattern;
this.str = str;
}
public bool is_valid () {
return pattern != null && str != null;
}
}
public class NotificationMatching : Object, Json.Serializable {
public string ? app_name { get; set; default = null; }
public string ? desktop_entry { get; set; default = null; }
......@@ -188,6 +202,112 @@ namespace SwayNotificationCenter {
}
node.set_string (eval.value_nick);
return node;
} else if (value.type ().is_a (Type.BOOLEAN)) {
// Somehow the default serializer doesn't handle booleans :/
var node = new Json.Node (Json.NodeType.VALUE);
node.set_boolean (value.get_boolean ());
return node;
}
return default_serialize_property (property_name, value, pspec);
}
}
public class ActionMatching : Object, Json.Serializable {
public string ? app_name { get; set; default = null; }
public string ? desktop_entry { get; set; default = null; }
public string ? id_matcher { get; set; }
public string ? text_matcher { get; set; }
public bool use_regex { get; set; default = false; }
private const RegexCompileFlags REGEX_COMPILE_OPTIONS =
RegexCompileFlags.MULTILINE;
private const RegexMatchFlags REGEX_MATCH_FLAGS = RegexMatchFlags.NOTEMPTY;
public bool matches_action (Action action) {
MatchCase[] matchers = {
MatchCase (id_matcher, action.identifier),
MatchCase (text_matcher, action.text),
};
foreach (MatchCase matcher in matchers) {
if (!matcher.is_valid ()) {
continue;
}
if (use_regex) {
bool result = Regex.match_simple (
matcher.pattern, matcher.str,
REGEX_COMPILE_OPTIONS,
REGEX_MATCH_FLAGS);
if (!result) return false;
} else {
if (matcher.pattern != matcher.str) {
return false;
}
}
}
return true;
}
public bool matches_notification (NotifyParams param) {
if (id_matcher == null && text_matcher == null) {
critical ("Action filter doesn't contain a matcher: %s", this.to_string ());
return false;
}
MatchCase[] matchers = {
MatchCase (app_name, param.app_name),
MatchCase (desktop_entry, param.desktop_entry),
};
foreach (MatchCase matcher in matchers) {
if (!matcher.is_valid ()) {
continue;
}
if (use_regex) {
bool result = Regex.match_simple (
matcher.pattern, matcher.str,
REGEX_COMPILE_OPTIONS,
REGEX_MATCH_FLAGS);
if (!result) return false;
} else {
if (matcher.pattern != matcher.str) {
return false;
}
}
}
return true;
}
public string to_string () {
string[] fields = {};
if (app_name != null) fields += "app-name: %s".printf (app_name);
if (desktop_entry != null) fields += "desktop-entry: %s".printf (desktop_entry);
if (id_matcher != null) fields += "name_matcher: %s".printf (id_matcher);
if (text_matcher != null) fields += "text_matcher: %s".printf (text_matcher);
return string.joinv (", ", fields);
}
public override Json.Node serialize_property (string property_name,
Value value,
ParamSpec pspec) {
// Return enum nickname instead of enum int value
if (value.type ().is_a (Type.ENUM)) {
var node = new Json.Node (Json.NodeType.VALUE);
EnumClass enumc = (EnumClass) value.type ().class_ref ();
unowned EnumValue ? eval
= enumc.get_value (value.get_enum ());
if (eval == null) {
node.set_value (value);
return node;
}
node.set_string (eval.value_nick);
return node;
} else if (value.type ().is_a (Type.BOOLEAN)) {
// Somehow the default serializer doesn't handle booleans :/
var node = new Json.Node (Json.NodeType.VALUE);
node.set_boolean (value.get_boolean ());
return node;
}
return default_serialize_property (property_name, value, pspec);
}
......@@ -556,6 +676,12 @@ namespace SwayNotificationCenter {
default = new OrderedHashTable<Category> ();
}
/** Filter Notification Actions */
public OrderedHashTable<ActionMatching> notification_action_filter {
get;
set;
default = new OrderedHashTable<ActionMatching> ();
}
/** Notification Status */
public OrderedHashTable<NotificationVisibility> notification_visibility {
......@@ -721,6 +847,15 @@ namespace SwayNotificationCenter {
out status);
value = result;
return status;
case "notification-action-filter":
bool status;
OrderedHashTable<ActionMatching> result =
extract_hashtable<ActionMatching> (
property_name,
property_node,
out status);
value = result;
return status;
case "notification-visibility":
bool status;
OrderedHashTable<NotificationVisibility> result =
......@@ -801,6 +936,11 @@ namespace SwayNotificationCenter {
var table = (OrderedHashTable<Category>) value;
node.set_object (serialize_hashtable<Category> (table));
break;
case "notification-action-filter":
node = new Json.Node (Json.NodeType.OBJECT);
var table = (OrderedHashTable<ActionMatching>) value;
node.set_object (serialize_hashtable<ActionMatching> (table));
break;
case "notification-visibility":
node = new Json.Node (Json.NodeType.OBJECT);
var table = (OrderedHashTable<NotificationVisibility>) value;
......
......@@ -275,6 +275,47 @@
}
}
},
"notification-action-filter": {
"type": "object",
"description": "Hides matching action(s) of matching notifications. If the notification doesn't include one of the properties, that property will be ignored. If all properties match the given notification, the matching actions will be hidden.",
"minProperties": 1,
"additionalProperties": false,
"patternProperties": {
"^.{1,}$": {
"type": "object",
"description": "Your script object.",
"minProperties": 1,
"anyOf": [
{ "required": ["id-matcher"] },
{ "required": ["text-matcher"] }
],
"additionalProperties": false,
"properties": {
"use-regex": {
"type": "boolean",
"default": false,
"description": "Indicates if all the below fields should use regex or not."
},
"app-name": {
"type": "string",
"description": "The app-name."
},
"desktop-entry": {
"type": "string",
"description": "The desktop-entry."
},
"id-matcher": {
"type": "string",
"description": "Matches the action identifier. Can be found by reading the output of swaync when run with the `G_MESSAGES_DEBUG=all` environment variable."
},
"text-matcher": {
"type": "string",
"description": "Matches the actions text visible in the notification."
}
}
}
}
},
"notification-visibility": {
"type": "object",
"description": "Set the visibility of each incoming notification. If the notification doesn't include one of the properties, that property will be ignored. All properties (except for state) use regex. If all properties match the given notification, the notification will be follow the provided state. Only the first matching object will be used.",
......
......@@ -54,12 +54,16 @@ namespace SwayNotificationCenter {
}
public class Action : Object {
public string identifier { get; set; }
public string name { get; set; }
public string identifier { get; construct set; }
public string text { get; construct set; }
public Action (string identifier, string text) {
Object (identifier: identifier, text: text);
}
public string to_string () {
if (identifier == null || name == null) return "None";
return "Name: %s, Id: %s".printf (name, identifier);
if (identifier == null || text == null) return "None";
return "Name: %s, Id: %s".printf (text, identifier);
}
}
......@@ -334,31 +338,56 @@ namespace SwayNotificationCenter {
private void parse_actions (string[] actions) {
Array<Action> parsed_actions = new Array<Action> ();
if (actions.length > 1 && actions.length % 2 == 0) {
for (int i = 0; i < actions.length; i++) {
var action = new Action ();
action.identifier = actions[i];
action.name = actions[i + 1];
if (action.name != null && action.identifier != null) {
string id = action.identifier.down ();
switch (id) {
case "default":
default_action = action;
break;
case "inline-reply":
if (action.name == "") {
action.name = "Reply";
}
inline_reply = action;
break;
default:
parsed_actions.append_val (action);
break;
}
if (actions.length <= 1 || actions.length % 2 != 0) {
this.actions = parsed_actions;
return;
}
// Find all the matching filters
List<unowned ActionMatching> valid_matchers = new List<unowned ActionMatching> ();
unowned OrderedHashTable<ActionMatching> filters =
ConfigModel.instance.notification_action_filter;
foreach (string key in filters.get_keys ()) {
unowned ActionMatching matcher = filters[key];
if (matcher.matches_notification (this)) {
valid_matchers.append (matcher);
}
}
for (int i = 0; i < actions.length; i += 2) {
Action action = new Action (actions[i], actions[i + 1]);
// Filtering
bool matches = false;
foreach (unowned ActionMatching matcher in valid_matchers) {
if (matcher.matches_action (action)) {
matches = true;
break;
}
}
if (matches) {
continue;
}
if (action.text != null && action.identifier != null) {
string id = action.identifier.down ();
switch (id) {
case "default":
default_action = action;
break;
case "inline-reply":
if (action.text == "") {
action.text = "Reply";
}
inline_reply = action;
break;
default:
parsed_actions.append_val (action);
break;
}
i++;
}
}
this.actions = parsed_actions;
}
......
......@@ -566,7 +566,7 @@ namespace SwayNotificationCenter {
},
null);
inline_reply_button.set_label (param.inline_reply.name ?? "Reply");
inline_reply_button.set_label (param.inline_reply.text ?? "Reply");
}
private void set_actions () {
......@@ -613,7 +613,7 @@ namespace SwayNotificationCenter {
flowbox_child.add_css_class ("notification-action");
alt_actions_box.append (flowbox_child);
Gtk.Button action_button = new Gtk.Button.with_label (action.name);
Gtk.Button action_button = new Gtk.Button.with_label (action.text);
action_button.clicked.connect (() => action_clicked (action));
action_button.set_can_focus (false);
flowbox_child.set_child (action_button);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment