Is there an indicator to quickly access recently used files?
I am looking for a tray icon indicator that - upon click on the icon - shows me the list of my recently used files. This would be a great way to quickly access these files.
Have your recently used files in the panel
With the script below, you can have an arbitrary number of recently used items at hand in the panel, e.g. 6 items:
...or 20 items:
...depending on your settings.
How it works
The setup exists of two items:
- an icon, named (exactly)
recent.png
- a script
Both need to be in one and the same folder. after that, simply run the script.
How to set up
Possibly, you need to install python3-gi
:
sudo apt-get install python3-gi
Then:
-
Copy the script below into an empty file, save it as
recused.py
#!/usr/bin/env python3 import signal import gi gi.require_version('Gtk', '3.0') gi.require_version('AppIndicator3', '0.1') from gi.repository import Gtk, AppIndicator3, GObject import time from threading import Thread import os import subprocess # --- set the number of recently used files to appear below n = 20 # --- home = os.environ["HOME"] recdata = os.path.join(home, ".local/share/recently-used.xbel") currpath = os.path.dirname(os.path.realpath(__file__)) class Indicator(): def __init__(self): self.app = 'show_recent' iconpath = os.path.join(currpath, "recent.png") self.indicator = AppIndicator3.Indicator.new( self.app, iconpath, AppIndicator3.IndicatorCategory.OTHER) self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) self.indicator.set_menu(self.create_menu()) # the thread: self.update = Thread(target=self.check_recent) # daemonize the thread to make the indicator stopable self.update.setDaemon(True) self.update.start() def get_files(self): # create the list of recently used files used = [l for l in open(recdata) if \ all([ '<bookmark href="file://' in l, not "/tmp" in l, "." in l, ])] relevant = [l.split('="') for l in set(used)] relevant = [[it[1][7:-7], it[-2][:-10]] for it in relevant] relevant.sort(key=lambda x: x[1]) return [item[0].replace("%20", " ") for item in relevant[::-1][:n]] def create_menu(self): # creates the (initial) menu self.menu = Gtk.Menu() # separator menu_sep = Gtk.SeparatorMenuItem() self.menu.append(menu_sep) # item_quit.show() self.menu.show_all() return self.menu def open_file(self, *args): # opens the file with the default application index = self.menu.get_children().index(self.menu.get_active()) selection = self.menu_items2[index] subprocess.Popen(["xdg-open", selection]) def set_new(self): # update the list, appearing in the menu for i in self.menu.get_children(): self.menu.remove(i) for file in self.menu_items2: sub = Gtk.MenuItem(file) self.menu.append(sub) sub.connect('activate', self.open_file) # separator menu_sep = Gtk.SeparatorMenuItem() self.menu.append(menu_sep) # quit item_quit = Gtk.MenuItem('Quit') item_quit.connect('activate', self.stop) self.menu.append(item_quit) self.menu.show_all() def check_recent(self): self.menu_items1 = [] while True: time.sleep(3) self.menu_items2 = self.get_files() if self.menu_items2 != self.menu_items1: GObject.idle_add( self.set_new, priority=GObject.PRIORITY_DEFAULT ) self.menu_items1 = self.menu_items2 def stop(self, source): Gtk.main_quit() Indicator() # this is where we call GObject.threads_init() GObject.threads_init() signal.signal(signal.SIGINT, signal.SIG_DFL) Gtk.main()
-
In the head section of the script, set the number of items to show:
# --- set the number of recently used files to appear below n = 20 # ---
-
In one and the same folder save the icon below as (exactly)
recent.png
(right-click on it -> save as)
-
Test- run the script by the command:
python3 /path/to/recused.py
-
If all works fine, add to Startup Applications: Dash > Startup Applications > Add. Add the command:
/bin/bash -c "sleep 15 && python3 /path/to/recused.py"
Notes
- This script was based on this previous answer, in case you are interested in an alternative, be it that that one was application-specific, and used a periodically updated
.desktop
file. - The script was tested on 16.04, but should work on earlier versions as well.
- Due to a bug in the
recently-used.xbel
file, sometimes a file is mentioned twice; once with, and once without extension. I solved that by filtering the latter out. The consequence is that files without extension won't appear in the list. If that is an issue, let me know, we can try to find another fix in that case. - If a file does not exist (anymore), the indicator simply (obviously) does not open the file. Most likely, I will polish the script a bit more to filter out those outdated entries from the
recently-used.xbel
file.
Explanation
Many applications, editing files and using a Gtk window, keep track of opened files in the file: ~/.local/share/recently-used.xbel
. "Records" include the date & time, the application and the file that was opened.
The script reads the .xbel
file, sorts the items by date/time, includes the first n- (depending on your settings) files in the menu of the indicator. The menu is updated (only if necessary) every 3 seconds as it is. Subsequently, when an item is selected, the file is opened with the command:
xdg-open <file>
Thus the selected file will be opened with the default application. It is very well possible to make sure the file is opened with the actual application it was last opened. That takes a somewhat more sophisticated parsing however. I will add that as an option to the planned ppa version on Launchpad.
The same goes for an option window to set a few options, like the number of files to show etc.
Note
The indicator is now merged with a few other things in this one.
Although you do not mention which Ubuntu flavour you are using, there is a nice app for Xubuntu. More specifically, it is part of Xfce, so you might still be able to use it in Ubuntu if you use Xfce. The app is called Places.
To enable, right-click on the panel, and from the menu select Panel -> Add New Items..., and there choose Places. This shows a list of folders and Recent Documents you just opened. See below:
You can configure it nicely too:
Here is the project's page.
If you are not using Xfce, you can install it very easily, as explained in this answer. Simply do sudo apt-get install xfce-panel
, and then run (see link). Notice that Xfce panel might not be compatible with all the other apps in your Unity (or other) panel. In my experience I have had no problem at all with it, so it might still be a good solution.
Not a tray icon but a quicklist script for the unity launcher (works with Umlauts), and you can pin files as well: Ubuntu-Recentquicklists
[detailed install instructions here]
Quick instructions:
- download .zip
- extract
- make
install.sh
andubuntu-recentquicklists.py
executable - run
install.sh
script
Features:
- automatically adds recent files
- manually add non-recent files / pin recent files
- specify how many items per list you want and
- how old the items may be
- resolve symbolic links
- display the full path (or just the filename, as in the screenshot)
Update Feb/24/2017: the indicator now has option to pin web links.
Introduction
Files Indicator presented below is simple indicator for accessing user's files and folders. It allows checking recently used files, bookmarking files and directories.
Update: indicator now also supports launching .desktop files that have been pinned. For instance if you have firefox.desktop pinned, it will launch firefox. Thus, the indicator can be used as quick launcher for programs. The feature is on the way into PPA at the time of writing (Nov 19, 7:53 pm GMT, should take about 24 hours to process), but is already on github as well as here, in updated source code.
Obtaining the indicator
The indicator is available from my personal PPA as well as GitHub. Use the following steps to obtain it:
sudo add-apt-repository ppa:1047481448-2/sergkolo
sudo apt-get update
sudo apt-get install files-indicator
Source code
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Author: Serg Kolo , contact: [email protected]
# Date: November 19 , 2016
# Purpose: appindicator for accessing files and folders
# Tested on: Ubuntu 16.04 LTS
#
#
# Licensed under The MIT License (MIT).
# See included LICENSE file or the notice below.
#
# Copyright © 2016 Sergiy Kolodyazhnyy
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import gi
gi.require_version('AppIndicator3', '0.1')
gi.require_version('Notify', '0.7')
from gi.repository import GLib as glib
from gi.repository import AppIndicator3 as appindicator
from gi.repository import Gtk as gtk
from gi.repository import Gio
from gi.repository import Notify
from collections import OrderedDict
# from collections import OrderedDict
import urllib.parse
import subprocess
import copy
import shutil
import dbus
import math
import json
import os
class FilesIndicator(object):
def __init__(self):
self.app = appindicator.Indicator.new(
'files-indicator', "document-open-recent",
appindicator.IndicatorCategory.HARDWARE
)
self.user_home = os.path.expanduser('~')
filename = '.pinned_files.json'
self.pinned_list = os.path.join(self.user_home,filename)
self.config = os.path.join(self.user_home,'.files_indicator.json')
self.max_items = 15
self.name_length = 20
self.read_config()
self.app.set_status(appindicator.IndicatorStatus.ACTIVE)
self.cached_files = self.get_recent_files()
self.make_menu()
self.update()
def read_config(self,*args):
config = {}
try:
with open(self.config) as f:
config = json.load(f)
except FileNotFoundError:
print('>>> ',self.config,' not found.Creating one')
f = open(self.config,'w')
config = {'max_items':self.max_items,
'name_length':self.name_length
}
json.dump(config,f,indent=4)
f.close()
except json.JSONDecodeError:
print(">>> Can't read ",self.pinned_list,',may be corrupt')
return None
else:
self.max_items = config['max_items']
self.name_length = config['name_length']
def add_menu_item(self, menu_obj, item_type, image, label, action, args):
""" dynamic function that can add menu items depending on
the item type and other arguments"""
menu_item, icon = None, None
if item_type is gtk.ImageMenuItem and label:
menu_item = gtk.ImageMenuItem.new_with_label(label)
menu_item.set_always_show_image(True)
if '/' in image:
icon = gtk.Image.new_from_file(image)
else:
icon = gtk.Image.new_from_icon_name(image, 48)
menu_item.set_image(icon)
elif item_type is gtk.ImageMenuItem and not label:
menu_item = gtk.ImageMenuItem()
menu_item.set_always_show_image(True)
if '/' in image:
icon = gtk.Image.new_from_file(image)
else:
icon = gtk.Image.new_from_icon_name(image, 16)
menu_item.set_image(icon)
elif item_type is gtk.MenuItem:
menu_item = gtk.MenuItem(label)
elif item_type is gtk.SeparatorMenuItem:
menu_item = gtk.SeparatorMenuItem()
if action:
menu_item.connect('activate', action, *args)
menu_obj.append(menu_item)
menu_item.show()
def get_user_dirs(self,*args):
user_dirs = []
for index,val in glib.UserDirectory.__enum_values__.items():
if index == 8: continue
dir = glib.get_user_special_dir(index)
if dir: user_dirs.append(dir)
return user_dirs
def get_file_icon(self,*args):
if args[-1].endswith('.desktop'):
desk_file = Gio.DesktopAppInfo.new_from_filename(args[-1])
icon = desk_file.get_icon()
if type(icon) == Gio.ThemedIcon:
themed_name = icon.get_names()[0]
theme = gtk.IconTheme.get_default()
name = theme.lookup_icon(themed_name, 48, 0).get_filename()
if type(icon) == Gio.FileIcon:
name = icon.get_file().get_uri()
icon_url= urllib.parse.unquote(name).replace('file://','')
return icon_url
file = Gio.File.new_for_path(args[-1])
file_info = file.query_info("standard::*",0)
icon_string = file_info.get_icon().to_string()
if 'folder-' in icon_string:
return icon_string.split()[-2]
return icon_string.split()[-1]
def get_recent_files(self,*args):
manager = gtk.RecentManager.get_default()
try:
files = OrderedDict()
for index,item in enumerate(manager.get_items(),1):
uri = item.get_uri()
uri_decoded = urllib.parse.unquote(uri)
filepath = uri_decoded.replace('file://','')
if not os.path.exists(filepath): continue
basename = os.path.basename(uri_decoded)
files[basename] = filepath
if index == self.max_items:
break
except Exception as e:
print(e)
return None
finally: return files
def callback(self,*args):
self.update()
def update(self,*args):
current_files = self.get_recent_files()
if current_files != self.cached_files:
self.make_menu()
self.cached_files = current_files
glib.timeout_add_seconds(3,self.callback)
def add_submenu(self,top_menu,label):
menuitem = gtk.MenuItem(label)
submenu = gtk.Menu()
menuitem.set_submenu(submenu)
top_menu.append(menuitem)
menuitem.show()
return submenu
def make_menu(self):
if hasattr(self, 'app_menu'):
for item in self.app_menu.get_children():
self.app_menu.remove(item)
else:
self.app_menu = gtk.Menu()
recent = self.add_submenu(self.app_menu,'Recent Files')
recent_dict = self.get_recent_files()
content = [recent,gtk.ImageMenuItem,'gtk-add',
'Add to Recent Files',self.add_recent,[None]
]
self.add_menu_item(*content)
content = [recent,gtk.ImageMenuItem,'user-trash',
'Clear recent files list',self.clear_recent,[None]
]
self.add_menu_item(*content)
content = [recent,gtk.SeparatorMenuItem,
None,None,
None,[None]
]
self.add_menu_item(*content)
self.add_menu_item(*content)
if not recent_dict:
content = [recent,gtk.MenuItem,None,
'No items',None,None
]
self.add_menu_item(*content)
last = None
for i in recent.get_children():
last = i
last.set_sensitive(False)
else:
for name,data in recent_dict.items():
icon = self.get_file_icon(data)
content = [recent, gtk.ImageMenuItem,
icon, name[:self.name_length],
self.open_item, [data]
]
self.add_menu_item(*content)
# Pinned files
bookmarks = self.add_submenu(self.app_menu,'Pinned Files')
content = [bookmarks,gtk.ImageMenuItem,
'bookmark_add','Pin a file',
self.pin_file,[bookmarks,None]
]
self.add_menu_item(*content)
content = [bookmarks,gtk.ImageMenuItem,
'remove','Remove item',
self.remove_pinned,['files']
]
self.add_menu_item(*content)
content = [bookmarks,gtk.ImageMenuItem,
'user-trash','Remove All',
self.remove_all_pinned,[None]
]
self.add_menu_item(*content)
content = [bookmarks,gtk.SeparatorMenuItem,
None,None,
None,[None]
]
self.add_menu_item(*content)
self.add_menu_item(*content)
pinned_files = self.get_pinned()
if (pinned_files and
'files' in pinned_files.keys() and
pinned_files['files']):
for filepath in pinned_files['files']:
icon = self.get_file_icon(filepath)
content = [bookmarks,gtk.ImageMenuItem,
icon,os.path.basename(filepath),
self.open_item,[filepath]
]
self.add_menu_item(*content)
else:
content = [bookmarks,gtk.MenuItem,None,
'No items',None,None
]
self.add_menu_item(*content)
last = None
for i in bookmarks.get_children():
last = i
last.set_sensitive(False)
places = self.add_submenu(self.app_menu,'Places')
content = [places,gtk.ImageMenuItem,'add',
'Pin Directory',self.pin_dir,[None]
]
self.add_menu_item(*content)
content = [places,gtk.ImageMenuItem,
'remove','Remove Pinned',
self.remove_pinned,['dirs']
]
self.add_menu_item(*content)
content = [places,gtk.SeparatorMenuItem,
None,None,
None,[None]
]
self.add_menu_item(*content)
content = [places,gtk.MenuItem,None,
'Standard Dirs',None,None
]
self.add_menu_item(*content)
last = None
for i in places.get_children():
last = i
last.set_sensitive(False)
for dir in self.get_user_dirs():
icon = self.get_file_icon(dir)
content = [places,gtk.ImageMenuItem,icon,
os.path.basename(dir),self.open_item,[dir]
]
self.add_menu_item(*content)
content = [places,gtk.SeparatorMenuItem,
None,None,
None,[None]
]
self.add_menu_item(*content)
content = [places,gtk.MenuItem,None,
'Pinned Dirs',None,None
]
self.add_menu_item(*content)
last = None
for i in places.get_children():
last = i
last.set_sensitive(False)
if (pinned_files and
'dirs' in pinned_files.keys() and
pinned_files['dirs']):
for dir in pinned_files['dirs']:
icon = self.get_file_icon(dir)
print(icon)
content = [places,gtk.ImageMenuItem,icon,
os.path.basename(dir),self.open_item,[dir]
]
self.add_menu_item(*content)
else:
content = [places,gtk.MenuItem,None,
'No items',None,None
]
self.add_menu_item(*content)
last = None
for i in places.get_children():
last = i
last.set_sensitive(False)
content = [self.app_menu,gtk.SeparatorMenuItem,
None,None,
None,[None]
]
self.add_menu_item(*content)
content = [self.app_menu,gtk.ImageMenuItem,'exit',
'quit',self.quit,[None]
]
self.add_menu_item(*content)
self.app.set_menu(self.app_menu)
def check_directory(self,*args):
current_set = set(os.listdir(args[-1]))
return current_set - self.cached_set
def get_pinned(self,*args):
try:
with open(self.pinned_list) as f:
return json.load(f,object_pairs_hook=OrderedDict)
except FileNotFoundError:
print('>>> ',self.pinned_list,' not found')
return None
except json.JSONDecodeError:
print(">>> Can't read ",self.pinned_list,',may be corrupt')
return None
def pin_dir(self,*args):
# TODO
current_list = self.get_pinned()
if not current_list:
current_list = OrderedDict()
current_list['dirs'] = []
f = open(self.pinned_list,'w')
f.write("")
f.close()
if not args[-1]:
cmd = "zenity --file-selection --directory --separator || --multiple"
dirs = self.run_cmd(cmd.split())
else:
dirs = args[-1]
dir_list = []
if not dirs: return None
dir_list = dirs.decode().strip().split("||")
if not 'dirs' in current_list.keys():
current_list['dirs'] = []
for f in dir_list:
#icon = self.get_file_icon(f)
current_list['dirs'].append(f)
with open(self.pinned_list,'w') as f:
json.dump(current_list,f,indent=4)
self.make_menu()
def pin_file(self,*args):
current_list = self.get_pinned()
if not current_list:
current_list = OrderedDict()
current_list['files'] = []
f = open(self.pinned_list,'w')
f.write("")
f.close()
if not args[-1]:
cmd = "zenity --file-selection --separator || --multiple "
files = self.run_cmd(cmd.split())
else:
files = args[-1]
file_list = []
if not files: return None
file_list = files.decode().strip().split("||")
if not 'files' in current_list.keys():
current_list['files'] = []
for f in file_list:
#icon = self.get_file_icon(f)
current_list['files'].append(f)
with open(self.pinned_list,'w') as f:
json.dump(current_list,f,indent=4)
self.make_menu()
def remove_all_pinned(self,*args):
try:
#os.unlink(self.pinned_list)
with open(self.pinned_list) as f:
pinned = json.load(f)
pinned.pop('files')
with open(self.pinned_list,'w') as f:
json.dump(pinned,f,indent=4)
except:
pass
finally:
self.make_menu()
def remove_pinned(self,*args):
key = args[-1]
pinned = self.get_pinned()
if not pinned: return
cmd_str = "zenity --forms --add-combo Remove --combo-values"
vals = "|".join(pinned[key])
cmd = cmd_str.split() + [vals]
item = self.run_cmd(cmd)
if item:
path = item.decode().strip()
index = pinned[key].index(path)
pinned[key].pop(index)
with open(self.pinned_list,'w') as f:
json.dump(pinned,f,indent=4)
self.make_menu()
def add_recent(self,*args):
cmd = "zenity --file-selection --separator || --multiple "
files = self.run_cmd(cmd.split())
file_list = []
if not files: return
file_list = files.decode().strip().split("||")
items = ['file://' + f for f in file_list]
for f in items: gtk.RecentManager().get_default().add_item(f)
def clear_recent(self,*args):
try:
gtk.RecentManager.get_default().purge_items()
self.make_menu()
except:
pass
def open_item(self,*args):
#self.run_cmd(['xdg-open',args[-1]])
if args[-1].endswith('.desktop'):
desk_file = Gio.DesktopAppInfo.new_from_filename(args[-1])
return desk_file.launch_uris()
return subprocess.Popen(['xdg-open',args[-1]])
def quit(self,*args):
gtk.main_quit()
def run_cmd(self, cmdlist):
""" utility: reusable function for running external commands """
#new_env = dict(os.environ)
#new_env['LC_ALL'] = 'C'
try:
stdout = subprocess.check_output(cmdlist) #env=new_env)
except subprocess.CalledProcessError:
pass
else:
if stdout:
return stdout
def run(self):
""" Launches the indicator """
try:
gtk.main()
except KeyboardInterrupt:
pass
def quit(self, *args):
""" closes indicator """
gtk.main_quit()
def main():
""" defines program entry point """
indicator = FilesIndicator()
indicator.run()
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
gtk.main_quit()
Configurations
The indicator is configured via two json files stored in user's home directory.
~/.files_indicator.json
controls user interface , the length of menu entries and maximum numbers in the Recent Files Menu.
{
"name_length": 30,
"max_items": 10
}
The ~/.pinned_files.json
controls the lists of pinned files and folders. Each item is a list/array.
{
"dirs": [
"/home/xieerqi/\u56fe\u7247/Wallpapers"
],
"files": [
"/home/xieerqi/work_in_progress/videonauth_code.py",
"/home/xieerqi/work_in_progress/spin_button.py"
]
}