mirror of
https://github.com/alrayyes/dmenu.git
synced 2024-11-24 20:46:22 +00:00
refactor: updated dependencies
This commit is contained in:
parent
5f11ab86e8
commit
a5e7482731
2 changed files with 2 additions and 337 deletions
9
PKGBUILD
9
PKGBUILD
|
@ -7,10 +7,10 @@ pkgdesc='Generic menu for X'
|
||||||
url='https://tools.suckless.org/dmenu/'
|
url='https://tools.suckless.org/dmenu/'
|
||||||
arch=('x86_64')
|
arch=('x86_64')
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
depends=('sh' 'libxinerama' 'libxft' 'freetype2' 'menu-calc' 'pass' 'yubikey-oath-dmenu' 'otf-nerd-fonts-fira-code')
|
depends=('sh' 'glibc' 'coreutils' 'libx11' 'libxinerama' 'freetype2' 'fontconfig' 'libfontconfig.so' 'menu-calc' 'pass' 'yubikey-oath-dmenu')
|
||||||
|
|
||||||
sha512sums=('2b6a7cdf5aefc5e7ca7a4944883c3c16ee6f5005d2a96b61482d4899ad395f9cb8926907681d88b9df3e1188cf421dad4cc17e343b752f6cb8b161d33384b3f3'
|
sha512sums=('2b6a7cdf5aefc5e7ca7a4944883c3c16ee6f5005d2a96b61482d4899ad395f9cb8926907681d88b9df3e1188cf421dad4cc17e343b752f6cb8b161d33384b3f3'
|
||||||
'2597b08c19aa0c0dede048b2ce27b2d0c1f11ffcaefa431aca256d7a8821b453d383e88ef34ca10cebb7b5d8118953b38e2dfb2ced417d1a394fe8df67779d0c'
|
'254cd0c4e82149700004897ca5ef6be63b02d791ce327c2a4f68d6ee6599645e5b469771514e80e188c4e531caa3e4349ecb035b2fdc10b687a380b7d6b36632'
|
||||||
'ad8183cca1ad1dd5b786896f94d127eeea53cece291c485f928388a7589402f49ed7e0ee8bdae71cc2ed41f3276b96591349cd820621f345376a54d557307fdf'
|
'ad8183cca1ad1dd5b786896f94d127eeea53cece291c485f928388a7589402f49ed7e0ee8bdae71cc2ed41f3276b96591349cd820621f345376a54d557307fdf'
|
||||||
'beed0f7725d84b4d73cb35fb7d78d7db90c7fa15ed0ac4d6cf19e6469caf83167bc3edae246c30a2f0a79e765b46d9a37997a9a72f5dec147cc0a904fa2ef790'
|
'beed0f7725d84b4d73cb35fb7d78d7db90c7fa15ed0ac4d6cf19e6469caf83167bc3edae246c30a2f0a79e765b46d9a37997a9a72f5dec147cc0a904fa2ef790'
|
||||||
'ce806c6481b20d5b2352fc387816878c75285162f6404b3a969a95f9dbd6c7476fd4c6571f260886fa30ccc40e4a388ea8f6902e40ff90447e04bec3faf0669d'
|
'ce806c6481b20d5b2352fc387816878c75285162f6404b3a969a95f9dbd6c7476fd4c6571f260886fa30ccc40e4a388ea8f6902e40ff90447e04bec3faf0669d'
|
||||||
|
@ -30,7 +30,6 @@ _patches=(
|
||||||
|
|
||||||
source=(https://dl.suckless.org/tools/dmenu-${pkgver}.tar.gz
|
source=(https://dl.suckless.org/tools/dmenu-${pkgver}.tar.gz
|
||||||
config.h
|
config.h
|
||||||
dmenu-frequency
|
|
||||||
"${_patches[@]}")
|
"${_patches[@]}")
|
||||||
|
|
||||||
prepare() {
|
prepare() {
|
||||||
|
@ -59,10 +58,6 @@ package() {
|
||||||
cd ${pkgname}-${pkgver}
|
cd ${pkgname}-${pkgver}
|
||||||
make PREFIX=/usr DESTDIR="${pkgdir}" install
|
make PREFIX=/usr DESTDIR="${pkgdir}" install
|
||||||
install -Dm 644 LICENSE -t "${pkgdir}/usr/share/licenses/${pkgname}"
|
install -Dm 644 LICENSE -t "${pkgdir}/usr/share/licenses/${pkgname}"
|
||||||
|
|
||||||
cd ../
|
|
||||||
install -Dm0755 dmenu-frequency "${pkgdir}/usr/bin/dmenu-frequency"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# vim: ts=2 sw=2 et:
|
# vim: ts=2 sw=2 et:
|
||||||
|
|
||||||
|
|
330
dmenu-frequency
330
dmenu-frequency
|
@ -1,330 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
"""Dmenu launcher with history sorted by frecency.
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
Loading…
Reference in a new issue