1
0
Fork 0
mirror of https://github.com/davatorium/rofi.git synced 2024-11-18 13:54:36 -05:00

[WIP] xdg thumbnails fetching with fallback on mimetype icons (#1939)

* implemented xdg thumbnails fetching with fallback on mimetype icons for menu entries in filebrowser mode

* included original license text

* added md5 header and source file

* implemented xdg compatible thumbnail's creation

* added -preview-cmd string option to program settings

* support custom command to create images for entries with thumbnail:// prefix

* fix custom thumbnailer command crash caused by null uri when entry is not a valid filename

* check entry_name is not NULL or empty when generating thumbnails;
use snprintf to avoid static analyzer complains

* avoid using gstrvbuilder to build thumbnailer command args

* fixed static analyzer complain about always wrong condition

* use g_spawn_check_exit_status to avoid bump to glib 2.70

* removed md5-c dependency and use glib checksum implementation

* fixed meson build after md5-c library removal

* support thumbnail generation in recursivebrowser mode

* restored check rofi_icon_fetcher_file_is_image

* create thumbnail directories if not existing

* use g_malloc0, g_strdup and g_strdup_printf

* fixed formatting with clang-format

* don't wait for jobs in execution when finalizing the icon fetcher worker threadpool

* destroy and rebuild the icon fetcher worker threadpool when the current page is changed

* added query_started boolean member to IconFetcherEntry;
check if an icon fetcher query was started on an IconFetcherEntry and submit the query again otherwise

* force icon cache lookup even if the item has a valid icon_fetch_uid (the fetching job could have been discarded before starting)

* search binaries in PATH when executing thumbnailer command

* mark icon query as not started in threadpool item free_func

* added listview page_changed_callback; rebuild icon fetcher threadpool in page_changed_callback

* [listview] Add missing code documentation param

* Create rofi-thumbnails.5.markdown

* Updated documentation with apparmor issues and workaround

* [Doc] Ship rofi-thumbnails.5

With some formatting fixes

* use a more compact thumbnailer example

---------

Co-authored-by: giomatfois62 <giomatfois62@yahoo.it>
Co-authored-by: Dave Davenport <DaveDavenport@users.noreply.github.com>
Co-authored-by: lbonn <github@lbonnans.net>
This commit is contained in:
giomatfois62 2024-06-21 18:47:38 +02:00 committed by GitHub
parent 5133f50f30
commit 75ae2a41f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 504 additions and 18 deletions

View file

@ -186,7 +186,8 @@ generate-manpage: doc/rofi.1\
doc/rofi-dmenu.5\ doc/rofi-dmenu.5\
doc/rofi-keys.5\ doc/rofi-keys.5\
doc/rofi-script.5\ doc/rofi-script.5\
doc/rofi-theme.5 doc/rofi-theme.5 \
doc/rofi-thumbnails.5
doc/rofi.1: doc/rofi.1.markdown 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 ./$@ ./$< 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 ./$@ ./$< 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 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 ./$@ ./$< 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 endif
@ -217,10 +220,12 @@ dist_man5_MANS=\
doc/rofi-dmenu.5\ doc/rofi-dmenu.5\
doc/rofi-keys.5\ doc/rofi-keys.5\
doc/rofi-script.5\ doc/rofi-script.5\
doc/rofi-theme.5 doc/rofi-theme.5\
doc/rofi-thumbnails.5
EXTRA_DIST += \ EXTRA_DIST += \
doc/rofi-theme.5.markdown \ doc/rofi-theme.5.markdown \
doc/rofi-thumbnails.5.markdown \
doc/rofi-debugging.5.markdown \ doc/rofi-debugging.5.markdown \
doc/rofi-script.5.markdown \ doc/rofi-script.5.markdown \
doc/rofi-keys.5.markdown \ doc/rofi-keys.5.markdown \

View file

@ -154,6 +154,7 @@ new issue.
- [rofi-debugging](doc/rofi-debugging.5.markdown) - [rofi-debugging](doc/rofi-debugging.5.markdown)
- [rofi-script](doc/rofi-script.5.markdown) - [rofi-script](doc/rofi-script.5.markdown)
- [rofi-theme-selector](doc/rofi-theme-selector.1.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-keys](doc/rofi-keys.5.markdown)
- [rofi-dmenu](doc/rofi-dmenu.5.markdown) - [rofi-dmenu](doc/rofi-dmenu.5.markdown)

View file

@ -46,6 +46,9 @@ Settings config = {
/** Whether to load and show icons */ /** Whether to load and show icons */
.show_icons = FALSE, .show_icons = FALSE,
/** Custom command to generate preview icons */
.preview_cmd = NULL,
/** Terminal to use. (for ssh and open in terminal) */ /** Terminal to use. (for ssh and open in terminal) */
.terminal_emulator = "rofi-sensible-terminal", .terminal_emulator = "rofi-sensible-terminal",
.ssh_client = "ssh", .ssh_client = "ssh",

View file

@ -7,6 +7,7 @@ man_files = [
'rofi-keys.5', 'rofi-keys.5',
'rofi-script.5', 'rofi-script.5',
'rofi-theme.5', 'rofi-theme.5',
'rofi-thumbnails.5',
] ]
fs = import('fs') fs = import('fs')

View file

@ -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 <tunables/global>
/usr/bin/rofi {
#include <abstractions/base>
# 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.

View file

@ -61,6 +61,9 @@ typedef struct {
/** Whether to load and show icons */ /** Whether to load and show icons */
gboolean show_icons; gboolean show_icons;
/** Custom command to generate preview icons */
char *preview_cmd;
/** Terminal to use */ /** Terminal to use */
char *terminal_emulator; char *terminal_emulator;
/** SSH client to use */ /** SSH client to use */

View file

@ -84,10 +84,18 @@ typedef void (*listview_selection_changed_callback)(listview *lv,
*/ */
typedef void (*listview_mouse_activated_cb)(listview *, gboolean, void *); 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 parent The widget's parent.
* @param name The name of the to be created widget. * @param name The name of the to be created widget.
* @param cb The update callback. * @param cb The update callback.
* @param page_cb The page change callback.
* @param udata The user data to pass to the callback * @param udata The user data to pass to the callback
* @param eh The height of one element * @param eh The height of one element
* @param reverse Reverse the listview order. * @param reverse Reverse the listview order.
@ -95,7 +103,8 @@ typedef void (*listview_mouse_activated_cb)(listview *, gboolean, void *);
* @returns a new listview * @returns a new listview
*/ */
listview *listview_create(widget *parent, const char *name, 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); unsigned int eh, gboolean reverse);
/** /**

View file

@ -721,9 +721,6 @@ static cairo_surface_t *dmenu_get_icon(const Mode *sw,
if (dr->icon_name == NULL) { if (dr->icon_name == NULL) {
return 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 = uint32_t uid = dr->icon_fetch_uid =
rofi_icon_fetcher_query(dr->icon_name, height); rofi_icon_fetcher_query(dr->icon_name, height);
dr->icon_fetch_size = height; dr->icon_fetch_size = height;

View file

@ -603,11 +603,12 @@ static cairo_surface_t *_get_icon(const Mode *sw, unsigned int selected_line,
(FileBrowserModePrivateData *)mode_get_private_data(sw); (FileBrowserModePrivateData *)mode_get_private_data(sw);
g_return_val_if_fail(pd->array != NULL, NULL); g_return_val_if_fail(pd->array != NULL, NULL);
FBFile *dr = &(pd->array[selected_line]); 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)) { if (rofi_icon_fetcher_file_is_image(dr->path)) {
dr->icon_fetch_uid = rofi_icon_fetcher_query(dr->path, height); 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 { } else {
dr->icon_fetch_uid = rofi_icon_fetcher_query(icon_name[dr->type], height); dr->icon_fetch_uid = rofi_icon_fetcher_query(icon_name[dr->type], height);
} }

View file

@ -493,14 +493,14 @@ static cairo_surface_t *_get_icon(const Mode *sw, unsigned int selected_line,
(FileBrowserModePrivateData *)mode_get_private_data(sw); (FileBrowserModePrivateData *)mode_get_private_data(sw);
g_return_val_if_fail(pd->array != NULL, NULL); g_return_val_if_fail(pd->array != NULL, NULL);
FBFile *dr = &(pd->array[selected_line]); 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)) { if (rofi_icon_fetcher_file_is_image(dr->path)) {
dr->icon_fetch_uid = rofi_icon_fetcher_query(dr->path, height); 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 { } else {
dr->icon_fetch_uid = dr->icon_fetch_uid = rofi_icon_fetcher_query(rb_icon_name[dr->type], height);
rofi_icon_fetcher_query(rb_icon_name[dr->type], height);
} }
dr->icon_fetch_size = height; dr->icon_fetch_size = height;
return rofi_icon_fetcher_get(dr->icon_fetch_uid); return rofi_icon_fetcher_get(dr->icon_fetch_uid);

View file

@ -51,6 +51,10 @@
#include "helper.h" #include "helper.h"
#include <gdk-pixbuf/gdk-pixbuf.h> #include <gdk-pixbuf/gdk-pixbuf.h>
// thumbnailers key file's group and file extension
#define THUMBNAILER_ENTRY_GROUP "Thumbnailer Entry"
#define THUMBNAILER_EXTENSION ".thumbnailer"
typedef struct { typedef struct {
// Context for icon-themes. // Context for icon-themes.
NkXdgThemeContext *xdg_context; NkXdgThemeContext *xdg_context;
@ -63,6 +67,9 @@ typedef struct {
// list extensions // list extensions
GList *supported_extensions; GList *supported_extensions;
uint32_t last_uid; uint32_t last_uid;
// thumbnailers per mime-types hashmap
GHashTable *thumbnailers;
} IconFetcher; } IconFetcher;
typedef struct { typedef struct {
@ -82,6 +89,7 @@ typedef struct {
int hsize; int hsize;
cairo_surface_t *surface; cairo_surface_t *surface;
gboolean query_done; gboolean query_done;
gboolean query_started;
IconFetcherNameEntry *entry; IconFetcherNameEntry *entry;
} IconFetcherEntry; } IconFetcherEntry;
@ -93,10 +101,156 @@ static void rofi_icon_fetch_entry_free(gpointer data);
*/ */
IconFetcher *rofi_icon_fetcher_data = NULL; 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) { static void rofi_icon_fetch_thread_pool_entry_remove(gpointer data) {
IconFetcherEntry *entry = (IconFetcherEntry *)data; IconFetcherEntry *entry = (IconFetcherEntry *)data;
// Mark it in a way it should be re-fetched on next query? // 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) { static void rofi_icon_fetch_entry_free(gpointer data) {
IconFetcherNameEntry *entry = (IconFetcherNameEntry *)data; IconFetcherNameEntry *entry = (IconFetcherNameEntry *)data;
@ -147,6 +301,20 @@ void rofi_icon_fetcher_init(void) {
g_free(exts); g_free(exts);
} }
g_slist_free(l); 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) { static void free_wrapper(gpointer data, G_GNUC_UNUSED gpointer user_data) {
@ -158,6 +326,8 @@ void rofi_icon_fetcher_destroy(void) {
return; return;
} }
g_hash_table_unref(rofi_icon_fetcher_data->thumbnailers);
nk_xdg_theme_context_free(rofi_icon_fetcher_data->xdg_context); nk_xdg_theme_context_free(rofi_icon_fetcher_data->xdg_context);
g_hash_table_unref(rofi_icon_fetcher_data->icon_cache_uid); g_hash_table_unref(rofi_icon_fetcher_data->icon_cache_uid);
@ -288,6 +458,73 @@ gboolean rofi_icon_fetcher_file_is_image(const char *const path) {
return FALSE; 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, static void rofi_icon_fetcher_worker(thread_state *sdata,
G_GNUC_UNUSED gpointer user_data) { G_GNUC_UNUSED gpointer user_data) {
g_debug("starting up icon fetching thread."); g_debug("starting up icon fetching thread.");
@ -299,7 +536,118 @@ static void rofi_icon_fetcher_worker(thread_state *sdata,
const gchar *icon_path; const gchar *icon_path;
gchar *icon_path_ = NULL; 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; icon_path = sentry->entry->name;
} else if (g_str_has_prefix(sentry->entry->name, "<span")) { } else if (g_str_has_prefix(sentry->entry->name, "<span")) {
cairo_surface_t *surface = cairo_image_surface_create( cairo_surface_t *surface = cairo_image_surface_create(
@ -411,6 +759,9 @@ uint32_t rofi_icon_fetcher_query_advanced(const char *name, const int wsize,
iter = g_list_next(iter)) { iter = g_list_next(iter)) {
sentry = iter->data; sentry = iter->data;
if (sentry->wsize == wsize && sentry->hsize == hsize) { if (sentry->wsize == wsize && sentry->hsize == hsize) {
if (!sentry->query_started) {
g_thread_pool_push(tpool, sentry, NULL);
}
return sentry->uid; return sentry->uid;
} }
} }
@ -422,6 +773,7 @@ uint32_t rofi_icon_fetcher_query_advanced(const char *name, const int wsize,
sentry->hsize = hsize; sentry->hsize = hsize;
sentry->entry = entry; sentry->entry = entry;
sentry->query_done = FALSE; sentry->query_done = FALSE;
sentry->query_started = TRUE;
sentry->surface = NULL; sentry->surface = NULL;
entry->sizes = g_list_prepend(entry->sizes, sentry); 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)) { iter = g_list_next(iter)) {
sentry = iter->data; sentry = iter->data;
if (sentry->wsize == size && sentry->hsize == size) { if (sentry->wsize == size && sentry->hsize == size) {
if (!sentry->query_started) {
g_thread_pool_push(tpool, sentry, NULL);
}
return sentry->uid; return sentry->uid;
} }
} }
@ -460,6 +815,8 @@ uint32_t rofi_icon_fetcher_query(const char *name, const int size) {
sentry->wsize = size; sentry->wsize = size;
sentry->hsize = size; sentry->hsize = size;
sentry->entry = entry; sentry->entry = entry;
sentry->query_done = FALSE;
sentry->query_started = TRUE;
sentry->surface = NULL; sentry->surface = NULL;
entry->sizes = g_list_prepend(entry->sizes, sentry); 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. // Push into fetching queue.
sentry->state.callback = rofi_icon_fetcher_worker; sentry->state.callback = rofi_icon_fetcher_worker;
sentry->state.free = rofi_icon_fetch_thread_pool_entry_remove;
sentry->state.priority = G_PRIORITY_LOW; sentry->state.priority = G_PRIORITY_LOW;
g_thread_pool_push(tpool, sentry, NULL); g_thread_pool_push(tpool, sentry, NULL);

View file

@ -1371,6 +1371,11 @@ static void update_callback(textbox *t, icon *ico, unsigned int index,
textbox_font(t, *type); 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) { void rofi_view_update(RofiViewState *state, gboolean qr) {
if (!widget_need_redraw(WIDGET(state->main_window))) { if (!widget_need_redraw(WIDGET(state->main_window))) {
@ -2372,7 +2377,8 @@ static void rofi_view_add_widget(RofiViewState *state, widget *parent_widget,
return; return;
} }
state->list_view = listview_create(parent_widget, name, update_callback, 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( listview_set_selection_changed_callback(
state->list_view, selection_changed_callback, (void *)state); state->list_view, selection_changed_callback, (void *)state);
box_add((box *)parent_widget, WIDGET(state->list_view), TRUE); 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) { void rofi_view_workers_finalize(void) {
if (tpool) { 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; tpool = NULL;
} }
} }

View file

@ -121,6 +121,8 @@ struct _listview {
listview_mouse_activated_cb mouse_activated; listview_mouse_activated_cb mouse_activated;
void *mouse_activated_data; void *mouse_activated_data;
listview_page_changed_cb page_callback;
char *listview_name; char *listview_name;
PangoEllipsizeMode emode; PangoEllipsizeMode emode;
@ -284,6 +286,10 @@ static unsigned int scroll_per_page(listview *lv) {
(lv->max_elements > 0) ? (lv->selected / lv->max_elements) : 0; (lv->max_elements > 0) ? (lv->selected / lv->max_elements) : 0;
offset = page * lv->max_elements; offset = page * lv->max_elements;
if (page != lv->cur_page) { if (page != lv->cur_page) {
if (lv->page_callback)
lv->page_callback();
lv->cur_page = page; lv->cur_page = page;
lv->rchanged = TRUE; lv->rchanged = TRUE;
} }
@ -775,7 +781,8 @@ static gboolean listview_element_motion_notify(widget *wid,
} }
listview *listview_create(widget *parent, const char *name, 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) { unsigned int eh, gboolean reverse) {
listview *lv = g_malloc0(sizeof(listview)); listview *lv = g_malloc0(sizeof(listview));
widget_init(WIDGET(lv), parent, WIDGET_TYPE_LISTVIEW, name); 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->callback = cb;
lv->udata = udata; lv->udata = udata;
lv->page_callback = page_cb;
// Some settings. // Some settings.
lv->spacing = rofi_theme_get_distance(WIDGET(lv), "spacing", DEFAULT_SPACING); lv->spacing = rofi_theme_get_distance(WIDGET(lv), "spacing", DEFAULT_SPACING);
lv->menu_columns = lv->menu_columns =

View file

@ -129,6 +129,13 @@ static XrmOption xrmOptions[] = {
"Whether to load and show icons", "Whether to load and show icons",
CONFIG_DEFAULT}, CONFIG_DEFAULT},
{xrm_String,
"preview-cmd",
{.str = &config.preview_cmd},
NULL,
"Custom command to generate preview icons",
CONFIG_DEFAULT},
{xrm_String, {xrm_String,
"terminal", "terminal",
{.str = &config.terminal_emulator}, {.str = &config.terminal_emulator},