diff --git a/Makefile.am b/Makefile.am index adcda08c1..509f7761c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -186,7 +186,8 @@ generate-manpage: doc/rofi.1\ doc/rofi-dmenu.5\ doc/rofi-keys.5\ doc/rofi-script.5\ - doc/rofi-theme.5 + doc/rofi-theme.5 \ + doc/rofi-thumbnails.5 doc/rofi.1: doc/rofi.1.markdown pandoc --standalone --to=man --lua-filter=$(top_srcdir)/doc/man_filter.lua -f markdown-tex_math_dollars -o ./$@ ./$< @@ -204,6 +205,8 @@ doc/rofi-script.5: doc/rofi-script.5.markdown pandoc --standalone --to=man --lua-filter=$(top_srcdir)/doc/man_filter.lua -f markdown-tex_math_dollars -o ./$@ ./$< doc/rofi-theme.5: doc/rofi-theme.5.markdown pandoc --standalone --to=man --lua-filter=$(top_srcdir)/doc/man_filter.lua -f markdown-tex_math_dollars -o ./$@ ./$< +doc/rofi-thumbnails.5: doc/rofi-thumbnails.5.markdown + pandoc --standalone --to=man --lua-filter=$(top_srcdir)/doc/man_filter.lua -f markdown-tex_math_dollars -o ./$@ ./$< endif @@ -217,10 +220,12 @@ dist_man5_MANS=\ doc/rofi-dmenu.5\ doc/rofi-keys.5\ doc/rofi-script.5\ - doc/rofi-theme.5 + doc/rofi-theme.5\ + doc/rofi-thumbnails.5 EXTRA_DIST += \ doc/rofi-theme.5.markdown \ + doc/rofi-thumbnails.5.markdown \ doc/rofi-debugging.5.markdown \ doc/rofi-script.5.markdown \ doc/rofi-keys.5.markdown \ diff --git a/README.md b/README.md index 830447013..49cecb731 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,7 @@ new issue. - [rofi-debugging](doc/rofi-debugging.5.markdown) - [rofi-script](doc/rofi-script.5.markdown) - [rofi-theme-selector](doc/rofi-theme-selector.1.markdown) + - [rofi-thumbnails](doc/rofi-thumbnails.5.markdown) - [rofi-keys](doc/rofi-keys.5.markdown) - [rofi-dmenu](doc/rofi-dmenu.5.markdown) diff --git a/config/config.c b/config/config.c index 7bde97035..10ef93c07 100644 --- a/config/config.c +++ b/config/config.c @@ -46,6 +46,9 @@ Settings config = { /** Whether to load and show icons */ .show_icons = FALSE, + /** Custom command to generate preview icons */ + .preview_cmd = NULL, + /** Terminal to use. (for ssh and open in terminal) */ .terminal_emulator = "rofi-sensible-terminal", .ssh_client = "ssh", diff --git a/doc/meson.build b/doc/meson.build index b5bb2f015..7c399eb4c 100644 --- a/doc/meson.build +++ b/doc/meson.build @@ -7,6 +7,7 @@ man_files = [ 'rofi-keys.5', 'rofi-script.5', 'rofi-theme.5', + 'rofi-thumbnails.5', ] fs = import('fs') diff --git a/doc/rofi-thumbnails.5.markdown b/doc/rofi-thumbnails.5.markdown new file mode 100644 index 000000000..e18ced7e1 --- /dev/null +++ b/doc/rofi-thumbnails.5.markdown @@ -0,0 +1,85 @@ +# rofi-thumbnails(5) + +## NAME + +**rofi-thumbnails** - Rofi thumbnails system + +## DESCRIPTION + +**rofi** is now able to show thumbnails for all file types where an XDG compatible thumbnailer is present in the system. + +This is done by default in filebrowser and recursivebrowser mode, if **rofi** is launched with the `-show-icons` argument. + +In a custom user script or dmenu mode, it is possible to produce entry icons using XDG thumbnailers by adding the prefix `thumbnail://` to the filename +specified after `\0icon\x1f`, for example: + +```bash +echo -en "EntryName\0icon\x1fthumbnail://path/to/file\n" | rofi -dmenu -show-icons +``` + +### XDG thumbnailers + +XDG thumbnailers are files with a ".thumbnailer" suffix and a structure similar to ".desktop" files for launching applications. They are placed in `/usr/share/thumbnailers/` or `$HOME/.local/share/thumbnailers/`, and contain a list of mimetypes, for which is possible to produce the thumbnail image, and a string with the command to create said image. The example below shows the content of `librsvg.thumbnailer`, a thumbnailer for svg files using librsvg: + +``` +[Thumbnailer Entry] +TryExec=/usr/bin/gdk-pixbuf-thumbnailer +Exec=/usr/bin/gdk-pixbuf-thumbnailer -s %s %u %o +MimeType=image/svg+xml;image/svg+xml-compressed; +``` + +The images produced are named as the md5sum of the input files and placed, depending on their size, in the XDG thumbnails directories: `$HOME/.cache/thumbnails/{normal,large,x-large,xx-large}`. They are then loaded by **rofi** as entry icons and can also be used by file managers like Thunar, Caja or KDE Dolphin to show their thumbnails. Additionally, if a thumbnail for a file is found in the thumbnails directories (produced previously by **rofi** or a file manager), **rofi** will load it instead of calling the thumbnailer. + +If a suitable thumbnailer for a given file is not found, **rofi** will try to use the corresponding mimetype icon from the icon theme. + +### Custom command to create thumbnails + +It is possible to use a custom command to generate thumbnails for generic entry names, for example a script that downloads an icon given its url or selects different icons depending on the input. This can be done providing the `-preview-cmd` argument followed by a string with the command to execute, with the following syntax: + +``` +rofi ... -preview-cmd 'path/to/script_or_cmd "{input}" "{output}" "{size}"' +``` + +**rofi** will call the script or command substituting `{input}` with the input entry icon name (the string after `\0icon\x1fthumbnail://`), `{output}` with the output filename of the thumbnail and `{size}` with the requested thumbnail size. The script or command is responsible of producing a thumbnail image (if possible respecting the requested size) and saving it in the given `{output}` filename. + +### Issues with AppArmor + +In Linux distributions using AppArmor (such as Ubuntu and Debian), the default rules shipped can cause issues with thumbnails generation. If that is the case, AppArmor can be disabled by issuing the following commands + +``` +sudo systemctl stop apparmor +sudo systemctl disable apparmor +``` + +In alternative, the following apparmor profile con be placed in a file named /etc/apparmor.d/usr.bin.rofi + +``` +#vim:syntax=apparmor +# AppArmor policy for rofi + +#include + +/usr/bin/rofi { + #include + + # TCP/UDP network access for NFS + network inet stream, + network inet6 stream, + network inet dgram, + network inet6 dgram, + + /usr/bin/rofi mr, + + @{HOME}/ r, + @{HOME}/** rw, + owner @{HOME}/.cache/thumbnails/** rw, +} +``` + +then run + +``` +apparmor_parser -r /etc/apparmor.d/usr.bin.rofi +``` + +to reload the rule. This assumes that **rofi** binary is in /usr/bin, that is the case of a standard package installation. diff --git a/include/settings.h b/include/settings.h index 1af9b9527..5da4279fc 100644 --- a/include/settings.h +++ b/include/settings.h @@ -60,6 +60,9 @@ typedef struct { /** Whether to load and show icons */ gboolean show_icons; + + /** Custom command to generate preview icons */ + char *preview_cmd; /** Terminal to use */ char *terminal_emulator; diff --git a/include/widgets/listview.h b/include/widgets/listview.h index 292118383..9fee5b52a 100644 --- a/include/widgets/listview.h +++ b/include/widgets/listview.h @@ -84,10 +84,18 @@ typedef void (*listview_selection_changed_callback)(listview *lv, */ typedef void (*listview_mouse_activated_cb)(listview *, gboolean, void *); + +/** + * Callback when current page is changed. + */ +typedef void (*listview_page_changed_cb)(void); + + /** * @param parent The widget's parent. * @param name The name of the to be created widget. * @param cb The update callback. + * @param page_cb The page change callback. * @param udata The user data to pass to the callback * @param eh The height of one element * @param reverse Reverse the listview order. @@ -95,7 +103,8 @@ typedef void (*listview_mouse_activated_cb)(listview *, gboolean, void *); * @returns a new listview */ listview *listview_create(widget *parent, const char *name, - listview_update_callback cb, void *udata, + listview_update_callback cb, + listview_page_changed_cb page_cb, void *udata, unsigned int eh, gboolean reverse); /** diff --git a/source/modes/dmenu.c b/source/modes/dmenu.c index 3437a6e86..d393fd36f 100644 --- a/source/modes/dmenu.c +++ b/source/modes/dmenu.c @@ -721,9 +721,6 @@ static cairo_surface_t *dmenu_get_icon(const Mode *sw, if (dr->icon_name == NULL) { return NULL; } - if (dr->icon_fetch_uid > 0 && dr->icon_fetch_size == height) { - return rofi_icon_fetcher_get(dr->icon_fetch_uid); - } uint32_t uid = dr->icon_fetch_uid = rofi_icon_fetcher_query(dr->icon_name, height); dr->icon_fetch_size = height; diff --git a/source/modes/filebrowser.c b/source/modes/filebrowser.c index 57791cf80..3b4730b09 100644 --- a/source/modes/filebrowser.c +++ b/source/modes/filebrowser.c @@ -603,11 +603,12 @@ static cairo_surface_t *_get_icon(const Mode *sw, unsigned int selected_line, (FileBrowserModePrivateData *)mode_get_private_data(sw); g_return_val_if_fail(pd->array != NULL, NULL); FBFile *dr = &(pd->array[selected_line]); - if (dr->icon_fetch_uid > 0 && dr->icon_fetch_size == height) { - return rofi_icon_fetcher_get(dr->icon_fetch_uid); - } if (rofi_icon_fetcher_file_is_image(dr->path)) { dr->icon_fetch_uid = rofi_icon_fetcher_query(dr->path, height); + } else if (dr->type == RFILE) { + gchar* _path = g_strconcat("thumbnail://", dr->path, NULL); + dr->icon_fetch_uid = rofi_icon_fetcher_query(_path, height); + g_free(_path); } else { dr->icon_fetch_uid = rofi_icon_fetcher_query(icon_name[dr->type], height); } diff --git a/source/modes/recursivebrowser.c b/source/modes/recursivebrowser.c index 4a4ce6d5e..e6aa1f015 100644 --- a/source/modes/recursivebrowser.c +++ b/source/modes/recursivebrowser.c @@ -493,14 +493,14 @@ static cairo_surface_t *_get_icon(const Mode *sw, unsigned int selected_line, (FileBrowserModePrivateData *)mode_get_private_data(sw); g_return_val_if_fail(pd->array != NULL, NULL); FBFile *dr = &(pd->array[selected_line]); - if (dr->icon_fetch_uid > 0 && dr->icon_fetch_size == height) { - return rofi_icon_fetcher_get(dr->icon_fetch_uid); - } if (rofi_icon_fetcher_file_is_image(dr->path)) { dr->icon_fetch_uid = rofi_icon_fetcher_query(dr->path, height); + } else if (dr->type == RFILE) { + gchar* _path = g_strconcat("thumbnail://", dr->path, NULL); + dr->icon_fetch_uid = rofi_icon_fetcher_query(_path, height); + g_free(_path); } else { - dr->icon_fetch_uid = - rofi_icon_fetcher_query(rb_icon_name[dr->type], height); + dr->icon_fetch_uid = rofi_icon_fetcher_query(rb_icon_name[dr->type], height); } dr->icon_fetch_size = height; return rofi_icon_fetcher_get(dr->icon_fetch_uid); diff --git a/source/rofi-icon-fetcher.c b/source/rofi-icon-fetcher.c index ef5847a93..e22a49757 100644 --- a/source/rofi-icon-fetcher.c +++ b/source/rofi-icon-fetcher.c @@ -51,6 +51,10 @@ #include "helper.h" #include +// thumbnailers key file's group and file extension +#define THUMBNAILER_ENTRY_GROUP "Thumbnailer Entry" +#define THUMBNAILER_EXTENSION ".thumbnailer" + typedef struct { // Context for icon-themes. NkXdgThemeContext *xdg_context; @@ -63,6 +67,9 @@ typedef struct { // list extensions GList *supported_extensions; uint32_t last_uid; + + // thumbnailers per mime-types hashmap + GHashTable *thumbnailers; } IconFetcher; typedef struct { @@ -82,6 +89,7 @@ typedef struct { int hsize; cairo_surface_t *surface; gboolean query_done; + gboolean query_started; IconFetcherNameEntry *entry; } IconFetcherEntry; @@ -93,10 +101,156 @@ static void rofi_icon_fetch_entry_free(gpointer data); */ IconFetcher *rofi_icon_fetcher_data = NULL; +static void rofi_icon_fetcher_load_thumbnailers(const gchar *path) { + gchar *thumb_path = g_build_filename(path, "thumbnailers", NULL); + + GDir *dir = g_dir_open(thumb_path, 0, NULL); + + if (!dir) { + g_free(thumb_path); + return; + } + + const gchar *dirent; + + while ((dirent = g_dir_read_name(dir))) { + if (!g_str_has_suffix(dirent, THUMBNAILER_EXTENSION)) + continue; + + gchar *filename = g_build_filename(thumb_path, dirent, NULL); + GKeyFile *key_file = g_key_file_new(); + GError *error = NULL; + + if (!g_key_file_load_from_file(key_file, filename, 0, &error)) { + g_warning("Error loading thumbnailer %s: %s", filename, error->message); + g_error_free(error); + } else { + gchar *command = g_key_file_get_string( + key_file, THUMBNAILER_ENTRY_GROUP, "Exec", NULL); + gchar **mime_types = g_key_file_get_string_list( + key_file, THUMBNAILER_ENTRY_GROUP, "MimeType", NULL, NULL); + + if (mime_types && command) { + guint i; + for (i = 0; mime_types[i] != NULL; i++) { + if (!g_hash_table_lookup( + rofi_icon_fetcher_data->thumbnailers, mime_types[i])) { + g_info("Loading thumbnailer %s for mimetype %s", filename, mime_types[i]); + g_hash_table_insert( + rofi_icon_fetcher_data->thumbnailers, + g_strdup(mime_types[i]), + g_strdup(command) + ); + } + } + } + + if (mime_types) g_strfreev(mime_types); + if (command) g_free(command); + } + + g_key_file_free(key_file); + g_free(filename); + } + + g_dir_close(dir); + g_free(thumb_path); +} + +static gchar** setup_thumbnailer_command(const gchar *command, + const gchar *filename, + const gchar *encoded_uri, + const gchar *output_path, + int size) { + gchar **command_parts = g_strsplit(command, " ", 0); + guint command_parts_count = g_strv_length(command_parts); + + gchar **command_args = NULL; + + if (command_parts) { + command_args = g_malloc0(sizeof(gchar*) * (command_parts_count + 3 + 1)); + + // set process niceness value to 19 (low priority) + guint current_index = 0; + + command_args[current_index++] = g_strdup("nice"); + command_args[current_index++] = g_strdup("-n"); + command_args[current_index++] = g_strdup("19"); + + // add executable and arguments of the thumbnailer to the list + guint i; + for (i = 0; command_parts[i] != NULL; i++) { + if (strcmp(command_parts[i], "%i") == 0) { + command_args[current_index++] = g_strdup(filename); + } else if (strcmp(command_parts[i], "%u") == 0) { + command_args[current_index++] = g_strdup(encoded_uri); + } else if (strcmp(command_parts[i], "%o") == 0) { + command_args[current_index++] = g_strdup(output_path); + } else if (strcmp(command_parts[i], "%s") == 0) { + command_args[current_index++] = g_strdup_printf("%d", size); + } else { + command_args[current_index++] = g_strdup(command_parts[i]); + } + } + + command_args[current_index++] = NULL; + + g_strfreev(command_parts); + } + + return command_args; +} + +static gboolean exec_thumbnailer_command(gchar **command_args) { + // launch and wait thumbnailers process + gint wait_status; + GError *error = NULL; + + gboolean spawned = g_spawn_sync(NULL, command_args, + NULL, G_SPAWN_DEFAULT | G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, NULL, &wait_status, &error); + + if (spawned) { + return g_spawn_check_exit_status(wait_status, NULL); + } else { + g_warning("Error calling thumbnailer: %s", error->message); + g_error_free(error); + + return FALSE; + } +} + +static gboolean rofi_icon_fetcher_create_thumbnail(const gchar *mime_type, + const gchar *filename, + const gchar *encoded_uri, + const gchar *output_path, + int size) { + gboolean thumbnail_created = FALSE; + + gchar *command = g_hash_table_lookup( + rofi_icon_fetcher_data->thumbnailers, mime_type); + + if (!command) { + return thumbnail_created; + } + + // split command string to isolate arguments and expand them in a list + gchar **command_args = setup_thumbnailer_command( + command, filename, encoded_uri, output_path, size); + + if (command_args) { + thumbnail_created = exec_thumbnailer_command(command_args); + g_strfreev(command_args); + } + + return thumbnail_created; +} + static void rofi_icon_fetch_thread_pool_entry_remove(gpointer data) { IconFetcherEntry *entry = (IconFetcherEntry *)data; // Mark it in a way it should be re-fetched on next query? + entry->query_started = FALSE; } + static void rofi_icon_fetch_entry_free(gpointer data) { IconFetcherNameEntry *entry = (IconFetcherNameEntry *)data; @@ -147,6 +301,20 @@ void rofi_icon_fetcher_init(void) { g_free(exts); } g_slist_free(l); + + // load available thumbnailers from system dirs and user dir + rofi_icon_fetcher_data->thumbnailers = g_hash_table_new_full( + g_str_hash, g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)g_free); + + const gchar * const *system_data_dirs = g_get_system_data_dirs(); + const gchar *user_data_dir = g_get_user_data_dir(); + + rofi_icon_fetcher_load_thumbnailers(user_data_dir); + + guint i; + for (i = 0; system_data_dirs[i] != NULL; i++) { + rofi_icon_fetcher_load_thumbnailers(system_data_dirs[i]); + } } static void free_wrapper(gpointer data, G_GNUC_UNUSED gpointer user_data) { @@ -157,6 +325,8 @@ void rofi_icon_fetcher_destroy(void) { if (rofi_icon_fetcher_data == NULL) { return; } + + g_hash_table_unref(rofi_icon_fetcher_data->thumbnailers); nk_xdg_theme_context_free(rofi_icon_fetcher_data->xdg_context); @@ -288,6 +458,73 @@ gboolean rofi_icon_fetcher_file_is_image(const char *const path) { return FALSE; } +// build thumbnail's path using md5 hash of an entry name +static gchar* rofi_icon_fetcher_get_thumbnail(gchar* name, + int requested_size, + int *thumb_size) { + // calc entry_name md5 hash + GChecksum* checksum = g_checksum_new(G_CHECKSUM_MD5); + g_checksum_update(checksum, (guchar*)name, -1); + const gchar *md5_hex = g_checksum_get_string(checksum); + + // determine thumbnail folder based on the request size + const gchar* cache_dir = g_get_user_cache_dir(); + gchar* thumb_dir; + gchar* thumb_path; + + if (requested_size <= 128) { + *thumb_size = 128; + thumb_dir = g_strconcat(cache_dir, "/thumbnails/normal/", NULL); + thumb_path = g_strconcat(cache_dir, "/thumbnails/normal/", + md5_hex, ".png", NULL); + } else if (requested_size <= 256) { + *thumb_size = 256; + thumb_dir = g_strconcat(cache_dir, "/thumbnails/large/", NULL); + thumb_path = g_strconcat(cache_dir, "/thumbnails/large/", + md5_hex, ".png", NULL); + } else if (requested_size <= 512) { + *thumb_size = 512; + thumb_dir = g_strconcat(cache_dir, "/thumbnails/x-large/", NULL); + thumb_path = g_strconcat(cache_dir, "/thumbnails/x-large/", + md5_hex, ".png", NULL); + } else { + *thumb_size = 1024; + thumb_dir = g_strconcat(cache_dir, "/thumbnails/xx-large/", NULL); + thumb_path = g_strconcat(cache_dir, "/thumbnails/xx-large/", + md5_hex, ".png", NULL); + } + + // create thumbnail directory if it does not exist + g_mkdir_with_parents(thumb_dir, 0700); + + g_free(thumb_dir); + g_checksum_free(checksum); + + return thumb_path; +} + +// retrieves icon key from a .desktop file +static gchar* rofi_icon_fetcher_get_desktop_icon(const gchar* file_path) { + GKeyFile *kf = g_key_file_new(); + GError *key_error = NULL; + gchar *icon_key = NULL; + + gboolean res = g_key_file_load_from_file(kf, file_path, 0, &key_error); + + if (res) { + icon_key = g_key_file_get_string(kf, "Desktop Entry", "Icon", NULL); + } else { + g_debug("Failed to parse desktop file %s because: %s.", + file_path, key_error->message); + + g_error_free(key_error); + } + + g_key_file_free(kf); + + return icon_key; +} + static void rofi_icon_fetcher_worker(thread_state *sdata, G_GNUC_UNUSED gpointer user_data) { g_debug("starting up icon fetching thread."); @@ -299,7 +536,118 @@ static void rofi_icon_fetcher_worker(thread_state *sdata, const gchar *icon_path; gchar *icon_path_ = NULL; - if (g_path_is_absolute(sentry->entry->name)) { + if (g_str_has_prefix(sentry->entry->name, "thumbnail://")) { + // remove uri thumbnail prefix from entry name + gchar *entry_name = &sentry->entry->name[12]; + + if (strcmp(entry_name, "") == 0) { + sentry->query_done = TRUE; + rofi_view_reload(); + return; + } + + // use custom user command to generate the thumbnail + if (config.preview_cmd != NULL) { + int requested_size = MAX(sentry->wsize, sentry->hsize); + int thumb_size; + + icon_path = icon_path_ = rofi_icon_fetcher_get_thumbnail( + entry_name, requested_size, &thumb_size); + + if (!g_file_test(icon_path, G_FILE_TEST_EXISTS)) { + char **command_args = NULL; + int argsv = 0; + gchar *size_str = g_strdup_printf("%d", thumb_size); + + helper_parse_setup( + config.preview_cmd, &command_args, &argsv, + "{input}", entry_name, + "{output}", icon_path_, "{size}", size_str, NULL); + + g_free(size_str); + + if (command_args) { + exec_thumbnailer_command(command_args); + g_strfreev(command_args); + } + } + } else if (g_path_is_absolute(entry_name)) { + // if the entry name is an absolute path try to fetch its thumbnail + if (g_str_has_suffix(entry_name, ".desktop")) { + // if the entry is a .desktop file try to read its icon key + gchar *icon_key = rofi_icon_fetcher_get_desktop_icon(entry_name); + + if (icon_key == NULL || strlen(icon_key) == 0) { + // no icon in .desktop file, fallback on mimetype icon (text/plain) + icon_path = icon_path_ = nk_xdg_theme_get_icon( + rofi_icon_fetcher_data->xdg_context, themes, NULL, "text-plain", + MIN(sentry->wsize, sentry->hsize), 1, TRUE); + + g_free(icon_key); + } else if (g_path_is_absolute(icon_key)) { + // icon in .desktop file is an absolute path to an image + icon_path = icon_path_ = icon_key; + } else { + // icon in .desktop file is a standard icon name + icon_path = icon_path_ = nk_xdg_theme_get_icon( + rofi_icon_fetcher_data->xdg_context, themes, NULL, icon_key, + MIN(sentry->wsize, sentry->hsize), 1, TRUE); + + g_free(icon_key); + } + } else { + // build encoded uri string from absolute file path + gchar *encoded_uri = g_filename_to_uri(entry_name, NULL, NULL); + int requested_size = MAX(sentry->wsize, sentry->hsize); + int thumb_size; + + // look for file thumbnail in appropriate folder based on requested size + icon_path = icon_path_ = rofi_icon_fetcher_get_thumbnail( + encoded_uri, requested_size, &thumb_size); + + if (!g_file_test(icon_path, G_FILE_TEST_EXISTS)) { + // try to generate thumbnail + char *content_type = g_content_type_guess(entry_name, NULL, 0, NULL); + char *mime_type = g_content_type_get_mime_type(content_type); + + if (mime_type) { + gboolean created = rofi_icon_fetcher_create_thumbnail( + mime_type, entry_name, encoded_uri, icon_path_, thumb_size); + + if (!created) { + // replace forward slashes with minus sign to get the icon's name + int index = 0; + + while(mime_type[index]) { + if(mime_type[index] == '/') + mime_type[index] = '-'; + index++; + } + + g_free(icon_path_); + + // try to fetch the mime-type icon + icon_path = icon_path_ = nk_xdg_theme_get_icon( + rofi_icon_fetcher_data->xdg_context, themes, NULL, mime_type, + MIN(sentry->wsize, sentry->hsize), 1, TRUE); + } + + g_free(mime_type); + g_free(content_type); + } + } + + g_free(encoded_uri); + } + } + + // no suitable icon or thumbnail was found + if (icon_path_ == NULL || !g_file_test(icon_path, G_FILE_TEST_EXISTS)) { + sentry->query_done = TRUE; + rofi_view_reload(); + return; + } + } else if (g_path_is_absolute(sentry->entry->name)) { icon_path = sentry->entry->name; } else if (g_str_has_prefix(sentry->entry->name, "data; if (sentry->wsize == wsize && sentry->hsize == hsize) { + if (!sentry->query_started) { + g_thread_pool_push(tpool, sentry, NULL); + } return sentry->uid; } } @@ -422,6 +773,7 @@ uint32_t rofi_icon_fetcher_query_advanced(const char *name, const int wsize, sentry->hsize = hsize; sentry->entry = entry; sentry->query_done = FALSE; + sentry->query_started = TRUE; sentry->surface = NULL; entry->sizes = g_list_prepend(entry->sizes, sentry); @@ -450,6 +802,9 @@ uint32_t rofi_icon_fetcher_query(const char *name, const int size) { iter = g_list_next(iter)) { sentry = iter->data; if (sentry->wsize == size && sentry->hsize == size) { + if (!sentry->query_started) { + g_thread_pool_push(tpool, sentry, NULL); + } return sentry->uid; } } @@ -460,6 +815,8 @@ uint32_t rofi_icon_fetcher_query(const char *name, const int size) { sentry->wsize = size; sentry->hsize = size; sentry->entry = entry; + sentry->query_done = FALSE; + sentry->query_started = TRUE; sentry->surface = NULL; entry->sizes = g_list_prepend(entry->sizes, sentry); @@ -468,6 +825,7 @@ uint32_t rofi_icon_fetcher_query(const char *name, const int size) { // Push into fetching queue. sentry->state.callback = rofi_icon_fetcher_worker; + sentry->state.free = rofi_icon_fetch_thread_pool_entry_remove; sentry->state.priority = G_PRIORITY_LOW; g_thread_pool_push(tpool, sentry, NULL); diff --git a/source/view.c b/source/view.c index 712f1aa71..9e9f1661d 100644 --- a/source/view.c +++ b/source/view.c @@ -1371,6 +1371,11 @@ static void update_callback(textbox *t, icon *ico, unsigned int index, textbox_font(t, *type); } } +static void page_changed_callback() +{ + rofi_view_workers_finalize(); + rofi_view_workers_initialize(); +} void rofi_view_update(RofiViewState *state, gboolean qr) { if (!widget_need_redraw(WIDGET(state->main_window))) { @@ -2372,7 +2377,8 @@ static void rofi_view_add_widget(RofiViewState *state, widget *parent_widget, return; } state->list_view = listview_create(parent_widget, name, update_callback, - state, config.element_height, 0); + page_changed_callback, state, + config.element_height, 0); listview_set_selection_changed_callback( state->list_view, selection_changed_callback, (void *)state); box_add((box *)parent_widget, WIDGET(state->list_view), TRUE); @@ -2718,7 +2724,8 @@ void rofi_view_workers_initialize(void) { } void rofi_view_workers_finalize(void) { if (tpool) { - g_thread_pool_free(tpool, TRUE, TRUE); + // Discard all unprocessed jobs and don't wait for current jobs in execution + g_thread_pool_free(tpool, TRUE, FALSE); tpool = NULL; } } diff --git a/source/widgets/listview.c b/source/widgets/listview.c index ed64dfda6..b76bedee5 100644 --- a/source/widgets/listview.c +++ b/source/widgets/listview.c @@ -120,6 +120,8 @@ struct _listview { xcb_timestamp_t last_click; listview_mouse_activated_cb mouse_activated; void *mouse_activated_data; + + listview_page_changed_cb page_callback; char *listview_name; @@ -284,6 +286,10 @@ static unsigned int scroll_per_page(listview *lv) { (lv->max_elements > 0) ? (lv->selected / lv->max_elements) : 0; offset = page * lv->max_elements; if (page != lv->cur_page) { + + if (lv->page_callback) + lv->page_callback(); + lv->cur_page = page; lv->rchanged = TRUE; } @@ -775,7 +781,8 @@ static gboolean listview_element_motion_notify(widget *wid, } listview *listview_create(widget *parent, const char *name, - listview_update_callback cb, void *udata, + listview_update_callback cb, + listview_page_changed_cb page_cb, void *udata, unsigned int eh, gboolean reverse) { listview *lv = g_malloc0(sizeof(listview)); widget_init(WIDGET(lv), parent, WIDGET_TYPE_LISTVIEW, name); @@ -812,6 +819,8 @@ listview *listview_create(widget *parent, const char *name, lv->callback = cb; lv->udata = udata; + lv->page_callback = page_cb; + // Some settings. lv->spacing = rofi_theme_get_distance(WIDGET(lv), "spacing", DEFAULT_SPACING); lv->menu_columns = diff --git a/source/xrmoptions.c b/source/xrmoptions.c index c2f6efc28..72e16bc1b 100644 --- a/source/xrmoptions.c +++ b/source/xrmoptions.c @@ -128,6 +128,13 @@ static XrmOption xrmOptions[] = { NULL, "Whether to load and show icons", CONFIG_DEFAULT}, + + {xrm_String, + "preview-cmd", + {.str = &config.preview_cmd}, + NULL, + "Custom command to generate preview icons", + CONFIG_DEFAULT}, {xrm_String, "terminal",