Permalink
Branch: master
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 305 lines (266 sloc) 7.95 KB
#!/usr/bin/env python
# pylint: disable=misplaced-comparison-constant,missing-docstring
from __future__ import print_function
from os import environ
from os.path import exists, join
from re import compile as re_compile, finditer, match, MULTILINE, VERBOSE
from shutil import copy
from subprocess import check_output, PIPE, Popen
from sys import argv, exit as sys_exit
from tempfile import NamedTemporaryFile
from types import StringTypes
from argparse import ArgumentParser, SUPPRESS
CONFIG_FILE = join(environ['XDG_USER_CONFIG_DIR'], 'rofi', 'config.rasi')
ACTIVE_CHOICE_IDENTIFIER = " (active)"
MATCHING_METHODS = [
'fuzzy',
'glob',
'normal',
'regex',
]
FLAG_STATES = [
'true',
'false'
]
DEFAULTS = {
'matching': 'normal',
'sort': 'true',
'levenshtein-sort': 'false'
}
MESSAGES = {
'matching': 'change matching method',
'sort': 'enable sorting',
'levenshtein-sort': 'force levenshtein sorting'
}
CONFIG_OPTIONS_RAW_PATTERN = r"""
^
.*?
\s
(?P<option>
matching
|
(?:levenshtein-)?sort
)
:\s*
[\"']?
(?P<value>
.*?
)
[\"']?;
.*?
$
"""
CONFIG_OPTIONS_PATTERN = re_compile(
CONFIG_OPTIONS_RAW_PATTERN,
VERBOSE | MULTILINE
)
def review_diff():
"""Compares the config before and after running"""
with NamedTemporaryFile() as suppressed_output:
result_pipe = Popen(
[
'diff',
'--color=always',
'--unified=0',
'-t',
'--tabsize=4',
"%s.bak" % CONFIG_FILE,
CONFIG_FILE
],
stdout=suppressed_output,
)
result_pipe.communicate()
suppressed_output.seek(0)
result = suppressed_output.read()
print(result)
def update_config(key, value):
"""Updates the config file"""
if isinstance(value, StringTypes):
if not match("true|false", value):
value = '"%s"' % value
check_output(
[
'sed',
'-i',
'-e',
r"s/^.*\s%s:.*$/\t%s: %s;/g" % (key, key, value),
CONFIG_FILE
],
)
def validate_item(item_possibilities, defaults_key, desired_value):
"""
Validates the passed-in value. On failure, validates the current default. On
failure, returns the first possibility.
"""
if desired_value in item_possibilities:
return desired_value
desired_value = DEFAULTS[defaults_key]
if desired_value in item_possibilities:
return desired_value
return item_possibilities[0]
def pipe_choices_to_rofi(choices, defaults_key):
"""Throws the choices out via rofi and returns the result"""
rofi_pipe = Popen(
[
'rofi',
'-dmenu',
'-only-match',
'-mesg', "%s" % MESSAGES[defaults_key],
'-no-fixed-num-lines',
'-width', "%d" % (len(MESSAGES[defaults_key]) + 1),
'-hide-scrollbar',
'-theme-str',
'#inputbar {'
' children: [entry,case-indicator]; '
'}',
'-theme-str',
'#listview {'
' dynamic: true; '
'}',
'-theme-str',
'#mainbox {'
' children: [message,inputbar,listview]; '
'}',
'-theme-str',
'#message {'
' border: 0;'
' background-color: @selected-normal-background;'
' text-color: @selected-normal-foreground; '
'}',
'-theme-str',
'#textbox {'
' text-color: inherit; '
'}'
],
stdin=PIPE,
stdout=PIPE,
)
choices_pipe = Popen(
[
'echo',
'-e',
'\n'.join(choices),
],
stdout=rofi_pipe.stdin,
)
result = rofi_pipe.communicate()[0]
choices_pipe.wait()
result = result.strip()
if not result:
result = choices[0]
result = result.replace(ACTIVE_CHOICE_IDENTIFIER, "")
return result
def create_choices(all_choices, current_choice):
"""Creates a list of choices for dmenu"""
choices = ["%s%s" % (current_choice, ACTIVE_CHOICE_IDENTIFIER)]
all_choices.sort()
for choice in all_choices:
if current_choice != choice:
choices.append(choice)
return choices
def find_desired_item(item_possibilities, defaults_key):
"""Builds the dmenu list, polls the user, and validates the result"""
choices = create_choices(item_possibilities, DEFAULTS[defaults_key])
value = pipe_choices_to_rofi(choices, defaults_key)
return validate_item(item_possibilities, defaults_key, value)
def parse_and_update_desired_item(item_possibilities, defaults_key, options):
"""
Attempts to use passed-in values when possible. Otherwise polls the user for
each value via dmenu (via rofi)
"""
desired_value = getattr(options, defaults_key, None)
if (
not desired_value
or
desired_value != validate_item(
item_possibilities,
defaults_key,
desired_value
)
):
desired_value = find_desired_item(item_possibilities, defaults_key)
update_config(defaults_key, desired_value)
def determine_everything(options):
"""Parses and updates matching, sorting, and Levenshstein sorting options"""
parse_and_update_desired_item(MATCHING_METHODS, 'matching', options)
parse_and_update_desired_item(FLAG_STATES, 'sort', options)
parse_and_update_desired_item(FLAG_STATES, 'levenshtein-sort', options)
def parse_argv(args=None):
"""Parses CLI args"""
if args is None:
args = argv[1:]
parser = ArgumentParser(
description="Configure rofi's matching and sorting"
)
parser.add_argument(
'--skip-diff',
dest='skip_diff',
action='store_true',
help="Don't print the config diff"
)
exclusive_options = parser.add_mutually_exclusive_group()
exclusive_options.add_argument(
'-o', '--only',
dest='only',
default=SUPPRESS,
choices=['matching', 'sort', 'levenshtein-sort'],
help='Change only the specified option',
)
config_options = parser.add_argument_group()
config_options.add_argument(
'-m', '--matching',
dest='matching',
nargs='?',
const=DEFAULTS['matching'],
default=SUPPRESS,
choices=MATCHING_METHODS,
help='Sets the matching method',
)
config_options.add_argument(
'-s', '--sort',
dest='sort',
nargs='?',
const=DEFAULTS['sort'],
default=SUPPRESS,
choices=FLAG_STATES,
help='Enables sorting',
)
config_options.add_argument(
'-l', '--levenshtein-sort',
dest='levenshtein-sort',
nargs='?',
const=DEFAULTS['levenshtein-sort'],
default=SUPPRESS,
choices=FLAG_STATES,
help='Forces Levenshtein sorting',
)
return parser.parse_args(args)
def create_config_if_dne(raw_config):
"""Creates a new config file if it doesn't exist"""
if not exists(CONFIG_FILE):
with open(CONFIG_FILE, 'w+') as config_file:
config_file.write(raw_config)
def load_config():
"""Loads the active rofi config"""
raw_config = check_output(['rofi', '-dump-config'])
for config_match in finditer(CONFIG_OPTIONS_PATTERN, raw_config):
DEFAULTS[config_match.group('option')] = config_match.group('value')
create_config_if_dne(raw_config)
def backup_config():
"""Backs up the current config"""
copy(CONFIG_FILE, CONFIG_FILE + '.bak')
def cli():
"""Bootstraps the app"""
load_config()
backup_config()
options = parse_argv()
if getattr(options, 'only', None):
for key, value in DEFAULTS.iteritems():
if key != options.only:
setattr(options, key, value)
determine_everything(options)
if not options.skip_diff:
review_diff()
sys_exit(0)
if '__main__' == __name__:
cli()