1
0
mirror of https://github.com/alrayyes/dotfiles/ synced 2023-11-14 15:56:30 +00:00

Compare commits

..

No commits in common. "906a3389e180c8c16e50412fd8581e1333ba0d98" and "034c581b803249180574406ad90c2e6b2e0ea319" have entirely different histories.

9 changed files with 333 additions and 24 deletions

330
dmenu/.local/bin/dmenu-frequency Executable file
View File

@ -0,0 +1,330 @@
#!/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()

View File

@ -1,3 +0,0 @@
(("default" . ((user-emacs-directory . "~/.config/emacs")))
("doom" . ((user-emacs-directory . "~/.local/share/doom")
(env . (("DOOMDIR" . "~/.config/doom"))))))

View File

@ -1,12 +0,0 @@
[Desktop Entry]
Name=Doom Emacs
GenericName=Doom Emacs
Comment=Chemacs started with Doom Emacs profile
MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
Exec=emacs --with-profile doom %F
Icon=emacs
Type=Application
Terminal=false
Categories=Development;TextEditor;
StartupWMClass=Emacs
Keywords=Text;Editor;

View File

@ -27,7 +27,7 @@ end
# Add paths
path_if_exists ~/.local/share/yarn/global/node_modules/.bin
path_if_exists ~/.local/bin
path_if_exists ~/.local/share/doom/bin
path_if_exists ~/.emacs.d/bin
path_if_exists ~/.local/share/cargo/bin
path_if_exists ~/.local/share/go/bin
@ -133,9 +133,6 @@ abbr --add rm "rm -i"
# Colorize diff
abbr --add diff "diff --color"
# Doom Emacs alias
abbr --add demacs "emacs --with-profile doom"
# Load lfcd wiith proper icons
function lf
set -lx LF_ICONS "\

View File

@ -84,12 +84,12 @@ mkdir_if_not_exist ~/.config/systemd/user
mkdir_if_not_exist ~/.config/wal
mkdir_if_not_exist ~/.config/weechat
mkdir_if_not_exist ~/.config/X11
mkdir_if_not_exist ~/.emacs.d
mkdir_if_not_exist ~/.gnupg
mkdir_if_not_exist ~/.config/mpv/scripts
mkdir_if_not_exist ~/.config/coc/extensions
mkdir_if_not_exist ~/.config/dmenu-frecency
mkdir_if_not_exist ~/.config/doom
mkdir_if_not_exist ~/.config/emacs
mkdir_if_not_exist ~/.config/sxhkd
mkdir_if_not_exist ~/.local/share/applications

View File

@ -83,9 +83,6 @@ alias rm="rm -i"
# Colorize diff
alias diff="diff --color"
# Doom Emacs alias
alias demacs="emacs --with-profile doom"
# Use lf to switch directories and bind it to ctrl-o
lfcd() {
tmp="$(mktemp)"

View File

@ -10,7 +10,7 @@ TERM="xterm-256color"
# PATH
path_if_exists ~/.local/share/yarn/global/node_modules/.bin
path_if_exists ~/.local/bin
path_if_exists ~/.local/share/doom/bin
path_if_exists ~/.emacs.d/bin
path_if_exists ~/.local/share/go/bin
path_if_exists ~/.local/share/cargo/bin