我正在寻找一个托盘图标指示器,单击图标后会显示我最近使用的文件列表。这将是快速访问这些文件的好方法。
答案1
在面板中显示最近使用的文件
使用下面的脚本,您可以在面板中拥有任意数量的最近使用的项目,例如 6 个项目:
...或 20 件商品:
...取决于您的设置。
怎么运行的
该设置包含两项:
- 一个图标,名称(准确)
recent.png
- 脚本
两者都需要位于同一个文件夹中。之后,只需运行脚本即可。
如何设置
可能需要安装python3-gi
:
sudo apt-get install python3-gi
然后:
将以下脚本复制到一个空文件中,并将其另存为
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()
在脚本的头部部分,设置要显示的项目数:
# --- set the number of recently used files to appear below n = 20 # ---
在同一个文件夹将下面的图标保存为(准确)
recent.png
(右键点击 -> 另存为)
通过以下命令测试运行脚本:
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 版本的一个选项添加进去。
同样,选项窗口也可以设置一些选项,例如要显示的文件数量等。
笔记
该指标现已与其他一些指标合并这个。
答案2
虽然你没有提到你使用的是哪个版本的 Ubuntu,但有一个不错的 Xubuntu 应用程序。更具体地说,它是Xfce,因此如果你使用Xfce。该应用程序名为地点。
要启用,请右键单击面板,然后从菜单中选择面板 -> 添加新项目...,然后选择地点。这将显示您刚刚打开的文件夹和最近文档的列表。如下所示:
您也可以很好地配置它:
这里是项目页面。
如果你不使用Xfce,您可以非常轻松地安装它,如这个答案。只需执行sudo apt-get install xfce-panel
,然后运行(参见链接)。请注意,Xfce 面板可能不兼容与 Unity(或其他)面板中的所有其他应用程序兼容。根据我的经验,我完全没有遇到任何问题,因此它可能仍然是一个很好的解决方案。
答案3
不是托盘图标,而是统一启动器的快速列表脚本(与变音符号一起使用),您也可以固定文件:Ubuntu-最近的快速列表
快速说明:
- 下载 .zip
- 提炼
- 制作
install.sh
并ubuntu-recentquicklists.py
执行 - 运行
install.sh
脚本
特征:
- 自动添加最近的文件
- 手动添加非最近文件/固定最近文件
- 指定每个列表所需的项目数,以及
- 这些物品可能有多旧
- 解析符号链接
- 显示完整路径(或仅显示文件名,如屏幕截图所示)
答案4
更新日期:2017 年 2 月 24 日:指示器现在可以选择固定网络链接。
介绍
下面介绍的文件指示器是用于访问用户文件和文件夹的简单指示器。它允许检查最近使用的文件、为文件和目录添加书签。
更新: 指示器现在还支持启动已固定的 .desktop 文件。例如,如果您已固定 firefox.desktop,它将启动 firefox。因此,指示器可用作程序的快速启动器。在撰写本文时(格林威治标准时间 11 月 19 日晚上 7:53,大约需要 24 小时才能处理),该功能已在 github 和此处的更新源代码中提供。
获取指标
该指标可从我的个人 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: [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()
配置
该指标通过存储在用户主目录中的两个 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"
]
}