29 #define G_LOG_DOMAIN "Dialogs.DRun"
40 #include <sys/types.h>
58 #define DRUN_CACHE_FILE "rofi3.druncache"
60 char *DRUN_GROUP_NAME =
"Desktop Entry";
62 typedef struct _DRunModePrivateData DRunModePrivateData;
70 DRunModePrivateData *pd;
88 cairo_surface_t *
icon;
104 uint32_t icon_fetch_uid;
109 const char *entry_field_name;
115 DRUN_MATCH_FIELD_NAME,
116 DRUN_MATCH_FIELD_GENERIC,
117 DRUN_MATCH_FIELD_EXEC,
118 DRUN_MATCH_FIELD_CATEGORIES,
119 DRUN_MATCH_FIELD_COMMENT,
120 DRUN_MATCH_NUM_FIELDS,
121 } DRunMatchingFields;
123 static DRunEntryField matching_entry_fields[DRUN_MATCH_NUM_FIELDS] = {
124 { .entry_field_name =
"name", .enabled = TRUE, },
125 { .entry_field_name =
"generic", .enabled = TRUE, },
126 { .entry_field_name =
"exec", .enabled = TRUE, },
127 { .entry_field_name =
"categories", .enabled = TRUE, },
128 { .entry_field_name =
"comment", .enabled = FALSE, }
131 struct _DRunModePrivateData
133 DRunModeEntry *entry_list;
134 unsigned int cmd_list_length;
135 unsigned int cmd_list_length_actual;
137 GHashTable *disabled_entries;
138 unsigned int disabled_entries_length;
139 unsigned int expected_line_height;
142 const gchar *icon_theme;
144 gchar **current_desktop_list;
153 static gboolean drun_helper_eval_cb (
const GMatchInfo *info, GString *res, gpointer data )
156 struct RegexEvalArg *e = (
struct RegexEvalArg *) data;
160 match = g_match_info_fetch ( info, 0 );
161 if ( match != NULL ) {
180 char *esc = g_shell_quote ( e->e->path );
181 g_string_append ( res, esc );
187 char *esc = g_shell_quote ( e->e->name );
188 g_string_append ( res, esc );
203 static void exec_cmd_entry ( DRunModeEntry *e )
205 GError *error = NULL;
206 GRegex *reg = g_regex_new (
"%[a-zA-Z]", 0, 0, &error );
207 if ( error != NULL ) {
208 g_warning (
"Internal error, failed to create regex: %s.", error->message );
209 g_error_free ( error );
212 struct RegexEvalArg earg = { .e = e, .success = TRUE };
213 char *str = g_regex_replace_eval ( reg, e->exec, -1, 0, 0, drun_helper_eval_cb, &earg, &error );
214 if ( error != NULL ) {
215 g_warning (
"Internal error, failed replace field codes: %s.", error->message );
216 g_error_free ( error );
219 g_regex_unref ( reg );
220 if ( earg.success == FALSE ) {
221 g_warning (
"Invalid field code in Exec line: %s.", e->exec );;
225 g_warning (
"Nothing to execute after processing: %s.", e->exec );;
229 const gchar *fp = g_strstrip ( str );
230 gchar *exec_path = g_key_file_get_string ( e->key_file, e->action,
"Path", NULL );
231 if ( exec_path != NULL && strlen ( exec_path ) == 0 ) {
233 g_free ( exec_path );
239 .icon = e->icon_name,
242 gboolean sn = g_key_file_get_boolean ( e->key_file, e->action,
"StartupNotify", NULL );
243 gchar *wmclass = NULL;
244 if ( sn && g_key_file_has_key ( e->key_file, e->action,
"StartupWMClass", NULL ) ) {
245 context.
wmclass = wmclass = g_key_file_get_string ( e->key_file, e->action,
"StartupWMClass", NULL );
249 gboolean terminal = g_key_file_get_boolean ( e->key_file, e->action,
"Terminal", NULL );
251 char *path = g_build_filename (
cache_dir, DRUN_CACHE_FILE, NULL );
257 g_free ( exec_path );
263 static void read_desktop_file ( DRunModePrivateData *pd,
const char *root,
const char *path,
const gchar *basename,
const char *action )
268 const ssize_t id_len = strlen ( path ) - strlen ( root );
270 g_strlcpy (
id, &( path[strlen ( root ) + 1] ), id_len );
271 for (
int index = 0; index < id_len; index++ ) {
272 if (
id[index] ==
'/' ) {
278 if ( g_hash_table_contains ( pd->disabled_entries,
id ) && !parse_action ) {
279 g_debug (
"[%s] [%s] Skipping, was previously seen.",
id, path );
282 GKeyFile *kf = g_key_file_new ();
283 GError *error = NULL;
284 gboolean res = g_key_file_load_from_file ( kf, path, 0, &error );
287 g_debug (
"[%s] [%s] Failed to parse desktop file because: %s.",
id, path, error->message );
288 g_error_free ( error );
289 g_key_file_free ( kf );
293 if ( g_key_file_has_group ( kf, action ) == FALSE ) {
295 g_debug (
"[%s] [%s] Invalid desktop file: No %s group",
id, path, action );
296 g_key_file_free ( kf );
300 gchar *key = g_key_file_get_string ( kf, DRUN_GROUP_NAME,
"Type", NULL );
303 g_debug (
"[%s] [%s] Invalid desktop file: No type indicated",
id, path );
304 g_key_file_free ( kf );
307 if ( g_strcmp0 ( key,
"Application" ) ) {
308 g_debug (
"[%s] [%s] Skipping desktop file: Not of type application (%s)",
id, path, key );
310 g_key_file_free ( kf );
316 if ( !g_key_file_has_key ( kf, DRUN_GROUP_NAME,
"Name", NULL ) ) {
317 g_debug (
"[%s] [%s] Invalid desktop file: no 'Name' key present.",
id, path );
318 g_key_file_free ( kf );
323 if ( g_key_file_get_boolean ( kf, DRUN_GROUP_NAME,
"Hidden", NULL ) ) {
324 g_debug (
"[%s] [%s] Adding desktop file to disabled list: 'Hidden' key is true",
id, path );
325 g_key_file_free ( kf );
326 g_hash_table_add ( pd->disabled_entries, g_strdup (
id ) );
329 if ( pd->current_desktop_list ) {
330 gboolean show = TRUE;
332 if ( g_key_file_has_key ( kf, DRUN_GROUP_NAME,
"OnlyShowIn", NULL ) ) {
335 gchar **list = g_key_file_get_string_list ( kf, DRUN_GROUP_NAME,
"OnlyShowIn", &llength, NULL );
337 for ( gsize lcd = 0; !show && pd->current_desktop_list[lcd]; lcd++ ) {
338 for ( gsize lle = 0; !show && lle < llength; lle++ ) {
339 show = ( g_strcmp0 ( pd->current_desktop_list[lcd], list[lle] ) == 0 );
345 if ( show && g_key_file_has_key ( kf, DRUN_GROUP_NAME,
"NotShowIn", NULL ) ) {
347 gchar **list = g_key_file_get_string_list ( kf, DRUN_GROUP_NAME,
"NotShowIn", &llength, NULL );
349 for ( gsize lcd = 0; show && pd->current_desktop_list[lcd]; lcd++ ) {
350 for ( gsize lle = 0; show && lle < llength; lle++ ) {
351 show = !( g_strcmp0 ( pd->current_desktop_list[lcd], list[lle] ) == 0 );
359 g_debug (
"[%s] [%s] Adding desktop file to disabled list: 'OnlyShowIn'/'NotShowIn' keys don't match current desktop",
id, path );
360 g_key_file_free ( kf );
361 g_hash_table_add ( pd->disabled_entries, g_strdup (
id ) );
366 if ( g_key_file_get_boolean ( kf, DRUN_GROUP_NAME,
"NoDisplay", NULL ) ) {
367 g_debug (
"[%s] [%s] Adding desktop file to disabled list: 'NoDisplay' key is true",
id, path );
368 g_key_file_free ( kf );
369 g_hash_table_add ( pd->disabled_entries, g_strdup (
id ) );
373 if ( !g_key_file_has_key ( kf, DRUN_GROUP_NAME,
"Exec", NULL ) ) {
374 g_debug (
"[%s] [%s] Unsupported desktop file: no 'Exec' key present.",
id, path );
375 g_key_file_free ( kf );
379 if ( g_key_file_has_key ( kf, DRUN_GROUP_NAME,
"TryExec", NULL ) ) {
380 char *te = g_key_file_get_string ( kf, DRUN_GROUP_NAME,
"TryExec", NULL );
381 if ( !g_path_is_absolute ( te ) ) {
382 char *fp = g_find_program_in_path ( te );
385 g_key_file_free ( kf );
391 if ( g_file_test ( te, G_FILE_TEST_IS_EXECUTABLE ) == FALSE ) {
393 g_key_file_free ( kf );
400 size_t nl = ( ( pd->cmd_list_length ) + 1 );
401 if ( nl >= pd->cmd_list_length_actual ) {
402 pd->cmd_list_length_actual += 256;
403 pd->entry_list = g_realloc ( pd->entry_list, pd->cmd_list_length_actual * sizeof ( *( pd->entry_list ) ) );
407 if ( G_UNLIKELY ( pd->cmd_list_length > INT_MAX ) ) {
409 pd->entry_list[pd->cmd_list_length].sort_index = INT_MIN;
412 pd->entry_list[pd->cmd_list_length].sort_index = -nl;
414 pd->entry_list[pd->cmd_list_length].icon_size = 0;
415 pd->entry_list[pd->cmd_list_length].icon_fetch_uid = 0;
416 pd->entry_list[pd->cmd_list_length].root = g_strdup ( root );
417 pd->entry_list[pd->cmd_list_length].path = g_strdup ( path );
418 pd->entry_list[pd->cmd_list_length].desktop_id = g_strdup (
id );
419 pd->entry_list[pd->cmd_list_length].app_id = g_strndup ( basename, strlen ( basename ) - strlen (
".desktop" ) );
420 gchar *n = g_key_file_get_locale_string ( kf, DRUN_GROUP_NAME,
"Name", NULL, NULL );
422 if ( action != DRUN_GROUP_NAME ) {
423 gchar *na = g_key_file_get_locale_string ( kf, action,
"Name", NULL, NULL );
424 gchar *l = g_strdup_printf (
"%s - %s", n, na );
428 pd->entry_list[pd->cmd_list_length].name = n;
429 pd->entry_list[pd->cmd_list_length].action = DRUN_GROUP_NAME;
430 gchar *gn = g_key_file_get_locale_string ( kf, DRUN_GROUP_NAME,
"GenericName", NULL, NULL );
431 pd->entry_list[pd->cmd_list_length].generic_name = gn;
432 if ( matching_entry_fields[DRUN_MATCH_FIELD_CATEGORIES].enabled ) {
433 pd->entry_list[pd->cmd_list_length].categories = g_key_file_get_locale_string_list ( kf, DRUN_GROUP_NAME,
"Categories", NULL, NULL, NULL );
436 pd->entry_list[pd->cmd_list_length].categories = NULL;
438 pd->entry_list[pd->cmd_list_length].exec = g_key_file_get_string ( kf, action,
"Exec", NULL );
440 if ( matching_entry_fields[DRUN_MATCH_FIELD_COMMENT].enabled ) {
441 pd->entry_list[pd->cmd_list_length].comment = g_key_file_get_locale_string ( kf,
442 DRUN_GROUP_NAME,
"Comment", NULL, NULL );
445 pd->entry_list[pd->cmd_list_length].comment = NULL;
448 pd->entry_list[pd->cmd_list_length].icon_name = g_key_file_get_locale_string ( kf, DRUN_GROUP_NAME,
"Icon", NULL, NULL );
451 pd->entry_list[pd->cmd_list_length].icon_name = NULL;
453 pd->entry_list[pd->cmd_list_length].icon = NULL;
456 pd->entry_list[pd->cmd_list_length].key_file = kf;
458 g_hash_table_add ( pd->disabled_entries, g_strdup (
id ) );
459 g_debug (
"[%s] Using file %s.",
id, path );
460 ( pd->cmd_list_length )++;
462 if ( !parse_action ) {
463 gsize actions_length = 0;
464 char **actions = g_key_file_get_string_list ( kf, DRUN_GROUP_NAME,
"Actions", &actions_length, NULL );
465 for ( gsize iter = 0; iter < actions_length; iter++ ) {
466 char *new_action = g_strdup_printf (
"Desktop Action %s", actions[iter] );
467 read_desktop_file ( pd, root, path, basename, new_action );
468 g_free ( new_action );
470 g_strfreev ( actions );
478 static void walk_dir ( DRunModePrivateData *pd,
const char *root,
const char *dirname )
482 g_debug (
"Checking directory %s for desktop files.", dirname );
483 dir = opendir ( dirname );
489 gchar *filename = NULL;
491 while ( ( file = readdir ( dir ) ) != NULL ) {
492 if ( file->d_name[0] ==
'.' ) {
495 switch ( file->d_type )
501 filename = g_build_filename ( dirname, file->d_name, NULL );
509 if ( file->d_type == DT_LNK || file->d_type == DT_UNKNOWN ) {
510 file->d_type = DT_UNKNOWN;
511 if ( stat ( filename, &st ) == 0 ) {
512 if ( S_ISDIR ( st.st_mode ) ) {
513 file->d_type = DT_DIR;
515 else if ( S_ISREG ( st.st_mode ) ) {
516 file->d_type = DT_REG;
521 switch ( file->d_type )
525 if ( g_str_has_suffix ( file->d_name,
".desktop" ) ) {
526 read_desktop_file ( pd, root, filename, file->d_name, DRUN_GROUP_NAME );
530 walk_dir ( pd, root, filename );
544 static void delete_entry_history (
const DRunModeEntry *entry )
546 char *path = g_build_filename (
cache_dir, DRUN_CACHE_FILE, NULL );
551 static void get_apps_history ( DRunModePrivateData *pd )
553 TICK_N (
"Start drun history" );
554 unsigned int length = 0;
555 gchar *path = g_build_filename (
cache_dir, DRUN_CACHE_FILE, NULL );
557 for (
unsigned int index = 0; index < length; index++ ) {
558 for (
size_t i = 0; i < pd->cmd_list_length; i++ ) {
559 if ( g_strcmp0 ( pd->entry_list[i].desktop_id, retv[index] ) == 0 ) {
560 unsigned int sort_index = length - index;
561 if ( G_LIKELY ( sort_index < INT_MAX ) ) {
562 pd->entry_list[i].sort_index = sort_index;
566 pd->entry_list[i].sort_index = INT_MAX;
573 TICK_N (
"Stop drun history" );
576 static gint drun_int_sort_list ( gconstpointer a, gconstpointer b, G_GNUC_UNUSED gpointer user_data )
578 DRunModeEntry *da = (DRunModeEntry *) a;
579 DRunModeEntry *db = (DRunModeEntry *) b;
581 if ( da->sort_index < 0 && db->sort_index < 0 ) {
582 return g_utf8_collate ( da->name, db->name );
584 return db->sort_index - da->sort_index;
588 static void get_apps ( DRunModePrivateData *pd )
590 TICK_N (
"Get Desktop apps (start)" );
594 dir = g_build_filename ( g_get_user_data_dir (),
"applications", NULL );
595 walk_dir ( pd, dir, dir );
597 TICK_N (
"Get Desktop apps (user dir)" );
599 const gchar *
const * sys = g_get_system_data_dirs ();
600 for (
const gchar *
const *iter = sys; *iter != NULL; ++iter ) {
601 gboolean unique = TRUE;
603 for (
const gchar *
const *iterd = sys; iterd != iter; ++iterd ) {
604 if ( g_strcmp0 ( *iter, *iterd ) == 0 ) {
609 if ( unique && ( **iter ) !=
'\0' ) {
610 dir = g_build_filename ( *iter,
"applications", NULL );
611 walk_dir ( pd, dir, dir );
615 TICK_N (
"Get Desktop apps (system dirs)" );
616 get_apps_history ( pd );
618 g_qsort_with_data ( pd->entry_list, pd->cmd_list_length, sizeof ( DRunModeEntry ), drun_int_sort_list, NULL );
620 TICK_N (
"Sorting done." );
623 static void drun_mode_parse_entry_fields ()
628 const char *
const sep =
",#";
630 for (
unsigned int i = 0; i < DRUN_MATCH_NUM_FIELDS; i++ ) {
631 matching_entry_fields[i].enabled = FALSE;
633 for (
char *token = strtok_r ( switcher_str, sep, &savept ); token != NULL;
634 token = strtok_r ( NULL, sep, &savept ) ) {
635 if ( strcmp ( token,
"all" ) == 0 ) {
636 for (
unsigned int i = 0; i < DRUN_MATCH_NUM_FIELDS; i++ ) {
637 matching_entry_fields[i].enabled = TRUE;
642 gboolean matched = FALSE;
643 for (
unsigned int i = 0; i < DRUN_MATCH_NUM_FIELDS; i++ ) {
644 const char * entry_name = matching_entry_fields[i].entry_field_name;
645 if ( g_ascii_strcasecmp ( token, entry_name ) == 0 ) {
646 matching_entry_fields[i].enabled = TRUE;
651 g_warning (
"Invalid entry name :%s", token );
656 g_free ( switcher_str );
659 static int drun_mode_init (
Mode *sw )
664 DRunModePrivateData *pd = g_malloc0 (
sizeof ( *pd ) );
665 pd->disabled_entries = g_hash_table_new_full ( g_str_hash, g_str_equal, g_free, NULL );
668 const char *current_desktop = g_getenv (
"XDG_CURRENT_DESKTOP" );
669 pd->current_desktop_list = current_desktop ? g_strsplit ( current_desktop,
":", 0 ) : NULL;
671 drun_mode_parse_entry_fields ();
675 static void drun_entry_clear ( DRunModeEntry *e )
679 g_free ( e->app_id );
680 g_free ( e->desktop_id );
681 if ( e->icon != NULL ) {
682 cairo_surface_destroy ( e->icon );
684 g_free ( e->icon_name );
687 g_free ( e->generic_name );
688 g_free ( e->comment );
689 if ( e->action != DRUN_GROUP_NAME ) {
690 g_free ( e->action );
692 g_strfreev ( e->categories );
693 g_key_file_free ( e->key_file );
696 static ModeMode drun_mode_result (
Mode *sw,
int mretv,
char **input,
unsigned int selected_line )
712 else if ( ( mretv &
MENU_OK ) ) {
713 exec_cmd_entry ( &( rmpd->entry_list[selected_line] ) );
715 else if ( ( mretv &
MENU_CUSTOM_INPUT ) && *input != NULL && *input[0] !=
'\0' ) {
720 else if ( ( mretv &
MENU_ENTRY_DELETE ) && selected_line < rmpd->cmd_list_length ) {
722 if ( rmpd->entry_list[selected_line].sort_index >= 0 ) {
723 delete_entry_history ( &( rmpd->entry_list[selected_line] ) );
724 drun_entry_clear ( &( rmpd->entry_list[selected_line] ) );
725 memmove ( &( rmpd->entry_list[selected_line] ), &rmpd->entry_list[selected_line + 1],
726 sizeof ( DRunModeEntry ) * ( rmpd->cmd_list_length - selected_line - 1 ) );
727 rmpd->cmd_list_length--;
733 static void drun_mode_destroy (
Mode *sw )
736 if ( rmpd != NULL ) {
737 for (
size_t i = 0; i < rmpd->cmd_list_length; i++ ) {
738 drun_entry_clear ( &( rmpd->entry_list[i] ) );
740 g_hash_table_destroy ( rmpd->disabled_entries );
741 g_free ( rmpd->entry_list );
743 g_strfreev ( rmpd->current_desktop_list );
749 static char *
_get_display_value (
const Mode *sw,
unsigned int selected_line,
int *state, G_GNUC_UNUSED GList **list,
int get_entry )
756 if ( pd->entry_list == NULL ) {
758 return g_strdup (
"Failed" );
761 DRunModeEntry *dr = &( pd->entry_list[selected_line] );
763 if ( dr->categories ){
764 char *tcats = g_strjoinv(
",", dr->categories);
766 cats = g_markup_escape_text ( tcats, -1 );
774 if ( dr->generic_name ) {
775 egn = g_markup_escape_text ( dr->generic_name, -1 );
778 en = g_markup_escape_text ( dr->name, -1 );
781 ec = g_markup_escape_text ( dr->comment , -1 );
790 "{categories}", cats,
799 static cairo_surface_t *_get_icon (
const Mode *sw,
unsigned int selected_line,
int height )
802 g_return_val_if_fail ( pd->entry_list != NULL, NULL );
803 DRunModeEntry *dr = &( pd->entry_list[selected_line] );
804 if ( dr->icon_name == NULL ) {
807 if ( dr->icon_fetch_uid > 0 ) {
814 static char *drun_get_completion (
const Mode *sw,
unsigned int index )
818 DRunModeEntry *dr = &( pd->entry_list[index] );
819 if ( dr->generic_name == NULL ) {
820 return g_strdup ( dr->name );
823 return g_strdup_printf (
"%s", dr->name );
832 for (
int j = 0; match && tokens != NULL && tokens[j] != NULL; j++ ) {
836 if ( matching_entry_fields[DRUN_MATCH_FIELD_NAME].enabled ) {
837 if ( rmpd->entry_list[index].name ) {
841 if ( matching_entry_fields[DRUN_MATCH_FIELD_GENERIC].enabled ) {
843 if ( test == tokens[j]->invert && rmpd->entry_list[index].generic_name ) {
847 if ( matching_entry_fields[DRUN_MATCH_FIELD_EXEC].enabled ) {
849 if ( test == tokens[j]->invert ) {
853 if ( matching_entry_fields[DRUN_MATCH_FIELD_CATEGORIES].enabled ) {
855 if ( test == tokens[j]->invert ) {
856 gchar **list = rmpd->entry_list[index].categories;
857 for (
int iter = 0; test == tokens[j]->
invert && list && list[iter]; iter++ ) {
862 if ( matching_entry_fields[DRUN_MATCH_FIELD_COMMENT].enabled ) {
864 if ( test == tokens[j]->invert && rmpd->entry_list[index].comment ) {
877 static unsigned int drun_mode_get_num_entries (
const Mode *sw )
880 return pd->cmd_list_length;
886 .cfg_name_key =
"display-drun",
887 ._init = drun_mode_init,
888 ._get_num_entries = drun_mode_get_num_entries,
889 ._result = drun_mode_result,
890 ._destroy = drun_mode_destroy,
891 ._token_match = drun_token_match,
892 ._get_completion = drun_get_completion,
894 ._get_icon = _get_icon,
895 ._preprocess_input = NULL,
896 .private_data = NULL,
900 #endif // ENABLE_DRUN