代码拉取完成,页面将自动刷新
#!/usr/bin/env python
"""Dmenu launcher with history sorted by frecency. This file must be in /usr/bin
Usage:
dmenu-frecency [--read-apps]
Options:
--read-apps rereads all .desktop files.
"""
from docopt import docopt
import os
import sys
import xdg.BaseDirectory
from xdg.DesktopEntry import DesktopEntry
from subprocess import Popen, PIPE
from datetime import datetime
from collections import defaultdict
import pickle
import re
import gzip
import json
import tempfile
import shlex
CONFIG_DIR = xdg.BaseDirectory.save_config_path('dmenu-frecency')
# Python 2 compatibility
try:
FileNotFoundError
except NameError:
FileNotFoundError = IOError
class Application:
def __init__(self, name, command_line, mtime=None, path=None, is_desktop=False):
self.name = name
self.path = path
self.command_line = command_line
self.is_desktop = is_desktop
self.show_command = False
if mtime is None:
self.mtime = datetime.now()
else:
self.mtime = mtime
def run(self):
if os.fork() == 0:
if self.path:
os.chdir(self.path)
os.execvp(os.path.expanduser(self.command_line[0]), self.command_line)
def __lt__(self, other):
return (self.is_desktop, self.mtime) < (other.is_desktop, other.mtime)
def __eq__(self, other):
return self.name == other.name
def __hash__(self):
return hash(self.name)
def __str__(self):
return "<Application: {} {!r}>".format(self.name, self.command_line)
STATE_VERSION = 4
def get_command(desktop_entry):
tokens = []
for token in shlex.split(desktop_entry.getExec()):
if token == '%i':
if desktop_entry.getIcon():
tokens.append('--icon')
tokens.append(desktop_entry.getIcon())
else:
i = 0
newtok = ""
nc = len(token)
while i < nc:
c = token[i]
if c == '%' and i < nc - 1:
i += 1
code = token[i]
if code == 'c' and desktop_entry.getName():
newtok += desktop_entry.getName()
elif code == '%':
newtok += '%'
else:
newtok += c
i += 1
if newtok:
tokens.append(newtok)
return tuple(tokens)
class LauncherState:
STATE_FILENAME = os.path.join(CONFIG_DIR, 'state')
def __init__(self, config):
self.version = STATE_VERSION
self.config = config
self.find_apps()
self.apps_generated_at = datetime.now()
self.visits = defaultdict(list)
self.visit_count = defaultdict(int)
self.app_last_visit = None
self.frecency_cache = {}
def apps_by_frecency(self):
app_last_visit = self.app_last_visit if self.config['preselect-last-visit'] else None
if app_last_visit is not None:
yield app_last_visit
for app, frec in sorted(self.frecency_cache.items(), key=lambda x: (-x[1], x[0])):
if app_last_visit is None or app_last_visit != app:
yield app
for app in self.sorted_apps:
if app not in self.frecency_cache:
if app_last_visit is None or app_last_visit != app:
yield app
def add_visit(self, app):
if not app.is_desktop and app.command_line in self.command_apps:
app = self.command_apps[app.command_line]
app.show_command = True
try:
self.sorted_apps.remove(app)
except ValueError:
pass # not in list
vs = self.visits[app]
now = datetime.now()
vs.append(now)
self.visit_count[app] += 1
self.visits[app] = vs[-self.config['frecency-visits']:]
self.app_last_visit = app if self.config['preselect-last-visit'] else None
def update_frecencies(self):
for app in self.visits.keys():
self.frecency_cache[app] = self.frecency(app)
def frecency(self, app):
points = 0
for v in self.visits[app]:
days_ago = (datetime.now() - v).days
if days_ago < 4:
points += 100
elif days_ago < 14:
points += 70
elif days_ago < 31:
points += 50
elif days_ago < 90:
points += 30
else:
points += 10
return int(self.visit_count[app] * points / len(self.visits[app]))
@classmethod
def load(cls, config):
try:
with gzip.open(cls.STATE_FILENAME, 'rb') as f:
obj = pickle.load(f)
version = getattr(obj, 'version', 0)
if version < STATE_VERSION:
new_obj = cls(config)
if version <= 1:
for app, vs in obj.visits.items():
vc = obj.visit_count[app]
app.is_desktop = True
new_obj.visit_count[app] = vc
new_obj.visits[app] = vs
new_obj.find_apps()
new_obj.clean_cache()
new_obj.update_frecencies()
new_obj.config = config
return new_obj
else:
obj.config = config
return obj
except FileNotFoundError:
return cls(config)
def save(self):
with tempfile.NamedTemporaryFile(
'wb',
dir=os.path.dirname(self.STATE_FILENAME),
delete=False) as tf:
tempname = tf.name
with gzip.open(tempname, 'wb') as gzipf:
pickle.dump(self, gzipf)
os.rename(tempname, self.STATE_FILENAME)
def find_apps(self):
self.apps = {}
self.command_apps = {}
if self.config['scan-desktop-files']:
for applications_directory in xdg.BaseDirectory.load_data_paths("applications"):
if os.path.exists(applications_directory):
for dirpath, dirnames, filenames in os.walk(applications_directory):
for f in filenames:
if f.endswith('.desktop'):
full_filename = os.path.join(dirpath, f)
self.add_desktop(full_filename)
if self.config['scan-path']:
for pathdir in os.environ["PATH"].split(os.pathsep):
pathdir = pathdir.strip('"')
if not os.path.isdir(pathdir):
continue
for f in os.listdir(pathdir):
filename = os.path.join(pathdir, f)
if os.path.isfile(filename) and os.access(filename, os.X_OK):
app = Application(
name=f,
command_line=(f,),
mtime=datetime.fromtimestamp(os.path.getmtime(filename)))
self.add_app(app)
self.sorted_apps = sorted(self.apps.values(), reverse=True)
def add_desktop(self, filename):
try:
d = DesktopEntry(filename)
if d.getHidden() or d.getNoDisplay() or d.getTerminal() or d.getType() != 'Application':
return
app = Application(
name=d.getName(),
command_line=get_command(d),
mtime=datetime.fromtimestamp(os.path.getmtime(filename)),
is_desktop=True)
if d.getPath():
app.path = d.getPath()
self.add_app(app)
except (xdg.Exceptions.ParsingError,
xdg.Exceptions.DuplicateGroupError,
xdg.Exceptions.DuplicateKeyError,
ValueError) as e:
sys.stderr.write("Failed to parse desktop file '{}': {!r}\n".format(filename, e))
def add_app(self, app):
if app.command_line not in self.command_apps:
self.apps[app.name] = app
self.command_apps[app.command_line] = app
def clean_cache(self):
for app in list(self.frecency_cache.keys()):
if app.is_desktop and app.name not in self.apps:
del self.frecency_cache[app]
if self.app_last_visit is not None and self.app_last_visit.name not in self.apps:
self.app_last_visit = None
class DmenuFrecency:
CONFIG_FILENAME = os.path.join(CONFIG_DIR, 'config.json')
DEFAULT_CONFIG = {
'dmenu': 'dmenu',
'dmenu-args': ['-i'],
'cache-days': 1,
'frecency-visits': 10,
'preselect-last-visit': False,
'scan-desktop-files': True,
'scan-path': False,
}
NAME_WITH_COMMAND = re.compile(r"(.+) \([^()]+\)")
def __init__(self, arguments):
self.read_apps = arguments['--read-apps']
self.load_config()
self.state = LauncherState.load(self.config)
assert self.state, "Failed to load state."
def load_config(self):
self.config = {}
self.config.update(self.DEFAULT_CONFIG)
try:
with open(self.CONFIG_FILENAME, 'r') as f:
self.config.update(json.load(f))
except FileNotFoundError:
with open(self.CONFIG_FILENAME, 'w') as f:
json.dump(self.config, f, sort_keys=True, indent=4)
f.write('\n')
def main(self):
if self.read_apps:
self.state.find_apps()
self.state.clean_cache()
self.state.save()
return
dmenu = Popen([self.config['dmenu']] + self.config['dmenu-args'], stdin=PIPE, stdout=PIPE)
for app in self.state.apps_by_frecency():
app_name = app.name.encode('utf-8')
dmenu.stdin.write(app_name)
if app.show_command and app.name != app.command_line[0]:
dmenu.stdin.write(" ({})".format(' '.join(app.command_line)).encode('utf-8'))
dmenu.stdin.write(b'\n')
stdout, stderr = dmenu.communicate()
result = stdout.decode('utf-8').strip()
if not result:
return
if result in self.state.apps:
app = self.state.apps[result]
else:
m = self.NAME_WITH_COMMAND.match(result)
if m and m.group(1) in self.state.apps:
app = self.state.apps[m.group(1)]
else:
app = Application(
name=result,
command_line=tuple(shlex.split(result)))
app.run()
self.state.add_visit(app)
self.state.update_frecencies()
if (datetime.now() - self.state.apps_generated_at).days >= self.config['cache-days']:
self.state.find_apps()
self.state.clean_cache()
self.state.save()
if __name__ == '__main__':
arguments = docopt(__doc__, version="0.1")
DmenuFrecency(arguments).main()
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。