<commit_before>/*
 * vim:ts=8:sw=3:sts=8:noexpandtab:cino=>5n-3f0^-2{2
 */

/* TODO option for maximum items to cache */

#include "Evry.h"
#include "e_mod_main.h"
#include <Efreet_Trash.h>

#define MAX_ITEMS 10
#define MAX_SHOWN 300
#define TERM_ACTION_DIR "%s"

#define CMD_NONE        0
#define CMD_SHOW_ROOT   1
#define CMD_SHOW_HIDDEN 2
#define CMD_SHOW_PARENT 3

#define ACT_TRASH	1
#define ACT_DELETE	2
#define ACT_COPY	3
#define ACT_MOVE	4

#define ONE_DAY  86400.0
#define SIX_DAYS_AGO (ecore_time_get() - 518400.0)
#define TIME_FACTOR(_now) (1.0 - (evry_hist->begin / _now)) / 1000000000000000.0

/* #undef DBG
 * #define DBG(...) ERR(__VA_ARGS__) */

typedef struct _Plugin Plugin;
typedef struct _Data Data;
typedef struct _Module_Config Module_Config;

struct _Plugin
{
  Evry_Plugin base;

  Eina_List *files;
  const char *directory;
  const char *input;
  unsigned int command;
  unsigned int min_query;
  Eina_Bool parent;
  Eina_Bool show_hidden;
  Eina_Bool dirs_only;
  Eina_Bool show_recent;

  Ecore_Thread *thread;
  Ecore_File_Monitor *dir_mon;
  int wait_finish;
};

struct _Data
{
  Plugin *plugin;
  char *directory;
  long id;
  int level;
  int cnt;
  Eina_List *files;
  Eina_List *list;
  DIR *dirp;
  int run_cnt;
};

struct _Module_Config
{
  int version;

  unsigned char show_homedir;
  unsigned char show_recent;
  unsigned char search_recent;
  unsigned char cache_dirs;
  unsigned char search_cache;

  // TODO
  int sort_by;
  Eina_List *search_dirs;

  E_Config_Dialog *cfd;
  E_Module *module;
};

static Module_Config *_conf;
static char _module_icon[] = "system-file-manager";
static Eina_List *_plugins = NULL;
static Eina_List *_actions = NULL;
static const char *_mime_dir;
static const char *_mime_mount;
static Eina_Bool clear_cache = EINA_FALSE;

/***************************************************************************/

static void
_item_fill(Evry_Item_File *file)
{
   if (!file->mime)
     {
	const char *mime = efreet_mime_type_get(file->path);

	if (mime)
	  file->mime = eina_stringshare_ref(mime);
	else
	  file->mime = eina_stringshare_add("unknown");
     }

   if ((file->mime == _mime_dir) ||
       (file->mime == _mime_mount))
     EVRY_ITEM(file)->browseable = EINA_TRUE;

   EVRY_ITEM(file)->context = eina_stringshare_ref(file->mime);

   if (!EVRY_ITEM(file)->detail)
     evry_util_file_detail_set(file);


     evry_util_file_detail_set(file);
}

static int
_cb_sort(const void *data1, const void *data2)
{
   const Evry_Item *it1 = data1;
   const Evry_Item *it2 = data2;

   if (it1->browseable && !it2->browseable)
     return -1;

   if (!it1->browseable && it2->browseable)
     return 1;

   if (it1->fuzzy_match && it2->fuzzy_match)
     if (it1->fuzzy_match - it2->fuzzy_match)
       return (it1->fuzzy_match - it2->fuzzy_match);

   return strcasecmp(it1->label, it2->label);
}

static void
_item_free(Evry_Item *it)
{
   GET_FILE(file, it);
   if (file->url) eina_stringshare_del(file->url);
   if (file->path) eina_stringshare_del(file->path);
   if (file->mime) eina_stringshare_del(file->mime);

   E_FREE(file);
}

static void
_scan_func(void *data)
{
   Data *d = data;
   Plugin *p = d->plugin;
   char *filename;
   struct dirent *dp;
   Evry_Item_File *file;
   char buf[4096];
   Eina_Bool is_dir;

   d->dirp = opendir(d->directory);
   if (!d->dirp) return;

   while ((dp = readdir(d->dirp)))
     {
	if ((dp->d_name[0] == '.') &&
	    ((dp->d_name[1] == '\0') ||
	     ((dp->d_name[1] == '.') &&
	      (dp->d_name[2] == '\0'))))
	  continue;

#ifdef _DIRENT_HAVE_D_TYPE
	if (dp->d_type == DT_FIFO ||
	    dp->d_type == DT_BLK  ||
	    dp->d_type == DT_CHR  ||
	    dp->d_type == DT_SOCK)
	  continue;
#endif
	if (!p->show_hidden)
	  {
	     if (dp->d_name[0] == '.')
	       continue;
	  }
	else
	  {
	     if (dp->d_name[0] != '.')
	       continue;
	  }

	is_dir = EINA_FALSE;

#ifdef _DIRENT_HAVE_D_TYPE
	if (dp->d_type & DT_UNKNOWN)
	  {
#endif
	     if (ecore_file_is_dir(file->path))
	       is_dir = EINA_TRUE;

#ifdef _DIRENT_HAVE_D_TYPE
	  }
	else if (dp->d_type & DT_DIR)
	  {
	     is_dir = EINA_TRUE;
	  }
#endif

	if (p->dirs_only && !is_dir)
	  continue;

	file = EVRY_ITEM_NEW(Evry_Item_File, p, NULL, NULL, _item_free);

	filename = strdup(dp->d_name);
	EVRY_ITEM(file)->label = filename;
	EVRY_ITEM(file)->browseable = is_dir;
	d->files = eina_list_append(d->files, file);

	snprintf(buf, sizeof(buf), "%s/%s", d->directory, filename);
	file->path = strdup(buf);
     }
   closedir(d->dirp);
}

static void
_scan_mime_func(void *data)
{
   Data *d = data;
   Plugin *p = d->plugin;
   Evry_Item_File *file;
   Eina_List *l;
   const char *mime;
   int cnt = 0;

   EINA_LIST_FOREACH(d->files, l, file)
     {
	if (!EVRY_ITEM(file)->browseable)
	  {
	     if ((mime = efreet_mime_type_get(file->path)))
	       {
		  file->mime = mime;

		  if (!strncmp(file->mime, "inode/", 6) &&
		      ecore_file_is_dir(file->path))
		    EVRY_ITEM(file)->browseable = EINA_TRUE;
	       }
	     else
	       {
		  file->mime = "unknown";
	       }
	  }
	if (cnt++ > MAX_ITEMS * d->run_cnt) break;
     }
}

static int
_append_files(Plugin *p)
{
   int match;
   int cnt = 0;
   Evry_Item *it;
   Eina_List *l;

   EVRY_PLUGIN_ITEMS_CLEAR(p);

   if (!p->command && p->min_query)
     {
	if (!p->input)
	  return 0;
	if (strlen(p->input) < p->min_query)
	  return 0;
     }

   EINA_LIST_FOREACH(p->files, l, it)
     {
	if (cnt >= MAX_SHOWN) break;

	if (p->dirs_only && !it->browseable)
	  continue;

	if (p->input && (match = evry_fuzzy_match(it->label, p->input)))
	  {
	     it->fuzzy_match = match;
	     if (!it->browseable)
	       it->priority = 1;
	     EVRY_PLUGIN_ITEM_APPEND(p, it);
	     cnt++;
	  }
	else if (!p->input)
	  {
	     if (!it->browseable)
	       it->priority = 1;
	     EVRY_PLUGIN_ITEM_APPEND(p, it);
	     cnt++;
	  }
     }
   return cnt;
}

static void
_scan_cancel_func(void *data)
{
   Data *d = data;
   Plugin *p = d->plugin;
   Evry_Item_File *file;
   Evry_Item *item;

   if (!d->run_cnt)
     {
	EINA_LIST_FREE(d->files, file)
	  {
	     if (file->base.label) free((char *)(file->base.label));
	     if (file->path) free((char *)file->path);
	     free(file);
	  }
     }
   else
     {
	EINA_LIST_FREE(d->files, file)
	  {
	     if (file->path)
	       eina_stringshare_del(file->path);
	     if (EVRY_ITEM(file)->label)
	       eina_stringshare_del(EVRY_ITEM(file)->label);
	     if (EVRY_ITEM(file)->detail)
	       eina_stringshare_del(EVRY_ITEM(file)->detail);
	     free(file);
	  }
     }

   free(d->directory);
   E_FREE(d);

   p->thread = NULL;

   if (p->wait_finish)
     E_FREE(p);
}

static void
_scan_end_func(void *data)
{
   Data *d = data;
   Plugin *p = d->plugin;
   Evry_Item *item;
   Evry_Item_File *f;
   char *filename, *path, *mime;
   Eina_List *l, *ll, *lll;
   History_Item *hi;
   History_Entry *he;
   History_Types *ht = NULL;
   int cnt = 0;

   if (_conf->cache_dirs)
     ht = evry_history_types_get(evry_hist->subjects, EVRY_TYPE_FILE);

   if (!d->run_cnt)
     {
	EINA_LIST_FOREACH_SAFE(d->files, l, ll, item)
	  {
	     GET_FILE(file, item);

	     filename = (char *)item->label;
	     path = (char *) file->path;

	     if (!filename || !path)
	       goto next;

	     file->path = eina_stringshare_add(path);

	     if (item->browseable)
	       file->mime = eina_stringshare_ref(_mime_dir);

	     /* check if we can grab the mimetype from history */
	     if ((_conf->cache_dirs && ht) &&
		 (he = eina_hash_find(ht->types, file->path)))
	       {
		  EINA_LIST_FOREACH(he->items, lll, hi)
		    {
		       if (hi->data)
			 {
			    if (!file->mime)
			      file->mime = eina_stringshare_ref(hi->data);

			    DBG("cached: %s %s", file->mime, file->path);
			    hi->transient = 0;
			    break;
			 }
		    }
	       }

	     if (file->mime)
	       {
		  item->context = eina_stringshare_ref(file->mime);

		  d->files = eina_list_remove_list(d->files, l);
		  p->files = eina_list_append(p->files, file);
	       }

	     item->id = eina_stringshare_ref(file->path);
	     item->label = eina_stringshare_add(filename);
	     evry_util_file_detail_set(file);

	     E_FREE(filename);
	     E_FREE(path);
	     continue;

	  next:
	     E_FREE(filename);
	     E_FREE(path);
	     if (file->path) eina_stringshare_del(file->path);
	     d->files = eina_list_remove(d->files, file);
	     E_FREE(file);
	  }

	if (d->files)
	  {
	     d->run_cnt++;
	     d->files = eina_list_sort(d->files, -1, _cb_sort);
	     p->thread = ecore_thread_run(_scan_mime_func,
					  _scan_end_func,
					  _scan_cancel_func, d);
	     return;
	  }
     }
   else
     {
	EINA_LIST_FREE(d->files, item)
	  {
	     GET_FILE(file, item);

	     mime = (char *) file->mime;

	     if (!mime) break;

	     file->mime = eina_stringshare_ref(mime);
	     item->context = eina_stringshare_ref(mime);

	     p->files = eina_list_append(p->files, file);
	  }

	if (d->files)
	  {
	     d->run_cnt++;
	     p->thread = ecore_thread_run(_scan_mime_func,
					  _scan_end_func,
					  _scan_cancel_func, d);
	  }
     }

   if (!d->files)
     {
	if (!(p->command == CMD_SHOW_HIDDEN) &&  _conf->cache_dirs)
	  {
	     EINA_LIST_FOREACH(p->files, l, item)
	       {
		  GET_FILE(file, item);

		  if (!item->usage &&
		      (hi = evry_history_add(evry_hist->subjects, item, NULL, NULL)))
		    {
		       hi->last_used = SIX_DAYS_AGO - (0.001 * (double) cnt++);
		       hi->usage = TIME_FACTOR(hi->last_used);
		       hi->data = eina_stringshare_ref(file->mime);
		       item->hi = hi;
		    }
		  else if (item->hi && (item->hi->count == 1) &&
			   (item->hi->last_used < SIX_DAYS_AGO))
		    {
		       item->hi->last_used = SIX_DAYS_AGO - (0.001 * (double) cnt++);
		       item->hi->usage = TIME_FACTOR(hi->last_used);
		    }
	       }
	  }

	free(d->directory);
	E_FREE(d);
	p->thread = NULL;
     }

   p->files = eina_list_sort(p->files, -1, _cb_sort);

   _append_files(p);

   evry_plugin_async_update(EVRY_PLUGIN(p), EVRY_ASYNC_UPDATE_ADD);
}

static void
_dir_watcher(void *data, Ecore_File_Monitor *em, Ecore_File_Event event, const char *path)
{
   Plugin *p = data;
   Evry_Item_File *file;
   const char *label;
   Eina_List *ll, *l;

   switch (event)
     {
      case ECORE_FILE_EVENT_DELETED_SELF:
	 EINA_LIST_FREE(p->files, file)
	  evry_item_free(EVRY_ITEM(file));
	break;

      case ECORE_FILE_EVENT_CREATED_DIRECTORY:
      case ECORE_FILE_EVENT_CREATED_FILE:
	 DBG("added %s", path);

	 label = ecore_file_file_get(path);

	 file = EVRY_ITEM_NEW(Evry_Item_File, p, label, NULL, _item_free);
	 file->path = eina_stringshare_add(path);

	 if (event == ECORE_FILE_EVENT_CREATED_DIRECTORY)
	   file->mime = eina_stringshare_ref(_mime_dir);

	 _item_fill(file);

	 p->files = eina_list_append(p->files, file);

	 break;

      case ECORE_FILE_EVENT_DELETED_FILE:
      case ECORE_FILE_EVENT_DELETED_DIRECTORY:
	 label = eina_stringshare_add(path);
	 DBG("delete %s", path);

	 EINA_LIST_FOREACH_SAFE(p->files, l, ll, file)
	   {
	      if (file->path != label) continue;

	      p->files = eina_list_remove_list(p->files, l);

	      EVRY_ITEM_FREE(file);
	      break;
	   }
	 eina_stringshare_del(label);
	 break;

      default:
	 return;
     }

   _append_files(p);

   evry_plugin_async_update(EVRY_PLUGIN(p), EVRY_ASYNC_UPDATE_ADD);
}

static void
_read_directory(Plugin *p)
{
   Data *d = E_NEW(Data, 1);
   d->plugin = p;
   d->directory = strdup(p->directory);

   p->thread = ecore_thread_run(_scan_func, _scan_end_func, _scan_cancel_func, d);

   if (p->dir_mon)
     ecore_file_monitor_del(p->dir_mon);

   p->dir_mon = ecore_file_monitor_add(p->directory, _dir_watcher, p);
}

static Evry_Plugin *
_browse(Evry_Plugin *plugin, const Evry_Item *it)
{
   Plugin *p = NULL;

   if (!it || (it && !CHECK_TYPE(it, EVRY_TYPE_FILE)))
     return NULL;

   GET_PLUGIN(parent, plugin);
   GET_FILE(file, it);

   if (!evry_file_path_get(file) ||
       !ecore_file_is_dir(file->path))
     return NULL;

   p = E_NEW(Plugin, 1);
   p->base = *plugin;
   p->base.items = NULL;

   p->directory = eina_stringshare_add(file->path);
   p->parent = EINA_TRUE;

   _read_directory(p);

   return EVRY_PLUGIN(p);
}

static Evry_Plugin *
_begin(Evry_Plugin *plugin, const Evry_Item *it)
{
   Plugin *p = NULL;

   if (it && !CHECK_TYPE(it, EVRY_TYPE_ACTION))
     return NULL;

   p = E_NEW(Plugin, 1);
   p->base = *plugin;
   p->base.items = NULL;

   if (it)
     {
	/* provide object */

	p->directory = eina_stringshare_add(e_user_homedir_get());
	p->parent = EINA_FALSE;
	_read_directory(p);
     }
   else
     {
	/* provide subject */
	p->min_query = plugin->config->min_query;

	if (_conf->show_homedir)
	  p->directory = eina_stringshare_add(e_user_homedir_get());

	_read_directory(p);

	return EVRY_PLUGIN(p);
     }

   return EVRY_PLUGIN(p);
}

static void
_folder_item_add(Plugin *p, const char *path)
{
   Evry_Item_File *file;

   file = EVRY_ITEM_NEW(Evry_Item_File, p, path, NULL, _item_free);
   file->path = eina_stringshare_add(path);
   file->mime = eina_stringshare_ref(_mime_dir);
   EVRY_ITEM(file)->browseable = EINA_TRUE;
   p->files = eina_list_append(p->files, file);
   EVRY_PLUGIN_ITEM_APPEND(p, file);
}

static void
_free_files(Plugin *p)
{
   Evry_Item_File *file;

   if (p->thread)
     ecore_thread_cancel(p->thread);
   p->thread = NULL;

   EINA_LIST_FREE(p->files, file)
     EVRY_ITEM_FREE(file);

   if (p->dir_mon)
     ecore_file_monitor_del(p->dir_mon);
   p->dir_mon = NULL;
}

static void
_finish(Evry_Plugin *plugin)
{
   GET_PLUGIN(p, plugin);

   IF_RELEASE(p->input);
   IF_RELEASE(p->directory);

   if (p->thread)
     {
	ecore_thread_cancel(p->thread);
	p->wait_finish = 1;
	p->thread = NULL;
     }

   EVRY_PLUGIN_ITEMS_CLEAR(p);

   _free_files(p);

   if (!p->wait_finish)
     E_FREE(p);
}

static int
_fetch(Evry_Plugin *plugin, const char *input)
{
   GET_PLUGIN(p, plugin);
   Evry_Item_File *file;
   int len = (input ? strlen(input) : 0);

   if (!p->command)
     EVRY_PLUGIN_ITEMS_CLEAR(p);

   IF_RELEASE(p->input);

   if (!p->parent && input && !strncmp(input, "/", 1))
     {
	char *path = NULL;

	if ((p->command != CMD_SHOW_ROOT) ||
	    ((ecore_file_is_dir(input) ? (path = strdup(input)) : 0) ||
	     ((path = ecore_file_dir_get(input)) &&
	      (strcmp(p->directory, path)))))
	  {
	     _free_files(p);

	     eina_stringshare_del(p->directory);

	     if (path)
	       {
		  p->directory = eina_stringshare_add(path);
		  free(path);
	       }
	     else
	       {
		  p->directory = eina_stringshare_add("/");
	       }

	     _read_directory(p);

	     p->command = CMD_SHOW_ROOT;

	     return 0;
	  }
	int len = strlen(p->directory);
	len = (len == 1) ? len : len+1;

	p->input = eina_stringshare_add(input + len);
     }
   else if (p->directory && input && !strncmp(input, "..", 2))
     {
	if (p->command != CMD_SHOW_PARENT)
	  {
	     char *dir;
	     char buf[PATH_MAX];

	     if (strncmp(p->directory, "/", 1))
	       return 0;

	     _free_files(p);

	     strncpy(buf, p->directory, PATH_MAX);

	     _folder_item_add(p, p->directory);

	     while (strlen(buf) > 1)
	       {
		  buf[PATH_MAX - 1] = 0;
		  dir = dirname(buf);
		  _folder_item_add(p, dir);
		  strncpy(buf, dir, PATH_MAX);
	       }

	     p->command = CMD_SHOW_PARENT;
	  }
	return 1;
     }
   else if (p->directory && input && !strncmp(input, ".", 1))
     {
	if (p->command != CMD_SHOW_HIDDEN)
	  {
	     _free_files(p);

	     p->show_hidden = EINA_TRUE;
	     _read_directory(p);

	     p->command = CMD_SHOW_HIDDEN;

	     return 0;
	  }
	p->input = eina_stringshare_add(input);
     }
   else if (p->command)
     {
	/* clear command items */
	EINA_LIST_FREE(p->files, file)
	  evry_item_free(EVRY_ITEM(file));

	if (p->command == CMD_SHOW_ROOT)
	  {
	     if (p->directory)
	       eina_stringshare_del(p->directory);

	     p->directory = eina_stringshare_add(e_user_homedir_get());
	  }

	p->show_hidden = EINA_FALSE;
	_read_directory(p);

	p->command = CMD_NONE;
     }

   if (input && !p->command)
     p->input = eina_stringshare_add(input);

   if ((p->command) || (!p->min_query) || (len >= p->min_query))
     _append_files(p);

   return !!(EVRY_PLUGIN(p)->items);
}

/***************************************************************************/
/* recent files */

static int
_recentf_append_files(Plugin *p)
{
   int match;
   int cnt = 0;
   Evry_Item *it;
   Eina_List *l;

   EVRY_PLUGIN_ITEMS_CLEAR(p);

   EINA_LIST_FOREACH(p->files, l, it)
     {
	if (cnt >= MAX_SHOWN) break;

	if (p->dirs_only && !it->browseable)
	  continue;

	if (it->fuzzy_match < 0)
	  {
	     if ((match = evry_fuzzy_match(it->label, p->input)) ||
		 (match = evry_fuzzy_match(EVRY_FILE(it)->path, p->input)))
	       it->fuzzy_match = match;
	     else
	       it->fuzzy_match = 0;
	  }

	if (_conf->show_recent || it->fuzzy_match)
	  {
	     if (!it->browseable)
	       it->priority = 1;
	     EVRY_PLUGIN_ITEM_APPEND(p, it);
	     cnt++;
	  }
     }

   p->files = eina_list_sort(p->files, -1, _cb_sort);

   return cnt;
}

/* use thread only to not block ui for ecore_file_exists ... */
static void
_recentf_func(void *data)
{
   Data *d = data;
   Plugin *p = d->plugin;
   Eina_List *l;
   Evry_Item_File *file;
   History_Item *hi;

   EINA_LIST_FOREACH(d->files, l, file)
     {
	if ((!evry_file_path_get(file)) ||
	    (!ecore_file_exists(file->path)))
	  {
	     EVRY_ITEM(file)->hi->last_used -= ONE_DAY;
	     EVRY_ITEM(file)->hi = NULL;
	  }
     }
}

static void
_recentf_cancel_func(void *data)
{
   Data *d = data;
   Plugin *p = d->plugin;
   Evry_Item_File *file;

   EINA_LIST_FREE(d->files, file)
     {
	EVRY_ITEM_FREE(file);
     }

   E_FREE(d);

   if (p->wait_finish)
     E_FREE(p);
}

static void
_recentf_end_func(void *data)
{
   Data *d = data;
   Plugin *p = d->plugin;
   Eina_List *l, *ll;
   Evry_Item *it;
   const char *label;

   EINA_LIST_FREE(d->files, it)
     {
	GET_FILE(file, it);

	if (!it->hi)
	  {
	     evry_item_free(it);
	     continue;
	  }

	_item_fill(file);

	it->hi->data = eina_stringshare_ref(file->mime);

	p->files = eina_list_append(p->files, it);
     }

   _recentf_append_files(p);

   evry_plugin_async_update(EVRY_PLUGIN(p), EVRY_ASYNC_UPDATE_ADD);

   p->thread = NULL;
   E_FREE(d);
}

static Eina_Bool
_recentf_items_add_cb(const Eina_Hash *hash, const void *key, void *data, void *fdata)
{
   History_Entry *he = data;
   History_Item *hi = NULL, *hi2;
   Eina_List *l, *ll;
   Evry_Item_File *file;
   double last_used = 0.0;
   Data *d = fdata;
   Plugin *p = d->plugin;
   const char *label;
   const char *path;
   int match = 0;

   EINA_LIST_FOREACH(he->items, l, hi2)
     {
	if (hi2->last_used > last_used)
	  hi = hi2;
     }

   if (!hi)
     return EINA_TRUE;

   if (clear_cache)
     {
	DBG("clear %s", (char *)key);

	/* transient marks them for deletion */
	if ((hi->count == 1) && (hi->last_used < SIX_DAYS_AGO))
	  {
	     hi->usage = 0;
	     hi->transient = 1;
	  }

	return EINA_TRUE;
     }

   if (hi->transient)
     return EINA_TRUE;

   if (!_conf->search_cache)
     {
	if ((hi->count == 1) && (hi->last_used < SIX_DAYS_AGO))
	  return EINA_TRUE;
     }

   path = (const char *) key;

   if (!(label = ecore_file_file_get(path)))
     return EINA_TRUE;

   path = eina_stringshare_add(path);

   EINA_LIST_FOREACH(p->files, ll, file)
     {
	if (file->path == path)
	  {
	     DBG("already added %s", path);
	     eina_stringshare_del(path);
	     EVRY_ITEM(file)->fuzzy_match = -1;
	     return EINA_TRUE;
	  }
     }

   /* searching subdirs */
   if (p->directory && strncmp(path, p->directory, strlen(p->directory)))
     {
	DBG("not in dir %s", path);
	eina_stringshare_del(path);
	return EINA_TRUE;
     }

   if (!(match = evry_fuzzy_match(label, p->input)) &&
       !(match = evry_fuzzy_match(path, p->input)))
     {
	DBG("not matched %s", path);
	eina_stringshare_del(path);
	return EINA_TRUE;
     }

   DBG("add %s", path);

   file = EVRY_ITEM_NEW(Evry_Item_File, p, label, NULL, _item_free);

   file->path = path;

   if (hi->data)
     file->mime = eina_stringshare_ref(hi->data);

   EVRY_ITEM(file)->hi = hi;
   EVRY_ITEM(file)->fuzzy_match = match;
   EVRY_ITEM(file)->id = eina_stringshare_ref(file->path);

   d->files = eina_list_append(d->files, file);

   return EINA_TRUE;
}

static Evry_Plugin *
_recentf_browse(Evry_Plugin *plugin, const Evry_Item *it)
{
   Plugin *p = NULL;

   if (!it || (it && !CHECK_TYPE(it, EVRY_TYPE_FILE)))
     return NULL;

   GET_PLUGIN(parent, plugin);
   GET_FILE(file, it);

   if (!evry_file_path_get(file) ||
       !ecore_file_is_dir(file->path))
     return NULL;

   p = E_NEW(Plugin, 1);
   p->base = *plugin;
   p->base.items = NULL;

   p->directory = eina_stringshare_add(file->path);
   p->parent = EINA_TRUE;

   return EVRY_PLUGIN(p);
}

static Evry_Plugin *
_recentf_begin(Evry_Plugin *plugin, const Evry_Item *it)
{
   Plugin *p;

   if (it && !CHECK_TYPE(it, EVRY_TYPE_ACTION))
     return NULL;

   p = E_NEW(Plugin, 1);
   p->base = *plugin;
   p->base.items = NULL;
   p->parent = EINA_FALSE;

   if (it)
     {
	/* provide object */
     }
   else
     {
	/* provide subject */
	p->min_query = plugin->config->min_query;

	if (clear_cache)
	  {
	     History_Types *ht = evry_history_types_get(evry_hist->subjects, EVRY_TYPE_FILE);
	     if (ht)
	       eina_hash_foreach(ht->types, _recentf_items_add_cb, p);

	     clear_cache = EINA_FALSE;
	  }
     }

   return EVRY_PLUGIN(p);
}

static int
_recentf_fetch(Evry_Plugin *plugin, const char *input)
{
   GET_PLUGIN(p, plugin);
   Evry_Item_File *file;
   History_Types *ht;
   int len = (input ? strlen(input) : 0);

   EVRY_PLUGIN_ITEMS_CLEAR(p);

   IF_RELEASE(p->input);

   if (len >= plugin->config->min_query)
     {
	Eina_List *l;
	Evry_Item *it;

	if (input)
	  p->input = eina_stringshare_add(input);

	if ((ht = evry_history_types_get(evry_hist->subjects, EVRY_TYPE_FILE)))
	  {
	     Data *d = E_NEW(Data, 1);
	     d->plugin = p;
	     eina_hash_foreach(ht->types, _recentf_items_add_cb, d);
	     p->thread = ecore_thread_run(_recentf_func, _recentf_end_func,
					  _recentf_cancel_func, d);
	  }

	EINA_LIST_FOREACH(p->files, l, it)
	  it->fuzzy_match = -1;

	return _recentf_append_files(p);
     }

   return 0;
}

/***************************************************************************/
/* actions */

static int
_open_folder_check(Evry_Action *act __UNUSED__, const Evry_Item *it)
{
   return (it->browseable && e_action_find("fileman"));
}<commit_msg>more work on recent-files<commit_after>/*
 * vim:ts=8:sw=3:sts=8:noexpandtab:cino=>5n-3f0^-2{2
 */

/* TODO option for maximum items to cache */

#include "Evry.h"
#include "e_mod_main.h"
#include <Efreet_Trash.h>

#define MAX_ITEMS 10
#define MAX_SHOWN 300
#define TERM_ACTION_DIR "%s"

#define CMD_NONE        0
#define CMD_SHOW_ROOT   1
#define CMD_SHOW_HIDDEN 2
#define CMD_SHOW_PARENT 3

#define ACT_TRASH	1
#define ACT_DELETE	2
#define ACT_COPY	3
#define ACT_MOVE	4

#define ONE_DAY  86400.0
#define SIX_DAYS_AGO (ecore_time_get() - 518400.0)
#define TIME_FACTOR(_now) (1.0 - (evry_hist->begin / _now)) / 1000000000000000.0

/* #undef DBG
 * #define DBG(...) ERR(__VA_ARGS__) */

typedef struct _Plugin Plugin;
typedef struct _Data Data;
typedef struct _Module_Config Module_Config;

struct _Plugin
{
  Evry_Plugin base;

  Eina_List *files;
  const char *directory;
  const char *input;
  unsigned int command;
  unsigned int min_query;
  Eina_Bool parent;
  Eina_Bool show_hidden;
  Eina_Bool dirs_only;
  Eina_Bool show_recent;

  Ecore_Thread *thread;
  Ecore_File_Monitor *dir_mon;
  int wait_finish;
};

struct _Data
{
  Plugin *plugin;
  char *directory;
  long id;
  int level;
  int cnt;
  Eina_List *files;
  Eina_List *list;
  DIR *dirp;
  int run_cnt;
};

struct _Module_Config
{
  int version;

  unsigned char show_homedir;
  unsigned char show_recent;
  unsigned char search_recent;
  unsigned char cache_dirs;
  unsigned char search_cache;

  // TODO
  int sort_by;
  Eina_List *search_dirs;

  E_Config_Dialog *cfd;
  E_Module *module;
};

static Module_Config *_conf;
static char _module_icon[] = "system-file-manager";
static Eina_List *_plugins = NULL;
static Eina_List *_actions = NULL;
static const char *_mime_dir;
static const char *_mime_mount;
static Eina_Bool clear_cache = EINA_FALSE;

/***************************************************************************/

static void
_item_fill(Evry_Item_File *file)
{
   if (!file->mime)
     {
	const char *mime = efreet_mime_type_get(file->path);

	if (mime)
	  file->mime = eina_stringshare_ref(mime);
	else
	  file->mime = eina_stringshare_add("unknown");
     }

   if ((file->mime == _mime_dir) ||
       (file->mime == _mime_mount))
     EVRY_ITEM(file)->browseable = EINA_TRUE;

   EVRY_ITEM(file)->context = eina_stringshare_ref(file->mime);

   if (!EVRY_ITEM(file)->detail)
     evry_util_file_detail_set(file);


     evry_util_file_detail_set(file);
}

static int
_cb_sort(const void *data1, const void *data2)
{
   const Evry_Item *it1 = data1;
   const Evry_Item *it2 = data2;

   if (it1->browseable && !it2->browseable)
     return -1;

   if (!it1->browseable && it2->browseable)
     return 1;

   if (it1->fuzzy_match && it2->fuzzy_match)
     if (it1->fuzzy_match - it2->fuzzy_match)
       return (it1->fuzzy_match - it2->fuzzy_match);

   return strcasecmp(it1->label, it2->label);
}

static void
_item_free(Evry_Item *it)
{
   GET_FILE(file, it);
   if (file->url) eina_stringshare_del(file->url);
   if (file->path) eina_stringshare_del(file->path);
   if (file->mime) eina_stringshare_del(file->mime);

   E_FREE(file);
}

static void
_scan_func(void *data)
{
   Data *d = data;
   Plugin *p = d->plugin;
   char *filename;
   struct dirent *dp;
   Evry_Item_File *file;
   char buf[4096];
   Eina_Bool is_dir;

   d->dirp = opendir(d->directory);
   if (!d->dirp) return;

   while ((dp = readdir(d->dirp)))
     {
	if ((dp->d_name[0] == '.') &&
	    ((dp->d_name[1] == '\0') ||
	     ((dp->d_name[1] == '.') &&
	      (dp->d_name[2] == '\0'))))
	  continue;

#ifdef _DIRENT_HAVE_D_TYPE
	if (dp->d_type == DT_FIFO ||
	    dp->d_type == DT_BLK  ||
	    dp->d_type == DT_CHR  ||
	    dp->d_type == DT_SOCK)
	  continue;
#endif
	if (!p->show_hidden)
	  {
	     if (dp->d_name[0] == '.')
	       continue;
	  }
	else
	  {
	     if (dp->d_name[0] != '.')
	       continue;
	  }

	is_dir = EINA_FALSE;

#ifdef _DIRENT_HAVE_D_TYPE
	if (dp->d_type & DT_UNKNOWN)
	  {
#endif
	     if (ecore_file_is_dir(file->path))
	       is_dir = EINA_TRUE;

#ifdef _DIRENT_HAVE_D_TYPE
	  }
	else if (dp->d_type & DT_DIR)
	  {
	     is_dir = EINA_TRUE;
	  }
#endif

	if (p->dirs_only && !is_dir)
	  continue;

	file = EVRY_ITEM_NEW(Evry_Item_File, p, NULL, NULL, _item_free);

	filename = strdup(dp->d_name);
	EVRY_ITEM(file)->label = filename;
	EVRY_ITEM(file)->browseable = is_dir;
	d->files = eina_list_append(d->files, file);

	snprintf(buf, sizeof(buf), "%s/%s", d->directory, filename);
	file->path = strdup(buf);
     }
   closedir(d->dirp);
}

static void
_scan_mime_func(void *data)
{
   Data *d = data;
   Plugin *p = d->plugin;
   Evry_Item_File *file;
   Eina_List *l;
   const char *mime;
   int cnt = 0;

   EINA_LIST_FOREACH(d->files, l, file)
     {
	if (!EVRY_ITEM(file)->browseable)
	  {
	     if ((mime = efreet_mime_type_get(file->path)))
	       {
		  file->mime = mime;

		  if (!strncmp(file->mime, "inode/", 6) &&
		      ecore_file_is_dir(file->path))
		    EVRY_ITEM(file)->browseable = EINA_TRUE;
	       }
	     else
	       {
		  file->mime = "unknown";
	       }
	  }
	if (cnt++ > MAX_ITEMS * d->run_cnt) break;
     }
}

static int
_append_files(Plugin *p)
{
   int match;
   int cnt = 0;
   Evry_Item *it;
   Eina_List *l;

   EVRY_PLUGIN_ITEMS_CLEAR(p);

   if (!p->command && p->min_query)
     {
	if (!p->input)
	  return 0;
	if (strlen(p->input) < p->min_query)
	  return 0;
     }

   EINA_LIST_FOREACH(p->files, l, it)
     {
	if (cnt >= MAX_SHOWN) break;

	if (p->dirs_only && !it->browseable)
	  continue;

	if (p->input && (match = evry_fuzzy_match(it->label, p->input)))
	  {
	     it->fuzzy_match = match;
	     if (!it->browseable)
	       it->priority = 1;
	     EVRY_PLUGIN_ITEM_APPEND(p, it);
	     cnt++;
	  }
	else if (!p->input)
	  {
	     if (!it->browseable)
	       it->priority = 1;
	     EVRY_PLUGIN_ITEM_APPEND(p, it);
	     cnt++;
	  }
     }
   return cnt;
}

static void
_scan_cancel_func(void *data)
{
   Data *d = data;
   Plugin *p = d->plugin;
   Evry_Item_File *file;
   Evry_Item *item;

   if (!d->run_cnt)
     {
	EINA_LIST_FREE(d->files, file)
	  {
	     if (file->base.label) free((char *)(file->base.label));
	     if (file->path) free((char *)file->path);
	     free(file);
	  }
     }
   else
     {
	EINA_LIST_FREE(d->files, file)
	  {
	     if (file->path)
	       eina_stringshare_del(file->path);
	     if (EVRY_ITEM(file)->label)
	       eina_stringshare_del(EVRY_ITEM(file)->label);
	     if (EVRY_ITEM(file)->detail)
	       eina_stringshare_del(EVRY_ITEM(file)->detail);
	     free(file);
	  }
     }

   free(d->directory);
   E_FREE(d);

   p->thread = NULL;

   if (p->wait_finish)
     E_FREE(p);
}

static void
_scan_end_func(void *data)
{
   Data *d = data;
   Plugin *p = d->plugin;
   Evry_Item *item;
   Evry_Item_File *f;
   char *filename, *path, *mime;
   Eina_List *l, *ll, *lll;
   History_Item *hi;
   History_Entry *he;
   History_Types *ht = NULL;
   int cnt = 0;

   if (_conf->cache_dirs)
     ht = evry_history_types_get(evry_hist->subjects, EVRY_TYPE_FILE);

   if (!d->run_cnt)
     {
	EINA_LIST_FOREACH_SAFE(d->files, l, ll, item)
	  {
	     GET_FILE(file, item);

	     filename = (char *)item->label;
	     path = (char *) file->path;

	     if (!filename || !path)
	       goto next;

	     file->path = eina_stringshare_add(path);

	     if (item->browseable)
	       file->mime = eina_stringshare_ref(_mime_dir);

	     /* check if we can grab the mimetype from history */
	     if ((_conf->cache_dirs && ht) &&
		 (he = eina_hash_find(ht->types, file->path)))
	       {
		  EINA_LIST_FOREACH(he->items, lll, hi)
		    {
		       if (hi->data)
			 {
			    if (!file->mime)
			      file->mime = eina_stringshare_ref(hi->data);

			    /* DBG("cached: %s %s", file->mime, file->path); */
			    hi->transient = 0;
			    break;
			 }
		    }
	       }

	     if (file->mime)
	       {
		  item->context = eina_stringshare_ref(file->mime);

		  d->files = eina_list_remove_list(d->files, l);
		  p->files = eina_list_append(p->files, file);
	       }

	     item->id = eina_stringshare_ref(file->path);
	     item->label = eina_stringshare_add(filename);
	     evry_util_file_detail_set(file);

	     E_FREE(filename);
	     E_FREE(path);
	     continue;

	  next:
	     E_FREE(filename);
	     E_FREE(path);
	     if (file->path) eina_stringshare_del(file->path);
	     d->files = eina_list_remove(d->files, file);
	     E_FREE(file);
	  }

	if (d->files)
	  {
	     d->run_cnt++;
	     d->files = eina_list_sort(d->files, -1, _cb_sort);
	     p->thread = ecore_thread_run(_scan_mime_func,
					  _scan_end_func,
					  _scan_cancel_func, d);
	     return;
	  }
     }
   else
     {
	EINA_LIST_FREE(d->files, item)
	  {
	     GET_FILE(file, item);

	     mime = (char *) file->mime;

	     if (!mime) break;

	     file->mime = eina_stringshare_ref(mime);
	     item->context = eina_stringshare_ref(mime);

	     p->files = eina_list_append(p->files, file);
	  }

	if (d->files)
	  {
	     d->run_cnt++;
	     p->thread = ecore_thread_run(_scan_mime_func,
					  _scan_end_func,
					  _scan_cancel_func, d);
	  }
     }

   if (!d->files)
     {
	if (!(p->command == CMD_SHOW_HIDDEN) &&  _conf->cache_dirs)
	  {
	     EINA_LIST_FOREACH(p->files, l, item)
	       {
		  GET_FILE(file, item);

		  if (!item->usage &&
		      (hi = evry_history_add(evry_hist->subjects, item, NULL, NULL)))
		    {
		       hi->last_used = SIX_DAYS_AGO - (0.001 * (double) cnt++);
		       hi->usage = TIME_FACTOR(hi->last_used);
		       hi->data = eina_stringshare_ref(file->mime);
		       item->hi = hi;
		    }
		  else if (item->hi && (item->hi->count == 1) &&
			   (item->hi->last_used < SIX_DAYS_AGO))
		    {
		       item->hi->last_used = SIX_DAYS_AGO - (0.001 * (double) cnt++);
		       item->hi->usage = TIME_FACTOR(hi->last_used);
		    }
	       }
	  }

	free(d->directory);
	E_FREE(d);
	p->thread = NULL;
     }

   p->files = eina_list_sort(p->files, -1, _cb_sort);

   _append_files(p);

   evry_plugin_async_update(EVRY_PLUGIN(p), EVRY_ASYNC_UPDATE_ADD);
}

static void
_dir_watcher(void *data, Ecore_File_Monitor *em, Ecore_File_Event event, const char *path)
{
   Plugin *p = data;
   Evry_Item_File *file;
   const char *label;
   Eina_List *ll, *l;

   switch (event)
     {
      case ECORE_FILE_EVENT_DELETED_SELF:
	 EINA_LIST_FREE(p->files, file)
	  evry_item_free(EVRY_ITEM(file));
	break;

      case ECORE_FILE_EVENT_CREATED_DIRECTORY:
      case ECORE_FILE_EVENT_CREATED_FILE:
	 /* DBG("added %s", path); */

	 label = ecore_file_file_get(path);

	 file = EVRY_ITEM_NEW(Evry_Item_File, p, label, NULL, _item_free);
	 file->path = eina_stringshare_add(path);

	 if (event == ECORE_FILE_EVENT_CREATED_DIRECTORY)
	   file->mime = eina_stringshare_ref(_mime_dir);

	 _item_fill(file);

	 p->files = eina_list_append(p->files, file);

	 break;

      case ECORE_FILE_EVENT_DELETED_FILE:
      case ECORE_FILE_EVENT_DELETED_DIRECTORY:
	 label = eina_stringshare_add(path);
	 /* DBG("delete %s", path); */

	 EINA_LIST_FOREACH_SAFE(p->files, l, ll, file)
	   {
	      if (file->path != label) continue;

	      p->files = eina_list_remove_list(p->files, l);

	      EVRY_ITEM_FREE(file);
	      break;
	   }
	 eina_stringshare_del(label);
	 break;

      default:
	 return;
     }

   _append_files(p);

   evry_plugin_async_update(EVRY_PLUGIN(p), EVRY_ASYNC_UPDATE_ADD);
}

static void
_read_directory(Plugin *p)
{
   Data *d = E_NEW(Data, 1);
   d->plugin = p;
   d->directory = strdup(p->directory);

   p->thread = ecore_thread_run(_scan_func, _scan_end_func, _scan_cancel_func, d);

   if (p->dir_mon)
     ecore_file_monitor_del(p->dir_mon);

   p->dir_mon = ecore_file_monitor_add(p->directory, _dir_watcher, p);
}

static Evry_Plugin *
_browse(Evry_Plugin *plugin, const Evry_Item *it)
{
   Plugin *p = NULL;

   if (!it || (it && !CHECK_TYPE(it, EVRY_TYPE_FILE)))
     return NULL;

   GET_PLUGIN(parent, plugin);
   GET_FILE(file, it);

   if (!evry_file_path_get(file) ||
       !ecore_file_is_dir(file->path))
     return NULL;

   p = E_NEW(Plugin, 1);
   p->base = *plugin;
   p->base.items = NULL;

   p->directory = eina_stringshare_add(file->path);
   p->parent = EINA_TRUE;

   _read_directory(p);

   return EVRY_PLUGIN(p);
}

static Evry_Plugin *
_begin(Evry_Plugin *plugin, const Evry_Item *it)
{
   Plugin *p = NULL;

   if (it && !CHECK_TYPE(it, EVRY_TYPE_ACTION))
     return NULL;

   p = E_NEW(Plugin, 1);
   p->base = *plugin;
   p->base.items = NULL;

   if (it)
     {
	/* provide object */

	p->directory = eina_stringshare_add(e_user_homedir_get());
	p->parent = EINA_FALSE;
	_read_directory(p);
     }
   else
     {
	/* provide subject */
	p->min_query = plugin->config->min_query;

	if (_conf->show_homedir)
	  p->directory = eina_stringshare_add(e_user_homedir_get());

	_read_directory(p);

	return EVRY_PLUGIN(p);
     }

   return EVRY_PLUGIN(p);
}

static void
_folder_item_add(Plugin *p, const char *path)
{
   Evry_Item_File *file;

   file = EVRY_ITEM_NEW(Evry_Item_File, p, path, NULL, _item_free);
   file->path = eina_stringshare_add(path);
   file->mime = eina_stringshare_ref(_mime_dir);
   EVRY_ITEM(file)->browseable = EINA_TRUE;
   p->files = eina_list_append(p->files, file);
   EVRY_PLUGIN_ITEM_APPEND(p, file);
}

static void
_free_files(Plugin *p)
{
   Evry_Item_File *file;

   if (p->thread)
     ecore_thread_cancel(p->thread);
   p->thread = NULL;

   EINA_LIST_FREE(p->files, file)
     EVRY_ITEM_FREE(file);

   if (p->dir_mon)
     ecore_file_monitor_del(p->dir_mon);
   p->dir_mon = NULL;
}

static void
_finish(Evry_Plugin *plugin)
{
   GET_PLUGIN(p, plugin);

   IF_RELEASE(p->input);
   IF_RELEASE(p->directory);

   if (p->thread)
     {
	ecore_thread_cancel(p->thread);
	p->wait_finish = 1;
	p->thread = NULL;
     }

   EVRY_PLUGIN_ITEMS_CLEAR(p);

   _free_files(p);

   if (!p->wait_finish)
     E_FREE(p);
}

static int
_fetch(Evry_Plugin *plugin, const char *input)
{
   GET_PLUGIN(p, plugin);
   Evry_Item_File *file;
   int len = (input ? strlen(input) : 0);

   if (!p->command)
     EVRY_PLUGIN_ITEMS_CLEAR(p);

   IF_RELEASE(p->input);

   if (!p->parent && input && !strncmp(input, "/", 1))
     {
	char *path = NULL;

	if ((p->command != CMD_SHOW_ROOT) ||
	    ((ecore_file_is_dir(input) ? (path = strdup(input)) : 0) ||
	     ((path = ecore_file_dir_get(input)) &&
	      (strcmp(p->directory, path)))))
	  {
	     _free_files(p);

	     eina_stringshare_del(p->directory);

	     if (path)
	       {
		  p->directory = eina_stringshare_add(path);
		  free(path);
	       }
	     else
	       {
		  p->directory = eina_stringshare_add("/");
	       }

	     _read_directory(p);

	     p->command = CMD_SHOW_ROOT;

	     return 0;
	  }
	int len = strlen(p->directory);
	len = (len == 1) ? len : len+1;

	p->input = eina_stringshare_add(input + len);
     }
   else if (p->directory && input && !strncmp(input, "..", 2))
     {
	if (p->command != CMD_SHOW_PARENT)
	  {
	     char *dir;
	     char buf[PATH_MAX];

	     if (strncmp(p->directory, "/", 1))
	       return 0;

	     _free_files(p);

	     strncpy(buf, p->directory, PATH_MAX);

	     _folder_item_add(p, p->directory);

	     while (strlen(buf) > 1)
	       {
		  buf[PATH_MAX - 1] = 0;
		  dir = dirname(buf);
		  _folder_item_add(p, dir);
		  strncpy(buf, dir, PATH_MAX);
	       }

	     p->command = CMD_SHOW_PARENT;
	  }
	return 1;
     }
   else if (p->directory && input && !strncmp(input, ".", 1))
     {
	if (p->command != CMD_SHOW_HIDDEN)
	  {
	     _free_files(p);

	     p->show_hidden = EINA_TRUE;
	     _read_directory(p);

	     p->command = CMD_SHOW_HIDDEN;

	     return 0;
	  }
	p->input = eina_stringshare_add(input);
     }
   else if (p->command)
     {
	/* clear command items */
	EINA_LIST_FREE(p->files, file)
	  evry_item_free(EVRY_ITEM(file));

	if (p->command == CMD_SHOW_ROOT)
	  {
	     if (p->directory)
	       eina_stringshare_del(p->directory);

	     p->directory = eina_stringshare_add(e_user_homedir_get());
	  }

	p->show_hidden = EINA_FALSE;
	_read_directory(p);

	p->command = CMD_NONE;
     }

   if (input && !p->command)
     p->input = eina_stringshare_add(input);

   if ((p->command) || (!p->min_query) || (len >= p->min_query))
     _append_files(p);

   return !!(EVRY_PLUGIN(p)->items);
}

/***************************************************************************/
/* recent files */

static int
_cb_sort_recent(const void *data1, const void *data2)
{
   const Evry_Item *it1 = data1;
   const Evry_Item *it2 = data2;

   if (it1->browseable && !it2->browseable)
     return -1;

   if (!it1->browseable && it2->browseable)
     return 1;

   if (it1->hi && it2->hi)
     return (it1->hi->last_used > it2->hi->last_used ? -1 : 1);

   if (it1->fuzzy_match && it2->fuzzy_match)
     if (it1->fuzzy_match - it2->fuzzy_match)
       return (it1->fuzzy_match - it2->fuzzy_match);

   return strcasecmp(it1->label, it2->label);
}

static int
_recentf_append_files(Plugin *p)
{
   int match;
   int cnt = 0;
   Evry_Item *it;
   Eina_List *l, *new = NULL;

   EVRY_PLUGIN_ITEMS_CLEAR(p);

   EINA_LIST_FOREACH(p->files, l, it)
     {
	if (p->dirs_only && !it->browseable)
	  continue;

	if (it->fuzzy_match <= 0)
	  {
	     if ((match = evry_fuzzy_match(it->label, p->input)) ||
		 (match = evry_fuzzy_match(EVRY_FILE(it)->path, p->input)))
	       it->fuzzy_match = match;
	     else
	       it->fuzzy_match = 0;

	     DBG("check match %d %s", it->fuzzy_match, it->label);
	  }

	if (_conf->show_recent || it->fuzzy_match)
	  {
	     if (!it->browseable)
	       it->priority = 1;
	     new = eina_list_append(new, it);
	  }
     }

   new = eina_list_sort(new, -1, _cb_sort_recent);

   EINA_LIST_FREE(new, it)
     {
	if (cnt++ < MAX_SHOWN);
	EVRY_PLUGIN_ITEM_APPEND(p, it);
     }

   return cnt;
}

/* use thread only to not block ui for ecore_file_exists ... */
static void
_recentf_func(void *data)
{
   Data *d = data;
   Plugin *p = d->plugin;
   Eina_List *l;
   Evry_Item_File *file;
   History_Item *hi;

   EINA_LIST_FOREACH(d->files, l, file)
     {
	if ((!evry_file_path_get(file)) ||
	    (!ecore_file_exists(file->path)))
	  {
	     EVRY_ITEM(file)->hi->last_used -= ONE_DAY;
	     EVRY_ITEM(file)->hi = NULL;
	  }
     }
}

static void
_recentf_cancel_func(void *data)
{
   Data *d = data;
   Plugin *p = d->plugin;
   Evry_Item_File *file;

   EINA_LIST_FREE(d->files, file)
     {
	EVRY_ITEM_FREE(file);
     }

   E_FREE(d);

   if (p->wait_finish)
     E_FREE(p);
}

static void
_recentf_end_func(void *data)
{
   Data *d = data;
   Plugin *p = d->plugin;
   Eina_List *l, *ll;
   Evry_Item *it;
   const char *label;

   EINA_LIST_FREE(d->files, it)
     {
	GET_FILE(file, it);

	if (!it->hi)
	  {
	     evry_item_free(it);
	     continue;
	  }

	_item_fill(file);

	it->hi->data = eina_stringshare_ref(file->mime);

	p->files = eina_list_append(p->files, it);
     }

   _recentf_append_files(p);

   evry_plugin_async_update(EVRY_PLUGIN(p), EVRY_ASYNC_UPDATE_ADD);

   p->thread = NULL;
   E_FREE(d);
}

static Eina_Bool
_recentf_items_add_cb(const Eina_Hash *hash, const void *key, void *data, void *fdata)
{
   History_Entry *he = data;
   History_Item *hi = NULL, *hi2;
   Eina_List *l, *ll;
   Evry_Item_File *file;
   double last_used = 0.0;
   Data *d = fdata;
   Plugin *p = d->plugin;
   const char *label;
   const char *path;
   int match = 0;

   EINA_LIST_FOREACH(he->items, l, hi2)
     {
	if (hi2->last_used > last_used)
	  hi = hi2;
     }

   if (!hi)
     return EINA_TRUE;

   if (clear_cache)
     {
	DBG("clear %s", (char *)key);

	/* transient marks them for deletion */
	if ((hi->count == 1) && (hi->last_used < SIX_DAYS_AGO))
	  {
	     hi->usage = 0;
	     hi->transient = 1;
	  }

	return EINA_TRUE;
     }

   if (hi->transient)
     return EINA_TRUE;

   if (!_conf->search_cache)
     {
	if ((hi->count == 1) && (hi->last_used < SIX_DAYS_AGO))
	  return EINA_TRUE;
     }

   path = (const char *) key;

   if (!(label = ecore_file_file_get(path)))
     return EINA_TRUE;

   path = eina_stringshare_add(path);

   EINA_LIST_FOREACH(p->files, ll, file)
     {
	if (file->path == path)
	  {
	     /* DBG("already added %s", path); */
	     eina_stringshare_del(path);
	     EVRY_ITEM(file)->fuzzy_match = -1;
	     return EINA_TRUE;
	  }
     }

   /* searching subdirs */
   if (p->directory && strncmp(path, p->directory, strlen(p->directory)))
     {
	/* DBG("not in dir %s", path); */
	eina_stringshare_del(path);
	return EINA_TRUE;
     }

   if (!(match = evry_fuzzy_match(label, p->input)) &&
       !(match = evry_fuzzy_match(path, p->input)))
     {
	/* DBG("not matched %s", path); */
	eina_stringshare_del(path);
	return EINA_TRUE;
     }

   DBG("add %s", path);

   file = EVRY_ITEM_NEW(Evry_Item_File, p, label, NULL, _item_free);

   file->path = path;

   if (hi->data)
     file->mime = eina_stringshare_ref(hi->data);

   EVRY_ITEM(file)->hi = hi;
   EVRY_ITEM(file)->fuzzy_match = match;
   EVRY_ITEM(file)->id = eina_stringshare_ref(file->path);

   d->files = eina_list_append(d->files, file);

   return EINA_TRUE;
}

static Evry_Plugin *
_recentf_browse(Evry_Plugin *plugin, const Evry_Item *it)
{
   Plugin *p = NULL;

   if (!it || (it && !CHECK_TYPE(it, EVRY_TYPE_FILE)))
     return NULL;

   GET_PLUGIN(parent, plugin);
   GET_FILE(file, it);

   if (!evry_file_path_get(file) ||
       !ecore_file_is_dir(file->path))
     return NULL;

   p = E_NEW(Plugin, 1);
   p->base = *plugin;
   p->base.items = NULL;

   p->directory = eina_stringshare_add(file->path);
   p->parent = EINA_TRUE;

   return EVRY_PLUGIN(p);
}

static Evry_Plugin *
_recentf_begin(Evry_Plugin *plugin, const Evry_Item *it)
{
   Plugin *p;

   if (it && !CHECK_TYPE(it, EVRY_TYPE_ACTION))
     return NULL;

   p = E_NEW(Plugin, 1);
   p->base = *plugin;
   p->base.items = NULL;
   p->parent = EINA_FALSE;

   if (it)
     {
	/* provide object */
     }
   else
     {
	/* provide subject */
	p->min_query = plugin->config->min_query;

	if (clear_cache)
	  {
	     History_Types *ht = evry_history_types_get(evry_hist->subjects, EVRY_TYPE_FILE);
	     if (ht)
	       eina_hash_foreach(ht->types, _recentf_items_add_cb, p);

	     clear_cache = EINA_FALSE;
	  }
     }

   return EVRY_PLUGIN(p);
}

static int
_recentf_fetch(Evry_Plugin *plugin, const char *input)
{
   GET_PLUGIN(p, plugin);
   Evry_Item_File *file;
   History_Types *ht;
   int len = (input ? strlen(input) : 0);

   IF_RELEASE(p->input);

   if (p->thread)
     ecore_thread_cancel(p->thread);
   p->thread = NULL;

   if (input && isspace(input[len - 1]))
     return !!(plugin->items);

   if (len >= plugin->config->min_query)
     {
	Eina_List *l;
	Evry_Item *it;

	if (input)
	  p->input = eina_stringshare_add(input);

	EINA_LIST_FOREACH(p->files, l, it)
	  it->fuzzy_match = -1;

	if ((ht = evry_history_types_get(evry_hist->subjects, EVRY_TYPE_FILE)))
	  {
	     Data *d = E_NEW(Data, 1);
	     d->plugin = p;
	     eina_hash_foreach(ht->types, _recentf_items_add_cb, d);
	     p->thread = ecore_thread_run(_recentf_func, _recentf_end_func,
					  _recentf_cancel_func, d);
	  }
	return !!(plugin->items);
     }

   EVRY_PLUGIN_ITEMS_CLEAR(p);

   return 0;
}

/***************************************************************************/
/* actions */

static int
_open_folder_check(Evry_Action *act __UNUSED__, const Evry_Item *it)
{
   return (it->browseable && e_action_find("fileman"));
}
{'fraction_non_alphanumeric': 0.15785465622280243, 'fraction_numerical': 0.006984334203655352, 'mean_word_length': 2.966942862074918, 'pattern_counts': {'":': 0, '<': 16, '<?xml version=': 0, '>': 746, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4727736', 'n_tokens_mistral': 19911, 'n_tokens_neox': 18215, 'n_words': 3359}