有没有一个指示器可以快速访问最近使用的文件?

我正在寻找一个托盘图标指示器,当点击图标时,显示我最近使用的文件列表。这将是快速访问这些文件的好方法。


可以做到,但列表将会有限,主要是针对带有Gtk窗口的文件。如果没有人回答,我会回答,但可能需要一些时间 :) (需要足够长的时间在家里编写代码 :) )。 - Jacob Vlijm
哇,那真是太棒了,Jacob!我已经能预料到你的解决方案会有相当不错的质量了。:-) 这样的解决方案会包括列出最近的音频文件吗(它们会有一个 Gtk 窗口吗?)? - orschiro
嗨Orschiro,我发布了“测试”版本。在我的系统上运行良好,但请告诉我你的情况。 - Jacob Vlijm
这真是太美了,Jacob!非常感谢你! :-) - orschiro
这个我得发挥出自己的技能了 :) 谢谢你让我有机会挑战自己! - Jacob Vlijm
1嗨orschiro,你可能已经注意到了,但这个问题将会进一步开发http://askubuntu.com/a/851148/72216。包括一个*仅显示*最近使用的文件的选项,作为一个灵活的工具。ppa是`ppa:vlijm/placesfiles`(请参阅链接的答案)。 - Jacob Vlijm
哇,我深受感动!:-) - orschiro
4个回答

在面板中显示最近使用的文件

通过下面的脚本,您可以在面板中随时查看任意数量的最近使用项目,例如6个项目:

enter image description here

...或者20个项目:

enter image description here

...根据您的设置而定。

工作原理

设置包括两个项目:

  1. 一个名为(确切地)recent.png的图标
  2. 一个脚本

这两者需要放在同一个文件夹中。之后,只需运行该脚本即可。

设置方法

可能需要安装python3-gi

sudo apt-get install python3-gi

然后:

将下面的脚本复制到一个空文件中,保存为`recused.py`:
```python #!/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
# --- 设置要显示的最近使用文件的数量 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()) # 线程: self.update = Thread(target=self.check_recent) # 将线程设置为守护线程,以便可以停止指示器 self.update.setDaemon(True) self.update.start()
def get_files(self): # 创建最近使用文件列表 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): # 创建(初始)菜单 self.menu = Gtk.Menu() # 分隔线 menu_sep = Gtk.SeparatorMenuItem() self.menu.append(menu_sep) # item_quit.show() self.menu.show_all() return self.menu
def open_file(self, *args): # 使用默认应用程序打开文件 index = self.menu.get_children().index(self.menu.get_active()) selection = self.menu_items2[index] subprocess.Popen(["xdg-open", selection])
def set_new(self): # 更新菜单中显示的列表 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) # 分隔线 menu_sep = Gtk.SeparatorMenuItem() self.menu.append(menu_sep) # 退出 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() # 这里是我们调用GObject.threads_init()的地方 GObject.threads_init() signal.signal(signal.SIGINT, signal.SIG_DFL) Gtk.main() ```
在脚本的头部设置要显示的项目数量:
```python # --- 设置要显示的最近使用文件的数量 n = 20 # --- ```
在**同一个文件夹**中将下面的图标保存为`recent.png`:
![enter image description here](https://istack.dev59.com/8rNn3.webp)
(右键点击图标 -> 另存为)
通过以下命令测试运行脚本:
``` python3 /path/to/recused.py ```
如果一切正常,将其添加到启动应用程序:Dash -> 启动应用程序 -> 添加。添加以下命令:
``` /bin/bash -c "sleep 15 && python3 /path/to/recused.py" ```

注意事项

  • 此脚本基于先前的答案,如果您对另一种选择感兴趣,那么该答案是应用程序特定的,并使用定期更新的.desktop文件。
  • 该脚本已在16.04上进行了测试,但也应该适用于早期版本。
  • 由于recently-used.xbel文件中的一个错误,有时会重复提到一个文件;一次带扩展名,一次不带扩展名。我通过过滤后者来解决这个问题。结果是没有扩展名的文件不会出现在列表中。如果这是一个问题,请告诉我,我们可以尝试找到另一种解决方法。
  • 如果文件不存在(或已经不存在),指示器显然不会打开该文件。最有可能的是,我将进一步完善脚本,从recently-used.xbel文件中过滤掉那些过时的条目。

说明

许多应用程序在编辑文件和使用Gtk窗口时,会在文件~/.local/share/recently-used.xbel中跟踪已打开的文件。"记录"包括日期和时间、应用程序以及已打开的文件。
脚本读取.xbel文件,按日期/时间对项目进行排序,并在指示器菜单中包含前n个(根据您的设置而定)文件。菜单每3秒更新一次(仅在必要时)。随后,当选择一个项目时,将使用以下命令打开文件:
xdg-open <file>

因此,所选文件将使用“默认”应用程序打开。很有可能确保文件使用上次打开的“实际”应用程序打开。然而,这需要更复杂的解析。我将在Launchpad计划的ppa版本中添加该选项。
同样,设置一些选项的选项窗口也是如此,例如要显示的文件数量等。
注意:
指示器现在与这个中的其他一些内容合并了。

1好答案!为什么不将这个脚本转化为一个功能齐全的应用程序,添加偏好设置选项呢?或者只需分叉xfce面板应用程序,使其在Gnome中运行?对我来说,这些都太高级了。 - user308164
@luchonacho 谢谢!是的,我认为这个应该移动到launchpad上,获得一个适当的设置菜单、ppa等。这是第一个“节日”版本 :) 感谢您的建议! - Jacob Vlijm
Jacob,我应该直接在这里提出一个错误吗?Umlaut字符(例如ü)没有被正确解释。ü变成了%C3%9C - orschiro
好的,慢慢来!只是想确保我不会忘记。:-) - orschiro
@orschiro 嗨orschiro,我刚刚发现连gedit和LibreOffice也无法很好地处理umlauts。即使是gedit的“最近使用”功能也会因为文件在xbel文件中没有正确显示而出错。对此我无能为力。 - Jacob Vlijm
嗨,Jacob,又来一个问题:1. 我录制了一个新的 .ogg 文件。2. 我打开指示器菜单,但在那里找不到它。3. 我用我的播放器播放文件。4. 我再次打开指示器菜单,现在它在那里可见。有什么想法为什么会这样? - orschiro
@orschiro,可能录音机不是一个Gtk应用程序吗?只有Gtk应用程序才会在xbel文件中跟踪文件。 - Jacob Vlijm
它涉及到录音机,我相信它是一个GTK应用程序。它至少提供了一个类似于GTK的界面。 - orschiro
@JacobVlijm 我想我找到了问题所在!正如screencast所示,录音器在录制完成后没有打开GTK窗口。因此它不会显示在xbel中。我猜这里你无能为力,对吗? - orschiro
@orschiro 我恐怕是这样。指示器取决于xbel文件显示的内容。 - Jacob Vlijm
@JacobVlijm 感谢你的澄清!我已经在那里提交了一个关于音频录制器的功能请求:https://bugs.launchpad.net/audio-recorder/+bug/1610508 - orschiro
@JacobVlijm 另一个功能请求:指示器是否支持拖放操作?例如,我点击一个条目,然后可以将文件拖放到浏览器中的上传字段,直接将其上传到网站。这将非常方便!:-) - orschiro
嗨@orschiro,谢谢你的建议。我觉得那有点超出了指示器的范围,并且也不是你期望它做的事情 :) - Jacob Vlijm
嗨 @JacobVlijm,我理解你的观点,并明白这可能有些过分,但如果有的话,我仍然认为这是一个非常好的噱头功能。 :-) - orschiro
@JacobVlijm 只是一个想法——脚本要怎么样才能列出~/Downloads中最新的文件,而不是Recent - orschiro
@RobertOrzanna 你是指“最新添加的”文件,还是在下载中最近使用过的?两者都很容易 :) - Jacob Vlijm
@JacobVlijm 嗯,我猜两者都很酷!:-) - orschiro
@JacobVlijm 太棒了!很高兴在任何阶段进行测试并提供反馈。 :-) - orschiro

虽然您没有提到您使用的是哪个Ubuntu版本,但Xubuntu有一个很好的应用程序。更具体地说,它是Xfce的一部分,所以如果您使用Xfce,您可能仍然可以在Ubuntu中使用它。这个应用程序叫做“Places”。
要启用它,请右键单击面板,从菜单中选择“面板”->“添加新项目...”,然后选择“Places”。这将显示您最近打开的文件夹和文档的列表。请参见下方:

enter image description here

你也可以很好地进行配置:

enter image description here

这是项目页面
如果你没有使用Xfce,你可以很容易地安装它,如this answer所述。只需执行sudo apt-get install xfce-panel,然后运行(参见链接)。请注意,Xfce面板可能与Unity(或其他)面板中的所有其他应用程序不兼容。根据我的经验,我完全没有遇到任何问题,所以它可能仍然是一个很好的解决方案。

非常好的答案,对忘记提到Ubuntu 16.04 Unity感到抱歉。我能在Unity上安装和运行这个Xfce places插件吗? - orschiro
这也是我自己想知道的事情。软件包在这里。为什么你不试试看?看看 sudo apt-get install xfce4-places-plugin 是否有效。我不能这样做,因为它是默认安装的。 - user308164
我已经安装了,但是没找到启动的方法。 - orschiro
你能在面板菜单的“添加新项目”中找到“地点”吗?它可能不会与Unity轻松集成。你可能需要切换到整个xfce-panel。尝试这个答案。然后它应该可以工作! - user308164
真遗憾,我真的很喜欢Unity面板,不想因为一个插件而失去它。 :-( - orschiro
同意。希望Jacob能提供一个对你有用的答案。这引出了一个很好的问题:如何将Xfce应用程序集成到Unity面板中?我没有看到相关的问题。也许可以从这个问题衍生出来一个新的问题? - user308164
很棒的主意!我们开始吧 - orschiro
太酷了!如果有答案的话,我可以在这里更新。 - user308164

2017年2月24日更新:该指示器现在具有固定网页链接的选项。

介绍

下面介绍的文件指示器是一个简单的指示器,用于访问用户的文件和文件夹。它允许检查最近使用的文件,收藏文件和目录。

enter image description here

更新: 指示器现在还支持启动已固定的.desktop文件。例如,如果您已经将firefox.desktop固定在任务栏上,它将启动Firefox浏览器。因此,该指示器可以用作程序的快速启动器。该功能正在写作时进入PPA(Nov 19, 7:53 pm GMT),大约需要24小时处理时间,但已经在GitHub和这里的更新源代码中提供了。

enter image description here

获取指标

该指标可以从我的个人PPA以及GitHub上获取。请按照以下步骤进行获取:

sudo add-apt-repository ppa:1047481448-2/sergkolo
sudo apt-get update
sudo apt-get install files-indicator

源代码

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

#
# Author: Serg Kolo , contact: 1047481448@qq.com
# 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()

配置

该指示器通过存储在用户主目录中的两个JSON文件进行配置。

~/.files_indicator.json 控制用户界面,菜单项的长度以及最近文件菜单中的最大数量。

{
    "name_length": 30,
    "max_items": 10
}
~/.pinned_files.json 控制着固定文件和文件夹的列表。每个项目都是一个列表/数组。
{
    "dirs": [
        "/home/xieerqi/\u56fe\u7247/Wallpapers"
    ],
    "files": [
        "/home/xieerqi/work_in_progress/videonauth_code.py",
        "/home/xieerqi/work_in_progress/spin_button.py"
    ]
}

不是一个托盘图标,而是一个用于Unity启动器的快速列表脚本(支持Umlauts),你还可以固定文件:Ubuntu-Recentquicklists

URQ screenshot

详细安装说明请点击此处

快速指南:

  • 下载 .zip 文件
  • 解压缩
  • install.shubuntu-recentquicklists.py 设为可执行文件
  • 运行 install.sh 脚本

功能特点:

  • 自动添加最近使用的文件
  • 手动添加非最近使用的文件/固定最近使用的文件
  • 指定每个列表中的项目数量
  • 指定项目的最大年龄
  • 解析符号链接
  • 显示完整路径(或者仅显示文件名,如截图所示)

这看起来非常有前途。谢谢分享!你能在回答中解释一下固定功能是什么吗? - orschiro
1如果最近的文件一段时间没有被访问,它们会消失。但是,如果你有一个重要的 .xls 文件希望保留,你可以进入固定模式(点击文件固定),选择你想要保留的文件(直到取消固定),然后退出固定模式(再次点击文件固定)。这样做也会将固定的文件写入配置文件中,所以重新启动等操作不会删除它们。 - hirsch