Ubuntu – How to add a dynamic, application specific quicklist for recently used files to the Unity Launcher

desktoplauncherlibreofficequicklistsunity

It would be an awesome feature to access recently used documents from LibreOffice with a dynamic quicklist in the launcher. There is quite some experience on how to create custom static quicklists.

But is anyone out there who might give some constructive orientation on how to build a dynamic quicklist for recently used docs in lo?

The Ubuntu wiki has a very brief description on how to create quicklists with python or vala. I am not experienced in either of the two and I did not find comprehensive example scripts for dynamic quicklists out there. Therefore I'm looking for some easier way to implement it or somebody who has already done/seen it.

Best Answer

Adding a dynamic "recently used" section to the launcher of an application

The full integration of an application with the mentioned dynamic quicklist -entries most likely needs to be done from inside of the application. After all, the most direct information on used files comes from the application itself.

However, since editing the source code is outside the scope of what we're doing, that would not be the road to take.

Then what?

That doesn't mean we cannot achieve pretty much exactly the same result, maybe even in a more flexible and general way, from "outside". All the information we need is available in the dynamically updated file: ~/.local/share/recently-used.xbel, from which we can retrieve the complete history of opened files, the corresponding date & time information and the application that was used.

Furthermore, to add a dynamically updated section to a launcher can very well be done as part of the "traditional" (static) section. The key of the solution is then to create a process that takes care of the above actions without adding a noticeable burden to your system.
As mentioned in the link from the question, some background process would be needed anyway to keep track of the changes and pass the instructions.

The script below is pretty much doing exactly that.

The solution; a background script

The values in the script below are specifically set for LibreOffice and its documents. Without any editing, it can be used to add a recently used -section to the LibreOffice-Writerlauncher. It will show the last 10 used documents, opened by any of the LibreOffice -modules.

The solution can however be used to add a "recently used" section to many applicatiosn with a .desktop file in /usr/share/applications. Since the file ~/.local/share/recently-used.xbel is Gtk related, most likely, applications with a Gtk window will be our potential candidates (that is, if the application opens and edits files). Furthermore, the number of files to show is arbitrary.

How it looks

The solution adds a section to the targeted launcher in the Unity launcher, showing an arbitrary number of recently used files, e.g.:

  • show the last seven files:

    enter image description here

  • or the last ten files:

    enter image description here

  • With the same ease however, we can give the gedit launcher a dynamic section, showing the last seven files, opened with gedit (see image further below)

How to use

Assuming you have LibreOffice preinstalled (the downloaded version does not have a referring .desktop file in /usr/share/applications which is needed by the script, but somewhere else, please mention if you need to setup the separately downloaded LO version)

  1. Copy the script below into an empty file, save it as dynamic_recent.py For LibreOffice, the process name is soffice, already set correctly in the script.

    #!/usr/bin/env python3
    import subprocess
    import os
    import time
    import shutil
    
    # --- set the number of docs to show in recently used
    n = 7
    # --- set the process name of the targeted application
    application = "soffice"
    #--- ONLY change the value below into "xdg-open" if you do not use LO preinstalled
    #    else the value should be the same as in application = (above)
    open_cmd = "soffice"
    # --- set the targeted .desktop file (e.g. "gedit.desktop")
    target = "libreoffice-writer.desktop"
    
    # --- don't change anything below
    home = os.environ["HOME"]+"/.local/share"
    loc = home+"/applications/"+target
    recdata = home+"/recently-used.xbel"
    
    def runs(app):
        try:
            # see if the application is running
            app = subprocess.check_output(["pgrep", app]).decode("utf-8")
        except subprocess.CalledProcessError:
            return False
        else:
            return True
    
    def get_lines():
        # retrieve information from the records:
        # -> get bookmark line *if* the application is in the exec= line
        with open(recdata) as infile:
            db = []
            for l in infile:
                if '<bookmark href="file://' in l:
                    sub = l
                elif 'exec="&apos;'+application in l:
                    db.append(sub)
        # fix bug in xbel -file in 15.04
        relevant = [l.split('="') for l in set(db) if all([not "/tmp" in l, "." in l])]
        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_section(line):
        # create shortcut section
        name = line.split("/")[-1]
        return [[
            "[Desktop Action "+name+"]",
            "Name="+name,
            "Exec="+open_cmd+" '"+line+"'",
            "\n",
            ], name]
    
    def setup_dirs():
        # copy the global .desktop file to /usr/share/applications/
        glo = "/usr/share/applications/"+target
        if not os.path.exists(loc):
            shutil.copy(glo,loc)
    
    def edit_launcher(newdyn, target, actionlist):
        # read the current .desktop file
        ql = [list(item) for item in list(enumerate(open(loc).read().splitlines()))]
        # find the Actions= line
        currlinks = [l for l in ql if "Actions=" in l[1]]
        # split the line (if it exists)  by the divider as delimiter 
        linkline = currlinks[0][1].split("divider1")[0] if currlinks else None
        # define the shortcut sections, belonging to the dynamic section (below the divider)
        lowersection = [l for l in ql if "[Desktop Action divider1]" in l]
        # compose the new Actions= line
        addlinks = (";").join(actionlist) + ";"
        if linkline:
            newlinks = linkline + addlinks
            ql[currlinks[0][0]][1] = newlinks
            # get rid of the "dynamic" section  
            ql = ql[:lowersection[0][0]] if lowersection else ql
            # define the new file
            ql = [it[1] for it in ql]+newdyn
            with open(loc, "wt") as out:
                for l in ql:
                    out.write(l+"\n")
        else:
            newlinks = "Acrions="+addlinks
    
    setup_dirs()
    lines1 = []
    
    while True:
        time.sleep(2)
        # if the application does not run, no need for a check of .xbel
        if runs(application):
            lines2 = get_lines()
            # (only) if the list of recently used changed: edit the quicklist
            if lines1 != lines2:
                actionlist = ["divider1"]
                newdyn = [
                    "[Desktop Action divider1]",
                    "Name=" + 37*".",
                    "\n",
                    ]
                for line in lines2:
                    data = create_section(line)
                    actionlist.append(data[1])
                    section = data[0]
                    for l in section:
                        newdyn.append(l)
                edit_launcher(newdyn, target, actionlist)           
            lines1 = lines2
    
  2. In the head section of the script, you can set a number of options:

    # --- set the number of docs to show in recently used
    n = 7
    # --- set the process name of the targeted application
    application = "soffice"
    #--- ONLY change the value below into "xdg-open" if you do not use LO preinstalled
    #    else the value should be the same as in application = (above)
    open_cmd = "soffice"
    # --- set the targeted .desktop file (e.g. "gedit.desktop")
    target = "libreoffice-writer.desktop"
    

    Most of the options speak for themselves, if you want to add the dynamic section to the LO-Writer launcher, leave everything as it is. If not, set the appropriate launcher.

  3. Test- run the script by running from a terminal:

    python3 /path/to/dynamic_recent.py
    
  4. The script copied the global .desktop file to ~/.local/share/applications (in this case ~/.local/share/applications/libreoffice-writer.desktop). Drag the local copy to the launcher (else you'd need to log out/in).

  5. If all works fine, add it to Startup Applications: Dash > Startup Applications > Add. Add the command:

    python3 /path/to/dynamic_recent.py
    

To use it on other applications

As mentioned, you can easily use the script to add a dynamic "recently used" section to other application's launcher(s). To do so, see the gedit example setting for the head section of the script:

# --- set the number of docs to show in recently used
n = 7
# --- set the process name of the targeted application
application = "gedit"
#--- ONLY change the value below into "xdg-open" if you do not use LO preinstalled
#    else the value should be the same as in application = (above)
open_cmd = "gedit"
# --- set the targeted .desktop file (e.g. "gedit.desktop")
target = "gedit.desktop"

enter image description here

How it works

  • The script periodically looks through the file ~/.local/share/recently-used.xbel to find matching files, opened with LibreOffice (processname: soffice)

    It uses a pretty fast algorithm to do so, "shooting" through the file in a single pass, to retrieve the needed lines (two per "record"). The result is that the script is very low on juice.

  • Once the relevant lines are retrieved from the file, the lines are sorted by date/time, creating a "top ten" (or any other number) of most recently used files of the corresponding application.

  • ONLY if this list is changed, the .desktop file is updated.

I could notice nor measure any additional load to my system, running the script in the background.

Tested on 14.04 / 15.10

How to restore the original launcher

Simply remove the local copy of the launcher in ~/.local/share/applications

Notes

  • In case you use Unity Quicklist Editor to edit your launchers (quicklists), you should avoid editing launchers with a dynamically updated "last used" -section from this answer. The edits you make with the QUicklist Editor will instantly be overwritten by the script.

  • You can edit your quicklist manually, but make sure you add new item before (on the left side of) divider1 in the Actions= - line

    Actions=Window;Document;divider1;aap.sh;Todo;pscript_2.py;currdate;bulkmail_llJacob;verhaal;test doc;

    All items on the right of divider1 belong to the dynamically updated section.


Major edit

Some major improvements were just made:

  1. The script now only checks the .xbel file while the targeted application runs (since there won't be changes on the recently used list if the application does not run). The script already was low on juice, but now, only keeping an eye on if the application runs, means even less to your system.
  2. In 15.04+, it turns out the .xbel file had double mentions of new files; one with, and one without extension. The effect of that is now eliminated.