Merge branch 'config-file' of ceda/Wish into master

Wish is now configured by a config file (although WISH_PLUGINS and
WISH_RIGHT_PLUGINS are still used as a fallback if no config file is
found. This way old setups won't break). Config file allows for higher
customization and re-use of plugins and is easier to maintain for
users. Add gINIe parser to Wish. Parse a gINIe config file for wish
config. Wrappers around plugins are created to allow re-use with
multiple configs and themes.
This commit is contained in:
Ceda EI 2019-12-31 13:41:47 +05:30
commit e05f37b5b9
7 changed files with 366 additions and 9 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
__pycache__/
*.pyc
config.gie

59
config.default.gie Normal file
View File

@ -0,0 +1,59 @@
# vim: set ft=dosini:
#
# Comments start with a # or ; and always exist on a line of their own.
#
# Key value pairs are in the form of key = value. Keys cannot have whitespaces
# or = in them. Values can have any character as part of them. Surrounding
# spaces in values are stripped away. To keep surrounding spaces as a part of
# the value. Although, keys are case-sensitive in gINIe, wish treats them case-
# insensitively.
#
# Block names are enclosed in [] (e.g. [core]). Block names are case sensitive.
# All key value pairs after a block starts and before the next block begins are
# considered a part of that block. All key value pairs must be in a block.
#
# Available Blocks:
#
# core: Core block configures Wish itself. Available keys are:
# - auto_newline: Automatically add a newline if last line of output
# doesn't end in newline. (0 to disable, 1 to enable)
# - theme: Wish theme.
# - powerline: Enable / Disable powerline. (0 to disable, 1 to enable)
#
# plugin: Adds a plugin to the section the block is added to. All config for
# that plugin goes there. The key "name" defines the plugin to use.
# Plugin blocks outside a section are ignored.
#
# Section names are enclosed in || (e.g. |left|). All blocks after a section
# starts and before the next section begins are considered a part of that
# section. Blocks don't necessarily need to be in a section.
#
# Available sections are left, right for left prompt and right prompt
# respectively.
[core]
auto_newline = 1
powerline = 1
theme = plain
|left|
[plugin]
name = exit_code_smiley
[plugin]
name = bg_jobs
[plugin]
name = date
format = %d %b %H:%M
[plugin]
name = path
[plugin]
name = newline
[plugin]
name = vcs
|right|

135
ginie.py Executable file
View File

@ -0,0 +1,135 @@
#!/usr/bin/env python3
"gINIe Parser for Wish"
import re
import sys
class GinieParseError(ValueError):
"Exception Class for errors in"
def loads(string):
"""
Deserialize `string` (a `str` instance containing a gINIe document)
into a Python list.
Return Value: List of `block` and `section` dicts
`block` dicts are of form
{
'type': 'block',
'name': 'name_of_block_as_in_config',
'config': {}
}
where `name` and `config` keys are based on the gINIe document.
`section` dicts are of form
{
'type': 'block',
'name': 'name_of_block_as_in_config',
'blocks': []
}
where `name` and `blocks` keys are based on the gINIe document and blocks
is a list of block dicts found in the section.
"""
config = []
section_re = re.compile(r"^\|(?P<section>.+)\|$")
block_re = re.compile(r"^\[(?P<block>[^\]]*)\]$")
line_re = re.compile(r"^\s*(?P<key>[^\s=]+)(\s*)=\s*?(?P<value>.*)$")
empty_re = re.compile(r"^\s*$")
comment_re = re.compile(r"^\s*(#|;)")
current_block = None
current_section = None
for idx, line in enumerate(string.splitlines()):
idx += 1 # Since line numbers begin with 1
# Skip comments and empty lines
if empty_re.match(line) or comment_re.match(line):
continue
# Section parsing
if line.startswith("|"):
match = section_re.match(line)
if match is None:
err = "Invalid line {}".format(idx)
raise GinieParseError(err)
current_section = {
"type": "section",
"name": match.group("section"),
"blocks": []
}
config.append(current_section)
continue
# Block Parsing
if line.startswith("["):
match = block_re.match(line)
if match is None:
err = "Invalid block name on line {}".format(idx)
raise GinieParseError(err)
current_block = {
"type": "block",
"name": match.group("block"),
"config": {}
}
if current_section is None:
config.append(current_block)
else:
current_section["blocks"].append(current_block)
continue
# If it is neither a comment, nor a section, nor a block, it has to be
# a key, value pair.
if current_block is None:
raise GinieParseError("Found lines outside a block")
match = line_re.match(line)
if match is None:
raise GinieParseError("Invalid line {}: {}".format(idx, line))
value = match.group('value').strip()
if value:
if value[0] == value[-1] == "'" or value[0] == value[-1] == '"':
value = value[1:-1]
current_block["config"][match.group('key').strip()] = value
return config
def load(file):
"""
Deserialize `file` (a file-like object containing a gINIe document)
into a Python list.
Return Value: List of `block` and `section` dicts
`block` dicts are of form
{
'type': 'block',
'name': 'name_of_block_as_in_config',
'config': {}
}
where `name` and `config` keys are based on the gINIe document.
`section` dicts are of form
{
'type': 'block',
'name': 'name_of_block_as_in_config',
'blocks': []
}
where `name` and `blocks` keys are based on the gINIe document and blocks
is a list of block dicts found in the section.
"""
return loads(file.read())
if __name__ == "__main__":
if len(sys.argv) == 1:
print("Usage: {} file.gie|-".format(sys.argv[0]))
elif sys.argv[1] == '-':
print(load(sys.stdin))
else:
with open(sys.argv[1]) as source:
print(load(source))

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
if hash git; then if hash git; then
git clone https://gitlab.com/ceda_ei/wish.git $HOME/.config/wish.git git clone https://gitlab.com/ceda_ei/Wish.git $HOME/.config/wish
else else
curl https://gitlab.com/ceda_ei/wish/-/archive/master/wish-master.tar -o /tmp/wish.tar curl https://gitlab.com/ceda_ei/wish/-/archive/master/wish-master.tar -o /tmp/wish.tar
mkdir $HOME/.config 2> /dev/null mkdir $HOME/.config 2> /dev/null
@ -10,12 +10,11 @@ else
mv wish-master/ wish/ mv wish-master/ wish/
mv wish/ $HOME/.config/ mv wish/ $HOME/.config/
fi fi
cp $HOME/.config/wish/config.default.gie $HOME/.config/wish/config.gie
cat >> ~/.bashrc <<EOF cat >> ~/.bashrc <<EOF
# Wish # Wish
WISH_CONFIG_FILE="$HOME/.config/wish/config.gie"
WISH_PLUGINS=(exit_code_smiley bg_jobs date path newline vcs) source $HOME/.config/wish/wish.sh
WISH_THEME=plain
source ~/.config/wish/wish.sh
EOF EOF

View File

@ -9,10 +9,10 @@ function wish_custom_text_end() {
function wish_custom_text_set_colors() { function wish_custom_text_set_colors() {
WISH_CUSTOM_TEXT_FG=${WISH_CUSTOM_TEXT_FG:-$WISH_DEFAULT_FG} WISH_CUSTOM_TEXT_FG=${WISH_CUSTOM_TEXT_FG:-$WISH_DEFAULT_FG}
WISH_CUSTOM_TEXT_BG=${WISH_CUSTOM_TEXT_BG:-$WISH_DEFAULT_BG} WISH_CUSTOM_TEXT_BG=${WISH_CUSTOM_TEXT_BG:-$WISH_DEFAULT_BG}
local default_text='To set custom text here, add WISH_CUSTOM_TEXT="your text" in ~/.bashrc' local default_text='To set custom text here, add text="your text" in your config'
WISH_CUSTOM_TEXT=${WISH_CUSTOM_TEXT:-$default_text} WISH_CUSTOM_TEXT_TEXT=${WISH_CUSTOM_TEXT_TEXT:-$default_text}
} }
function wish_custom_text_main() { function wish_custom_text_main() {
wish_append $WISH_CUSTOM_TEXT_BG $WISH_CUSTOM_TEXT_FG "$WISH_CUSTOM_TEXT" wish_append $WISH_CUSTOM_TEXT_BG $WISH_CUSTOM_TEXT_FG "$WISH_CUSTOM_TEXT_TEXT"
} }

140
wish.py Executable file
View File

@ -0,0 +1,140 @@
#!/usr/bin/env python3
"""
Generate function wrappers from config for wish.
wish.py works by writing plugins which are isolated from the global plugins.
It assigns all the config to relevant variables inside a wrapper. e.g.
If the config file is
```
|left|
[plugin]
name = date
format = %F
```
wish.py creates a wrapper around date as 0_date where 0 is the index of plugin.
Inside the wrapper, in each function, it sets WISH_DATE_FORMAT="%F" and
WISH_DATE_BG=$WISH_0_DATE_BG (same for FG), so the plugin sees its config as
defined in the config file. After calling the plugin's relevant function, it
sets WISH_0_DATE_BG=$WISH_DATE_BG (same for FG) in case, the plugin has changed
it). This allows multiple instances of the same plugin to coexist with
different config and different themes.
"""
from os.path import isfile
import shlex
import sys
import ginie
def parse_core(core):
"Parses a core block"
config = core["config"]
for key in core["config"]:
print("WISH_{}={}".format(key.upper(), shlex.quote(config[key])))
def parse_plugin(plugin, plugin_idx):
"Parses a plugin and writes a wrapper around it"
config = plugin["config"]
try:
plugin_name = config["name"].strip().lower()
config["name"] = plugin_name
except KeyError:
return False
keys_from_source = ("BG", "FG")
for j in ("start", "end", "set_colors", "main"):
print("function wish_{}_{}_{}()".format(plugin_idx, plugin_name, j))
print("{")
for key in keys_from_source:
print("\tlocal WISH_{0}_{2}=$WISH_{1}_{0}_{2}".format(
plugin_name.upper(),
plugin_idx,
key.upper()
))
for key in config:
if key == "name":
continue
print("\tlocal WISH_{}_{}={}".format(plugin_name.upper(),
key.upper(),
shlex.quote(config[key])))
print("\twish_{}_{} $*".format(plugin_name, j))
print("\tlocal err=$?")
for key in keys_from_source:
print("\tWISH_{1}_{0}_{2}=$WISH_{0}_{2}".format(
plugin_name.upper(),
plugin_idx,
key.upper()
))
print("\treturn $err")
print("}")
return True
def print_plugin_list(name, array, always=False):
"Prints a list of plugins as a bash array"
if array or always:
print("{}=(".format(name))
for plugin in array:
print("\t" + plugin)
print(")")
def main():
"Parse a config file passed as first argument"
config_file = []
for file_name in sys.argv[1:]:
if isfile(file_name):
with open(file_name) as file:
config_file += ginie.load(file)
else:
print("echo Invalid config file: {}".format(
shlex.quote(file_name)))
plugin_idx = 0
plugins_to_source = []
left_plugins = []
right_plugins = []
for i in config_file:
kind = i["type"]
name = i["name"]
# Parse core blocks outside a section.
# No other blocks outside a section need to be parsed for now.
if kind == "block" and name == "core":
parse_core(i)
elif kind == "section":
# All sections other than left and right are skipped.
if name not in ("left", "right"):
continue
for block in i["blocks"]:
if block["name"] == "core":
parse_core(block)
elif block["name"] == "plugin" and parse_plugin(block,
plugin_idx):
plugin_name = "{}_{}".format(plugin_idx,
block["config"]["name"])
if name == "left":
left_plugins.append(plugin_name)
else:
right_plugins.append(plugin_name)
plugins_to_source.append(block["config"]["name"])
plugin_idx += 1
print_plugin_list("WISH_PLUGINS", left_plugins)
print_plugin_list("WISH_RIGHT_PLUGINS", right_plugins)
print_plugin_list("WISH_PLUGINS_SOURCE", plugins_to_source)
# Python 2 patch
# TODO: Write a better patch for Python 2
try:
shlex.quote
except AttributeError:
shlex.quote = lambda x: x
if __name__ == "__main__":
main()

23
wish.sh
View File

@ -10,10 +10,31 @@ function wish_print_right_prompt() {
} }
function wish_init() { function wish_init() {
# Find default config file if WISH_CONFIG_FILE is unset
if [[ ! -v WISH_CONFIG_FILE ]]; then
for path in "$XDG_CONFIG_HOME" "/usr/share" "$HOME/.config"; do
if [[ -f "$path/wish/config.gie" ]]; then
WISH_CONFIG_FILE="$path/wish/config.gie"
break
fi
done
fi
# Source config files
for path in "$XDG_CONFIG_HOME" "/usr/share" "$HOME/.config"; do
if [[ -f "$path/wish/wish.py" ]]; then
source <($path/wish/wish.py ${WISH_CONFIG_FILE[@]})
break
fi
done
# Source all plugins # Source all plugins
# If WISH_CONFIG_FILE is not set, then assume that the user hasn't updated
# to a config file yet. Set WISH_PLUGINS_SOURCE=WISH_PLUGINS.
if [[ ! -v WISH_CONFIG_FILE ]]; then
WISH_PLUGINS_SOURCE=("${WISH_PLUGINS[@]}" "${WISH_RIGHT_PLUGINS[@]}")
fi
local plugin local plugin
local path local path
for plugin in ${WISH_PLUGINS[@]} ${WISH_RIGHT_PLUGINS[@]}; do for plugin in ${WISH_PLUGINS_SOURCE[@]}; do
for path in "$XDG_CONFIG_HOME" "/usr/share" "$HOME/.config"; do for path in "$XDG_CONFIG_HOME" "/usr/share" "$HOME/.config"; do
source "$path/wish/plugins/$plugin.sh" &> /dev/null && break source "$path/wish/plugins/$plugin.sh" &> /dev/null && break
done done