1 /*
2  * vala-panel
3  * Copyright (C) 2018 Konstantin Pugin <ria.freelander@gmail.com>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU Lesser General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "matcher.h"
20 
21 struct _ValaPanelMatcher
22 {
23 	GObject parent_instance;
24 	GHashTable *startupids;
25 	GHashTable *simpletons;
26 	GHashTable *desktops;
27 	GHashTable *exec_cache;
28 	GHashTable *pid_cache;
29 	GAppInfoMonitor *monitor;
30 	bool invalidated;
31 	GRecMutex __lock_invalidated;
32 	GDBusConnection *bus;
33 };
34 
35 static uint app_changed_singal;
36 
37 G_DEFINE_TYPE(ValaPanelMatcher, vala_panel_matcher, G_TYPE_OBJECT)
38 
39 static ValaPanelMatcher *default_matcher = NULL;
40 
vala_panel_matcher_finalize(GObject * obj)41 static void vala_panel_matcher_finalize(GObject *obj)
42 {
43 	ValaPanelMatcher *self = VALA_PANEL_MATCHER(obj);
44 	g_clear_pointer(&self->startupids, g_hash_table_unref);
45 	g_clear_pointer(&self->simpletons, g_hash_table_unref);
46 	g_clear_pointer(&self->desktops, g_hash_table_unref);
47 	g_clear_pointer(&self->exec_cache, g_hash_table_unref);
48 	g_clear_pointer(&self->pid_cache, g_hash_table_unref);
49 	g_rec_mutex_clear(&self->__lock_invalidated);
50 	g_clear_object(&self->bus);
51 	g_clear_object(&self->monitor);
52 	G_OBJECT_CLASS(vala_panel_matcher_parent_class)->finalize(obj);
53 }
54 
create_simpletons(ValaPanelMatcher * self)55 static void create_simpletons(ValaPanelMatcher *self)
56 {
57 	g_hash_table_insert(self->simpletons,
58 	                    g_strdup("google-chrome-stable"),
59 	                    g_strdup("google-chrome"));
60 	g_hash_table_insert(self->simpletons, g_strdup("calibre-gui"), g_strdup("calibre"));
61 	g_hash_table_insert(self->simpletons, g_strdup("code - oss"), g_strdup("vscode-oss"));
62 	g_hash_table_insert(self->simpletons, g_strdup("code"), g_strdup("vscode"));
63 	g_hash_table_insert(self->simpletons, g_strdup("psppire"), g_strdup("pspp"));
64 	g_hash_table_insert(self->simpletons,
65 	                    g_strdup("gnome-twitch"),
66 	                    g_strdup("com.vinszent.gnometwitch"));
67 	g_hash_table_insert(self->simpletons, g_strdup("anoise.py"), g_strdup("anoise"));
68 }
69 
vala_panel_matcher_init(ValaPanelMatcher * self)70 static void vala_panel_matcher_init(ValaPanelMatcher *self)
71 {
72 	self->simpletons = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
73 	create_simpletons(self);
74 	self->pid_cache  = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free);
75 	self->desktops   = NULL;
76 	self->exec_cache = NULL;
77 	self->startupids = NULL;
78 	self->monitor    = g_app_info_monitor_get();
79 	g_rec_mutex_init(&self->__lock_invalidated);
80 	self->invalidated = false;
81 }
82 
matcher_reload_ids(ValaPanelMatcher * self)83 static void matcher_reload_ids(ValaPanelMatcher *self)
84 {
85 	g_clear_pointer(&self->startupids, g_hash_table_unref);
86 	g_clear_pointer(&self->desktops, g_hash_table_unref);
87 	g_clear_pointer(&self->exec_cache, g_hash_table_unref);
88 	self->startupids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
89 	self->desktops   = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_object_unref);
90 	self->exec_cache = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
91 	GList *app_info_list = g_app_info_get_all();
92 	for (GList *l = app_info_list; l != NULL; l = g_list_next(l))
93 	{
94 		GDesktopAppInfo *dinfo = G_DESKTOP_APP_INFO(l->data);
95 		const char *id         = g_app_info_get_id(G_APP_INFO(dinfo));
96 		if (g_desktop_app_info_get_startup_wm_class(dinfo) != NULL)
97 		{
98 			char *down_index =
99 			    g_utf8_strdown(g_desktop_app_info_get_startup_wm_class(dinfo), -1);
100 			g_hash_table_insert(self->startupids, down_index, g_strdup(id));
101 		}
102 		char *down_index = g_utf8_strdown(id, -1);
103 		g_hash_table_insert(self->desktops, down_index, dinfo);
104 
105 		/* Get TryExec if we can, otherwise just Exec */
106 		char *try_exec = g_desktop_app_info_get_string(dinfo, "TryExec");
107 		if (try_exec == NULL)
108 		{
109 			const char *exec = g_app_info_get_executable(G_APP_INFO(dinfo));
110 			try_exec         = exec ? g_strdup(exec) : NULL;
111 		}
112 		if (try_exec == NULL)
113 			continue;
114 		/* Sanitize it */
115 		char *exec = g_uri_unescape_string(try_exec, NULL);
116 		g_clear_pointer(&try_exec, g_free);
117 		try_exec = g_path_get_basename(exec);
118 		g_clear_pointer(&exec, g_free);
119 		g_hash_table_insert(self->exec_cache, try_exec, g_strdup(id));
120 	}
121 	g_list_free(app_info_list);
122 }
123 
matcher_bus_signal_subscribe(GDBusConnection * connection,const gchar * sender_name,const gchar * object_path,const gchar * interface_name,const gchar * signal_name,GVariant * parameters,gpointer user_data)124 static void matcher_bus_signal_subscribe(GDBusConnection *connection, const gchar *sender_name,
125                                          const gchar *object_path, const gchar *interface_name,
126                                          const gchar *signal_name, GVariant *parameters,
127                                          gpointer user_data)
128 {
129 	ValaPanelMatcher *self              = VALA_PANEL_MATCHER(user_data);
130 	g_autoptr(GVariant) desktop_variant = NULL;
131 	int64_t pid                         = 0;
132 	g_variant_get(parameters,
133 	              "(@aysxas@a{sv})",
134 	              &desktop_variant,
135 	              NULL,
136 	              &pid,
137 	              NULL,
138 	              NULL,
139 	              NULL);
140 	const char *desktop_file = g_variant_get_bytestring(desktop_variant);
141 	if (!g_strcmp0(desktop_file, "") || !pid)
142 		return;
143 
144 	g_hash_table_insert(self->pid_cache, GINT_TO_POINTER(pid), g_strdup(desktop_file));
145 	g_signal_emit(self, app_changed_singal, 0, desktop_file);
146 }
147 
matcher_bus_get_finish(GObject * source_object,GAsyncResult * res,gpointer user_data)148 static void matcher_bus_get_finish(GObject *source_object, GAsyncResult *res, gpointer user_data)
149 {
150 	g_autoptr(GError) err  = NULL;
151 	ValaPanelMatcher *self = VALA_PANEL_MATCHER(user_data);
152 	self->bus              = g_bus_get_finish(res, &err);
153 	if (err)
154 	{
155 		g_warning("%s\n", err->message);
156 		return;
157 	}
158 	g_dbus_connection_signal_subscribe(self->bus,
159 	                                   NULL,
160 	                                   "org.gtk.gio.DesktopAppInfo",
161 	                                   "Launched",
162 	                                   "/org/gtk/gio/DesktopAppInfo",
163 	                                   NULL,
164 	                                   (GDBusSignalFlags)0,
165 	                                   matcher_bus_signal_subscribe,
166 	                                   self,
167 	                                   NULL);
168 }
169 
invalidate_ids(void * data)170 static bool invalidate_ids(void *data)
171 {
172 	ValaPanelMatcher *self = VALA_PANEL_MATCHER(data);
173 	g_rec_mutex_lock(&self->__lock_invalidated);
174 	self->invalidated = true;
175 	g_rec_mutex_unlock(&self->__lock_invalidated);
176 	return false;
177 }
178 
on_monitor_changed(GAppInfoMonitor * gappinfomonitor,gpointer user_data)179 static void on_monitor_changed(GAppInfoMonitor *gappinfomonitor, gpointer user_data)
180 {
181 	g_idle_add((GSourceFunc)invalidate_ids, user_data);
182 }
183 
vala_panel_matcher_constructor(GType type,guint n_construct_properties,GObjectConstructParam * construct_properties)184 static GObject *vala_panel_matcher_constructor(GType type, guint n_construct_properties,
185                                                GObjectConstructParam *construct_properties)
186 {
187 	GObjectClass *parent_class = G_OBJECT_CLASS(vala_panel_matcher_parent_class);
188 	GObject *obj =
189 	    parent_class->constructor(type, n_construct_properties, construct_properties);
190 	ValaPanelMatcher *self = VALA_PANEL_MATCHER(obj);
191 	g_bus_get(G_BUS_TYPE_SESSION, NULL, matcher_bus_get_finish, self);
192 	self->monitor = g_app_info_monitor_get();
193 	g_signal_connect(self->monitor, "changed", G_CALLBACK(on_monitor_changed), self);
194 	matcher_reload_ids(self);
195 	return obj;
196 }
197 
matcher_check_invalidated(ValaPanelMatcher * self)198 static void matcher_check_invalidated(ValaPanelMatcher *self)
199 {
200 	if (self->invalidated)
201 	{
202 		g_rec_mutex_lock(&self->__lock_invalidated);
203 		matcher_reload_ids(self);
204 		self->invalidated = false;
205 		g_rec_mutex_unlock(&self->__lock_invalidated);
206 	}
207 }
208 
vala_panel_matcher_get()209 ValaPanelMatcher *vala_panel_matcher_get()
210 {
211 	if (VALA_PANEL_IS_MATCHER(default_matcher))
212 		return g_object_ref(default_matcher);
213 
214 	return (default_matcher = g_object_new(vala_panel_matcher_get_type(), NULL));
215 }
216 
vala_panel_matcher_match_arbitrary(ValaPanelMatcher * self,const char * class,const char * group,const char * gtk,int64_t pid)217 GDesktopAppInfo *vala_panel_matcher_match_arbitrary(ValaPanelMatcher *self, const char *class,
218                                                     const char *group, const char *gtk, int64_t pid)
219 {
220 	matcher_check_invalidated(self);
221 	const char *checks[] = { class, group };
222 	for (int i = 0; i < 2; i++)
223 	{
224 		if (!checks[i])
225 			continue;
226 
227 		/* First, check startupids for this app */
228 		g_autofree char *check = g_utf8_strdown(checks[i], -1);
229 		if (g_hash_table_contains(self->startupids, check))
230 		{
231 			g_autofree char *dname =
232 			    g_utf8_strdown((const char *)g_hash_table_lookup(self->startupids,
233 			                                                     check),
234 			                   -1);
235 			if (g_hash_table_contains(self->desktops, dname))
236 				return G_DESKTOP_APP_INFO(
237 				    g_hash_table_lookup(self->desktops, dname));
238 		}
239 		/* Then try class -> desktop match */
240 		g_autofree char *dname = g_strdup_printf("%s.desktop", check);
241 		if (g_hash_table_contains(self->desktops, dname))
242 			return G_DESKTOP_APP_INFO(g_hash_table_lookup(self->desktops, dname));
243 	}
244 
245 	/* If no classes matched, try PID cache */
246 	if (g_hash_table_contains(self->pid_cache, GINT_TO_POINTER(pid)))
247 	{
248 		const char *filename =
249 		    (const char *)g_hash_table_lookup(self->pid_cache, GINT_TO_POINTER(pid));
250 		return g_desktop_app_info_new_from_filename(filename);
251 	}
252 
253 	/* Next, check GtkApplication ID */
254 	if (gtk != NULL)
255 	{
256 		g_autofree char *app_id = g_utf8_strdown(gtk, -1);
257 		g_autofree char *gtk_id = g_strdup_printf("%s.desktop", app_id);
258 		if (g_hash_table_contains(self->desktops, gtk_id))
259 			return G_DESKTOP_APP_INFO(g_hash_table_lookup(self->desktops, gtk_id));
260 	}
261 
262 	/* Check hardcoded matches */
263 	if (group)
264 	{
265 		g_autofree char *grp = g_utf8_strdown(group, -1);
266 		if (g_hash_table_contains(self->simpletons, grp))
267 		{
268 			g_autofree char *dname = g_strdup_printf("%s.desktop", grp);
269 			if (g_hash_table_contains(self->desktops, dname))
270 				return G_DESKTOP_APP_INFO(
271 				    g_hash_table_lookup(self->desktops, dname));
272 		}
273 	}
274 	if (class)
275 	{
276 		g_autofree char *grp = g_utf8_strdown(class, -1);
277 		if (g_hash_table_contains(self->simpletons, grp))
278 		{
279 			g_autofree char *dname = g_strdup_printf("%s.desktop", grp);
280 			if (g_hash_table_contains(self->desktops, dname))
281 				return G_DESKTOP_APP_INFO(
282 				    g_hash_table_lookup(self->desktops, dname));
283 		}
284 	}
285 
286 	/* Lastly, try to match an exec line */
287 	for (int i = 0; i < 2; i++)
288 	{
289 		if (!checks[i])
290 			continue;
291 
292 		g_autofree char *check = g_utf8_strdown(checks[i], -1);
293 		const char *id         = (const char *)g_hash_table_lookup(self->exec_cache, check);
294 		if (id == NULL)
295 			continue;
296 		GDesktopAppInfo *a = G_DESKTOP_APP_INFO(g_hash_table_lookup(self->desktops, id));
297 		if (a != NULL)
298 			return a;
299 	}
300 
301 	/* IDK. Sorry. */
302 	return NULL;
303 }
304 
vala_panel_matcher_class_init(ValaPanelMatcherClass * klass)305 static void vala_panel_matcher_class_init(ValaPanelMatcherClass *klass)
306 {
307 	vala_panel_matcher_parent_class    = g_type_class_peek_parent(klass);
308 	G_OBJECT_CLASS(klass)->constructor = vala_panel_matcher_constructor;
309 	G_OBJECT_CLASS(klass)->finalize    = vala_panel_matcher_finalize;
310 	app_changed_singal                 = g_signal_new("app-launched",
311                                           vala_panel_matcher_get_type(),
312                                           G_SIGNAL_RUN_LAST,
313                                           0,
314                                           NULL,
315                                           NULL,
316                                           g_cclosure_marshal_VOID__STRING,
317                                           G_TYPE_NONE,
318                                           1,
319                                           G_TYPE_STRING);
320 }
321