mirror of https://github.com/davatorium/rofi.git
[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:
parent
5133f50f30
commit
75ae2a41f0
|
@ -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 \
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -7,6 +7,7 @@ man_files = [
|
|||
'rofi-keys.5',
|
||||
'rofi-script.5',
|
||||
'rofi-theme.5',
|
||||
'rofi-thumbnails.5',
|
||||
]
|
||||
|
||||
fs = import('fs')
|
||||
|
|
|
@ -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.
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -51,6 +51,10 @@
|
|||
#include "helper.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 {
|
||||
// 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, "<span")) {
|
||||
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)) {
|
||||
sentry = iter->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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue