1 /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
2
3 #include "config.h"
4
5 #include "shell-app-system.h"
6 #include "shell-app-usage.h"
7 #include <string.h>
8
9 #include <gio/gio.h>
10 #include <glib/gi18n.h>
11
12 #include "shell-app-cache-private.h"
13 #include "shell-app-private.h"
14 #include "shell-window-tracker-private.h"
15 #include "shell-app-system-private.h"
16 #include "shell-global.h"
17 #include "shell-util.h"
18 #include "st.h"
19
20 /* Rescan for at most RESCAN_TIMEOUT_MS * MAX_RESCAN_RETRIES. That
21 * should be plenty of time for even a slow spinning drive to update
22 * the icon cache.
23 */
24 #define RESCAN_TIMEOUT_MS 2500
25 #define MAX_RESCAN_RETRIES 6
26
27 /* Vendor prefixes are something that can be preprended to a .desktop
28 * file name. Undo this.
29 */
30 static const char*const vendor_prefixes[] = { "gnome-",
31 "fedora-",
32 "mozilla-",
33 "debian-",
34 NULL };
35
36 enum {
37 PROP_0,
38
39 };
40
41 enum {
42 APP_STATE_CHANGED,
43 INSTALLED_CHANGED,
44 LAST_SIGNAL
45 };
46
47 static guint signals[LAST_SIGNAL] = { 0 };
48
49 typedef struct _ShellAppSystemPrivate ShellAppSystemPrivate;
50
51 struct _ShellAppSystem
52 {
53 GObject parent;
54
55 ShellAppSystemPrivate *priv;
56 };
57
58 struct _ShellAppSystemPrivate {
59 GHashTable *running_apps;
60 GHashTable *id_to_app;
61 GHashTable *startup_wm_class_to_id;
62 GList *installed_apps;
63
64 guint rescan_icons_timeout_id;
65 guint n_rescan_retries;
66 };
67
68 static void shell_app_system_finalize (GObject *object);
69
70 G_DEFINE_TYPE_WITH_PRIVATE (ShellAppSystem, shell_app_system, G_TYPE_OBJECT);
71
shell_app_system_class_init(ShellAppSystemClass * klass)72 static void shell_app_system_class_init(ShellAppSystemClass *klass)
73 {
74 GObjectClass *gobject_class = (GObjectClass *)klass;
75
76 gobject_class->finalize = shell_app_system_finalize;
77
78 signals[APP_STATE_CHANGED] = g_signal_new ("app-state-changed",
79 SHELL_TYPE_APP_SYSTEM,
80 G_SIGNAL_RUN_LAST,
81 0,
82 NULL, NULL, NULL,
83 G_TYPE_NONE, 1,
84 SHELL_TYPE_APP);
85 signals[INSTALLED_CHANGED] =
86 g_signal_new ("installed-changed",
87 SHELL_TYPE_APP_SYSTEM,
88 G_SIGNAL_RUN_LAST,
89 0,
90 NULL, NULL, NULL,
91 G_TYPE_NONE, 0);
92 }
93
94 static void
scan_startup_wm_class_to_id(ShellAppSystem * self)95 scan_startup_wm_class_to_id (ShellAppSystem *self)
96 {
97 ShellAppSystemPrivate *priv = self->priv;
98 const GList *l;
99 GList *all;
100
101 g_hash_table_remove_all (priv->startup_wm_class_to_id);
102
103 all = shell_app_cache_get_all (shell_app_cache_get_default ());
104
105 for (l = all; l != NULL; l = l->next)
106 {
107 GAppInfo *info = l->data;
108 const char *startup_wm_class, *id, *old_id;
109
110 id = g_app_info_get_id (info);
111 startup_wm_class = g_desktop_app_info_get_startup_wm_class (G_DESKTOP_APP_INFO (info));
112
113 if (startup_wm_class == NULL)
114 continue;
115
116 /* In case multiple .desktop files set the same StartupWMClass, prefer
117 * the one where ID and StartupWMClass match */
118 old_id = g_hash_table_lookup (priv->startup_wm_class_to_id, startup_wm_class);
119 if (old_id == NULL || strcmp (id, startup_wm_class) == 0)
120 g_hash_table_insert (priv->startup_wm_class_to_id,
121 g_strdup (startup_wm_class), g_strdup (id));
122 }
123 }
124
125 static gboolean
app_is_stale(ShellApp * app)126 app_is_stale (ShellApp *app)
127 {
128 GDesktopAppInfo *info, *old;
129 GAppInfo *old_info, *new_info;
130 gboolean is_unchanged;
131
132 if (shell_app_is_window_backed (app))
133 return FALSE;
134
135 info = shell_app_cache_get_info (shell_app_cache_get_default (),
136 shell_app_get_id (app));
137 if (!info)
138 return TRUE;
139
140 old = shell_app_get_app_info (app);
141 old_info = G_APP_INFO (old);
142 new_info = G_APP_INFO (info);
143
144 is_unchanged =
145 g_app_info_should_show (old_info) == g_app_info_should_show (new_info) &&
146 strcmp (g_desktop_app_info_get_filename (old),
147 g_desktop_app_info_get_filename (info)) == 0 &&
148 g_strcmp0 (g_app_info_get_executable (old_info),
149 g_app_info_get_executable (new_info)) == 0 &&
150 g_strcmp0 (g_app_info_get_commandline (old_info),
151 g_app_info_get_commandline (new_info)) == 0 &&
152 strcmp (g_app_info_get_name (old_info),
153 g_app_info_get_name (new_info)) == 0 &&
154 g_strcmp0 (g_app_info_get_description (old_info),
155 g_app_info_get_description (new_info)) == 0 &&
156 strcmp (g_app_info_get_display_name (old_info),
157 g_app_info_get_display_name (new_info)) == 0 &&
158 g_icon_equal (g_app_info_get_icon (old_info),
159 g_app_info_get_icon (new_info));
160
161 return !is_unchanged;
162 }
163
164 static gboolean
stale_app_remove_func(gpointer key,gpointer value,gpointer user_data)165 stale_app_remove_func (gpointer key,
166 gpointer value,
167 gpointer user_data)
168 {
169 return app_is_stale (value);
170 }
171
172 static gboolean
rescan_icon_theme_cb(gpointer user_data)173 rescan_icon_theme_cb (gpointer user_data)
174 {
175 ShellAppSystemPrivate *priv;
176 ShellAppSystem *self;
177 StTextureCache *texture_cache;
178 gboolean rescanned;
179
180 self = (ShellAppSystem *) user_data;
181 priv = self->priv;
182
183 texture_cache = st_texture_cache_get_default ();
184 rescanned = st_texture_cache_rescan_icon_theme (texture_cache);
185
186 priv->n_rescan_retries++;
187
188 if (rescanned || priv->n_rescan_retries >= MAX_RESCAN_RETRIES)
189 {
190 priv->n_rescan_retries = 0;
191 priv->rescan_icons_timeout_id = 0;
192 return G_SOURCE_REMOVE;
193 }
194
195 return G_SOURCE_CONTINUE;
196 }
197
198 static void
rescan_icon_theme(ShellAppSystem * self)199 rescan_icon_theme (ShellAppSystem *self)
200 {
201 ShellAppSystemPrivate *priv = self->priv;
202
203 priv->n_rescan_retries = 0;
204
205 if (priv->rescan_icons_timeout_id > 0)
206 return;
207
208 priv->rescan_icons_timeout_id = g_timeout_add (RESCAN_TIMEOUT_MS,
209 rescan_icon_theme_cb,
210 self);
211 }
212
213 static void
installed_changed(ShellAppCache * cache,ShellAppSystem * self)214 installed_changed (ShellAppCache *cache,
215 ShellAppSystem *self)
216 {
217 rescan_icon_theme (self);
218 scan_startup_wm_class_to_id (self);
219
220 g_hash_table_foreach_remove (self->priv->id_to_app, stale_app_remove_func, NULL);
221
222 g_signal_emit (self, signals[INSTALLED_CHANGED], 0, NULL);
223 }
224
225 static void
shell_app_system_init(ShellAppSystem * self)226 shell_app_system_init (ShellAppSystem *self)
227 {
228 ShellAppSystemPrivate *priv;
229 ShellAppCache *cache;
230
231 self->priv = priv = shell_app_system_get_instance_private (self);
232
233 priv->running_apps = g_hash_table_new_full (NULL, NULL, (GDestroyNotify) g_object_unref, NULL);
234 priv->id_to_app = g_hash_table_new_full (g_str_hash, g_str_equal,
235 NULL,
236 (GDestroyNotify)g_object_unref);
237
238 priv->startup_wm_class_to_id = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
239
240 cache = shell_app_cache_get_default ();
241 g_signal_connect (cache, "changed", G_CALLBACK (installed_changed), self);
242 installed_changed (cache, self);
243 }
244
245 static void
shell_app_system_finalize(GObject * object)246 shell_app_system_finalize (GObject *object)
247 {
248 ShellAppSystem *self = SHELL_APP_SYSTEM (object);
249 ShellAppSystemPrivate *priv = self->priv;
250
251 g_hash_table_destroy (priv->running_apps);
252 g_hash_table_destroy (priv->id_to_app);
253 g_hash_table_destroy (priv->startup_wm_class_to_id);
254 g_list_free_full (priv->installed_apps, g_object_unref);
255 g_clear_handle_id (&priv->rescan_icons_timeout_id, g_source_remove);
256
257 G_OBJECT_CLASS (shell_app_system_parent_class)->finalize (object);
258 }
259
260 /**
261 * shell_app_system_get_default:
262 *
263 * Return Value: (transfer none): The global #ShellAppSystem singleton
264 */
265 ShellAppSystem *
shell_app_system_get_default(void)266 shell_app_system_get_default (void)
267 {
268 static ShellAppSystem *instance = NULL;
269
270 if (instance == NULL)
271 instance = g_object_new (SHELL_TYPE_APP_SYSTEM, NULL);
272
273 return instance;
274 }
275
276 /**
277 * shell_app_system_lookup_app:
278 *
279 * Find a #ShellApp corresponding to an id.
280 *
281 * Return value: (transfer none): The #ShellApp for id, or %NULL if none
282 */
283 ShellApp *
shell_app_system_lookup_app(ShellAppSystem * self,const char * id)284 shell_app_system_lookup_app (ShellAppSystem *self,
285 const char *id)
286 {
287 ShellAppSystemPrivate *priv = self->priv;
288 ShellApp *app;
289 GDesktopAppInfo *info;
290
291 app = g_hash_table_lookup (priv->id_to_app, id);
292 if (app)
293 return app;
294
295 info = shell_app_cache_get_info (shell_app_cache_get_default (), id);
296 if (!info)
297 return NULL;
298
299 app = _shell_app_new (info);
300 g_hash_table_insert (priv->id_to_app, (char *) shell_app_get_id (app), app);
301 return app;
302 }
303
304 /**
305 * shell_app_system_lookup_heuristic_basename:
306 * @system: a #ShellAppSystem
307 * @id: Probable application identifier
308 *
309 * Find a valid application corresponding to a given
310 * heuristically determined application identifier
311 * string, or %NULL if none.
312 *
313 * Returns: (transfer none): A #ShellApp for @name
314 */
315 ShellApp *
shell_app_system_lookup_heuristic_basename(ShellAppSystem * system,const char * name)316 shell_app_system_lookup_heuristic_basename (ShellAppSystem *system,
317 const char *name)
318 {
319 ShellApp *result;
320 const char *const *prefix;
321
322 result = shell_app_system_lookup_app (system, name);
323 if (result != NULL)
324 return result;
325
326 for (prefix = vendor_prefixes; *prefix != NULL; prefix++)
327 {
328 char *tmpid = g_strconcat (*prefix, name, NULL);
329 result = shell_app_system_lookup_app (system, tmpid);
330 g_free (tmpid);
331 if (result != NULL)
332 return result;
333 }
334
335 return NULL;
336 }
337
338 /**
339 * shell_app_system_lookup_desktop_wmclass:
340 * @system: a #ShellAppSystem
341 * @wmclass: (nullable): A WM_CLASS value
342 *
343 * Find a valid application whose .desktop file, without the extension
344 * and properly canonicalized, matches @wmclass.
345 *
346 * Returns: (transfer none): A #ShellApp for @wmclass
347 */
348 ShellApp *
shell_app_system_lookup_desktop_wmclass(ShellAppSystem * system,const char * wmclass)349 shell_app_system_lookup_desktop_wmclass (ShellAppSystem *system,
350 const char *wmclass)
351 {
352 char *canonicalized;
353 char *desktop_file;
354 ShellApp *app;
355
356 if (wmclass == NULL)
357 return NULL;
358
359 /* First try without changing the case (this handles
360 org.example.Foo.Bar.desktop applications)
361
362 Note that is slightly wrong in that Gtk+ would set
363 the WM_CLASS to Org.example.Foo.Bar, but it also
364 sets the instance part to org.example.Foo.Bar, so we're ok
365 */
366 desktop_file = g_strconcat (wmclass, ".desktop", NULL);
367 app = shell_app_system_lookup_heuristic_basename (system, desktop_file);
368 g_free (desktop_file);
369
370 if (app)
371 return app;
372
373 canonicalized = g_ascii_strdown (wmclass, -1);
374
375 /* This handles "Fedora Eclipse", probably others.
376 * Note g_strdelimit is modify-in-place. */
377 g_strdelimit (canonicalized, " ", '-');
378
379 desktop_file = g_strconcat (canonicalized, ".desktop", NULL);
380
381 app = shell_app_system_lookup_heuristic_basename (system, desktop_file);
382
383 g_free (canonicalized);
384 g_free (desktop_file);
385
386 return app;
387 }
388
389 /**
390 * shell_app_system_lookup_startup_wmclass:
391 * @system: a #ShellAppSystem
392 * @wmclass: (nullable): A WM_CLASS value
393 *
394 * Find a valid application whose .desktop file contains a
395 * StartupWMClass entry matching @wmclass.
396 *
397 * Returns: (transfer none): A #ShellApp for @wmclass
398 */
399 ShellApp *
shell_app_system_lookup_startup_wmclass(ShellAppSystem * system,const char * wmclass)400 shell_app_system_lookup_startup_wmclass (ShellAppSystem *system,
401 const char *wmclass)
402 {
403 const char *id;
404
405 if (wmclass == NULL)
406 return NULL;
407
408 id = g_hash_table_lookup (system->priv->startup_wm_class_to_id, wmclass);
409 if (id == NULL)
410 return NULL;
411
412 return shell_app_system_lookup_app (system, id);
413 }
414
415 void
_shell_app_system_notify_app_state_changed(ShellAppSystem * self,ShellApp * app)416 _shell_app_system_notify_app_state_changed (ShellAppSystem *self,
417 ShellApp *app)
418 {
419 ShellAppState state = shell_app_get_state (app);
420
421 switch (state)
422 {
423 case SHELL_APP_STATE_RUNNING:
424 g_hash_table_insert (self->priv->running_apps, g_object_ref (app), NULL);
425 break;
426 case SHELL_APP_STATE_STARTING:
427 break;
428 case SHELL_APP_STATE_STOPPED:
429 g_hash_table_remove (self->priv->running_apps, app);
430 break;
431 default:
432 g_warn_if_reached();
433 break;
434 }
435 g_signal_emit (self, signals[APP_STATE_CHANGED], 0, app);
436 }
437
438 /**
439 * shell_app_system_get_running:
440 * @self: A #ShellAppSystem
441 *
442 * Returns the set of applications which currently have at least one
443 * open window. The returned list will be sorted by shell_app_compare().
444 *
445 * Returns: (element-type ShellApp) (transfer container): Active applications
446 */
447 GSList *
shell_app_system_get_running(ShellAppSystem * self)448 shell_app_system_get_running (ShellAppSystem *self)
449 {
450 gpointer key, value;
451 GSList *ret;
452 GHashTableIter iter;
453
454 g_hash_table_iter_init (&iter, self->priv->running_apps);
455
456 ret = NULL;
457 while (g_hash_table_iter_next (&iter, &key, &value))
458 {
459 ShellApp *app = key;
460
461 ret = g_slist_prepend (ret, app);
462 }
463
464 ret = g_slist_sort (ret, (GCompareFunc)shell_app_compare);
465
466 return ret;
467 }
468
469 /**
470 * shell_app_system_search:
471 * @search_string: the search string to use
472 *
473 * Wrapper around g_desktop_app_info_search() that replaces results that
474 * don't validate as UTF-8 with the empty string.
475 *
476 * Returns: (array zero-terminated=1) (element-type GStrv) (transfer full): a
477 * list of strvs. Free each item with g_strfreev() and free the outer
478 * list with g_free().
479 */
480 char ***
shell_app_system_search(const char * search_string)481 shell_app_system_search (const char *search_string)
482 {
483 char ***results = g_desktop_app_info_search (search_string);
484 char ***groups, **ids;
485
486 for (groups = results; *groups; groups++)
487 for (ids = *groups; *ids; ids++)
488 if (!g_utf8_validate (*ids, -1, NULL))
489 **ids = '\0';
490
491 return results;
492 }
493
494 /**
495 * shell_app_system_get_installed:
496 * @self: the #ShellAppSystem
497 *
498 * Returns all installed apps, as a list of #GAppInfo
499 *
500 * Returns: (transfer none) (element-type GAppInfo): a list of #GAppInfo
501 * describing all known applications. This memory is owned by the
502 * #ShellAppSystem and should not be freed.
503 **/
504 GList *
shell_app_system_get_installed(ShellAppSystem * self)505 shell_app_system_get_installed (ShellAppSystem *self)
506 {
507 return shell_app_cache_get_all (shell_app_cache_get_default ());
508 }
509