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