Add ability to override configuration options

Related changes:
- Moved all non-ASCII symbols into `segment_data`
- Added --config_path, replaced nargs='*' with better action='append'
- Added g:powerline_config_path vim option
- Added ipython overrides (via additional arguments to setup() or c.Powerline)

TODO: support for non-string scalars in vim overrides.

Fixes #231
This commit is contained in:
ZyX 2013-02-18 20:11:45 +04:00 committed by Kim Silkebækken
parent c86b047ed4
commit f211bb6c74
17 changed files with 437 additions and 135 deletions

View File

@ -132,8 +132,8 @@ Common configuration is a subdictionary that is a value of ``common`` key in
``paths``
Defines additional paths which will be searched for modules when using
:ref:`module segment option <config-themes-seg-module>`. Paths defined
here have priority when searching for modules.
:ref:`module segment option <config-themes-seg-module>`. Paths defined here
have priority when searching for modules.
Extension-specific configuration
--------------------------------
@ -236,7 +236,8 @@ Themes
using :ref:`local themes <config-ext-local_themes>` values of these keys are
first searched in the segment description, then in ``segment_data`` key of
a local theme, then in ``segment_data`` key of a :ref:`default theme
<config-ext-theme>`.
<config-ext-theme>`. For the :ref:`default theme <config-ext-theme>` itself
step 2 is obviously avoided.
``segments``
A dict with a ``left`` and a ``right`` list, consisting of segment
@ -354,3 +355,74 @@ A segment function must return one of the following values:
* A list of dicts consisting of a ``contents`` string, and
a ``highlight_group`` list. This is useful for providing a particular
highlighting group depending on the segment contents.
Local configuration
===================
Depending on the application used it is possible to override configuration. Here
is the list:
Vim overrides
-------------
Vim configuration can be overridden using the following options:
``g:powerline_config_overrides``
Dictionary, recursively merged with contents of
:file:`powerline/config.json`.
``g:powerline_theme_overrides__{theme_name}``
Dictionary, recursively merged with contents of
:file:`powerline/themes/vim/{theme_name}.json`. Note that this way you cant
redefine some value (e.g. segment) in list, only the whole list itself: only
dictionaries are merged recursively.
``g:powerline_config_path``
Path (must be expanded, ``~`` shortcut is not supported). Points to the
directory which will be searched for configuration. When this option is
present, none of the other locations are searched.
Powerline script overrides
--------------------------
Powerline script has a number of options controlling powerline behavior. Here
``VALUE`` always means “some JSON object”.
``-c KEY.NESTED_KEY=VALUE`` or ``--config=KEY.NESTED_KEY=VALUE``
Overrides options from :file:`powerline/config.json`.
``KEY.KEY2.KEY3=VALUE`` is a shortcut for ``KEY={"KEY2": {"KEY3": VALUE}}``.
Multiple options (i.e. ``-c K1=V1 -c K2=V2``) are allowed, result (in the
example: ``{"K1": V1, "K2": V2}``) is recursively merged with the contents
of the file.
``-t THEME_NAME.KEY.NESTED_KEY=VALUE`` or ``--theme_option=THEME_NAME.KEY.NESTED_KEY=VALUE``
Overrides options from :file:`powerline/themes/{ext}/{THEME_NAME}.json`.
``KEY.NESTED_KEY=VALUE`` is processed like described above, ``{ext}`` is the
first argument to powerline script. May be passed multiple times.
``-p PATH`` or ``--config_path=PATH``
Sets directory where configuration should be read from. If present, no
default locations are searched for configuration. No expansions are
performed by powerline script itself, but ``-p ~/.powerline`` will likely be
expanded by the shell to something like ``-p /home/user/.powerline``.
Ipython overrides
-----------------
Ipython overrides depend on ipython version. Before ipython-0.11 you should pass
additional keyword arguments to setup() function. After ipython-0.11 you should
use ``c.Powerline.KEY``. Supported ``KEY`` strings or keyword argument names:
``config_overrides``
Overrides options from :file:`powerline/config.json`. Should be a dictionary
that will be recursively merged with the contents of the file.
``theme_overrides``
Overrides options from :file:`powerline/themes/ipython/*.json`. Should be
a dictionary where keys are theme names and values are dictionaries which
will be recursively merged with the contents of the given theme.
``path``
Sets directory where configuration should be read from. If present, no
default locations are searched for configuration. No expansions are
performed thus you cannot use paths starting with ``~/``.

View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
import importlib
import json
import os
import sys
from powerline.colorscheme import Colorscheme
from powerline.lib import underscore_to_camelcase
def load_json_config(search_paths, config_file):
config_file += '.json'
for path in search_paths:
config_file_path = os.path.join(path, config_file)
if os.path.isfile(config_file_path):
with open(config_file_path, 'r') as config_file_fp:
return json.load(config_file_fp)
raise IOError('Config file not found in search path: {0}'.format(config_file))
class Powerline(object):
def __init__(self, ext, renderer_module=None):
self.config_paths = self.get_config_paths()
# Load main config file
config = self.load_main_config()
common_config = config['common']
ext_config = config['ext'][ext]
self.ext = ext
# Load and initialize colorscheme
colorscheme_config = self.load_colorscheme_config(ext_config['colorscheme'])
colorscheme = Colorscheme(colorscheme_config)
# Load and initialize extension theme
theme_config = self.load_theme_config(ext_config.get('theme', 'default'))
common_config['paths'] = [os.path.expanduser(path) for path in common_config.get('paths', [])]
self.import_paths = common_config['paths']
theme_kwargs = {
'ext': ext,
'colorscheme': colorscheme,
'common_config': common_config,
'segment_info': self.get_segment_info(),
}
local_themes = self.get_local_themes(ext_config.get('local_themes', {}))
# Load and initialize extension renderer
renderer_module_name = renderer_module or ext
renderer_module_import = 'powerline.renderers.{0}'.format(renderer_module_name)
renderer_class_name = '{0}Renderer'.format(underscore_to_camelcase(renderer_module_name))
try:
Renderer = getattr(importlib.import_module(renderer_module_import), renderer_class_name)
except ImportError as e:
sys.stderr.write('Error while importing renderer module: {0}\n'.format(e))
sys.exit(1)
options = {'term_truecolor': common_config.get('term_24bit_colors', False)}
self.renderer = Renderer(theme_config, local_themes, theme_kwargs, **options)
def get_config_paths(self):
config_home = os.environ.get('XDG_CONFIG_HOME', os.path.join(os.path.expanduser('~'), '.config'))
config_path = os.path.join(config_home, 'powerline')
plugin_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), 'config_files')
return [config_path, plugin_path]
def load_theme_config(self, name):
return load_json_config(self.config_paths, os.path.join('themes', self.ext, name))
def load_main_config(self):
return load_json_config(self.config_paths, 'config')
def load_colorscheme_config(self, name):
return load_json_config(self.config_paths, os.path.join('colorschemes', self.ext, name))
@staticmethod
def get_local_themes(local_themes):
return {}
@staticmethod
def get_segment_info():
return None

View File

@ -1,4 +1,4 @@
from powerline.core import Powerline
from powerline.ipython import IpythonPowerline
from IPython.core.prompts import PromptManager
@ -19,8 +19,23 @@ class PowerlinePromptManager(PromptManager):
return res if color else res_nocolor
class ConfigurableIpythonPowerline(IpythonPowerline):
def __init__(self, ip):
config = ip.config.Powerline
self.config_overrides = config.get('config_overrides')
self.theme_overrides = config.get('theme_overrides', {})
self.path = config.get('path')
super(ConfigurableIpythonPowerline, self).__init__()
def load_ipython_extension(ip):
powerline = Powerline('ipython')
global old_prompt_manager
old_prompt_manager = ip.prompt_manager
powerline = ConfigurableIpythonPowerline(ip)
ip.prompt_manager = PowerlinePromptManager(powerline=powerline,
shell=ip.prompt_manager.shell, config=ip.prompt_manager.config)
def unload_ipython_extension(ip):
ip.prompt_manager = old_prompt_manager

View File

@ -1,4 +1,4 @@
from powerline.core import Powerline
from powerline.ipython import IpythonPowerline
from IPython.Prompts import BasePrompt
from IPython.ipapi import get as get_ipython
@ -18,10 +18,18 @@ class PowerlinePrompt(BasePrompt):
return '%s>%s' % ('-' * self.prompt_text_len, ' ' * self.nrspaces)
def setup(prompt='1'):
class ConfigurableIpythonPowerline(IpythonPowerline):
def __init__(self, config_overrides=None, theme_overrides={}, path=None):
self.config_overrides = config_overrides
self.theme_overrides = theme_overrides
self.path = path
super(ConfigurableIpythonPowerline, self).__init__()
def setup(prompt='1', **kwargs):
ip = get_ipython()
powerline = Powerline('ipython')
powerline = ConfigurableIpythonPowerline(**kwargs)
attr = 'prompt' + prompt

View File

@ -3,7 +3,7 @@
from libqtile import bar
from libqtile.widget import base
from powerline.core import Powerline as PowerlineCore
from powerline import Powerline as PowerlineCore
class Powerline(base._TextBox):

View File

@ -18,15 +18,16 @@ endif
let s:powerline_pycmd = substitute(get(g:, 'powerline_pycmd', 'py'), '\v^(py)%[thon](3?)$', '\1\2', '')
let s:powerline_pyeval = get(g:, 'powerline_pyeval', s:powerline_pycmd.'eval')
let s:import_cmd = 'from powerline.vim import VimPowerline'
try
exec s:powerline_pycmd 'from powerline.core import Powerline'
exec s:powerline_pycmd s:import_cmd
catch
" An error occured while importing the module, it could be installed
" outside of Python's module search paths. Update sys.path and try again.
exec s:powerline_pycmd 'import sys, vim'
exec s:powerline_pycmd 'sys.path.append(vim.eval(''expand("<sfile>:h:h:h:h:h")''))'
try
exec s:powerline_pycmd 'from powerline.core import Powerline'
exec s:powerline_pycmd s:import_cmd
catch
call s:CriticalError('An error occured while importing the Powerline package.
\ This could be caused by an invalid sys.path setting, or by an incompatible
@ -35,7 +36,8 @@ catch
finish
endtry
endtry
exec s:powerline_pycmd 'powerline = Powerline("vim", segment_info={})'
exec s:powerline_pycmd 'powerline = VimPowerline()'
exec s:powerline_pycmd 'del VimPowerline'
if !get(g:, 'powerline_debugging_pyeval') && exists('*'. s:powerline_pyeval)
let s:pyeval = function(s:powerline_pyeval)

View File

@ -1,8 +1,18 @@
import zsh
from powerline.core import Powerline
from powerline.shell import ShellPowerline
def get_var_config(var):
try:
return dict(((k, json.loads(v)) for k, v in zsh.getvalue(var).items()))
except:
return None
class Args(object):
ext = ['shell']
renderer_module = 'zsh_prompt'
@property
def last_exit_code(self):
return zsh.last_exit_code()
@ -11,6 +21,14 @@ class Args(object):
def last_pipe_status(self):
return zsh.pipestatus()
@property
def config(self):
return get_var_config('POWERLINE_CONFIG')
@property
def theme_option(self):
return get_var_config('POWERLINE_THEME_CONFIG')
class Prompt(object):
__slots__ = ('render', 'side', 'savedpsvar', 'savedps')
@ -38,6 +56,6 @@ def set_prompt(powerline, psvar, side):
def setup():
powerline = Powerline(ext='shell', renderer_module='zsh_prompt', segment_info=Args())
powerline = ShellPowerline(Args())
set_prompt(powerline, 'PS1', 'left')
set_prompt(powerline, 'RPS1', 'right')

View File

@ -1,20 +1,29 @@
{
"default_module": "powerline.segments.common",
"segment_data": {
"hostname": {
"before": " ",
"args": {
"only_if_ssh": true
}
},
"virtualenv": {
"before": "ⓔ "
},
"branch": {
"before": " "
}
},
"segments": {
"left": [
{
"name": "hostname",
"before": " ",
"args": {
"only_if_ssh": true
}
"name": "hostname"
},
{
"name": "user"
},
{
"name": "virtualenv",
"before": "ⓔ "
"name": "virtualenv"
},
{
"name": "cwd",
@ -31,8 +40,7 @@
"highlight_group": "exit_fail"
},
{
"name": "branch",
"before": " "
"name": "branch"
}
]
}

View File

@ -1,21 +1,32 @@
{
"default_module": "powerline.segments.common",
"segment_data": {
"hostname": {
"before": " ",
"args": {
"only_if_ssh": true
}
},
"virtualenv": {
"before": "ⓔ "
},
"branch": {
"before": " "
}
},
"segments": {
"left": [
{
"name": "hostname",
"before": " "
"name": "hostname"
},
{
"name": "user"
},
{
"name": "virtualenv",
"before": "ⓔ "
"name": "virtualenv"
},
{
"name": "branch",
"before": " "
"name": "branch"
},
{
"name": "cwd",

View File

@ -1,16 +1,32 @@
{
"default_module": "powerline.segments.common",
"segment_data": {
"uptime": {
"before": "⇑ "
},
"external_ip": {
"before": "ⓦ "
},
"date": {
"before": "⌚ "
},
"email_imap_alert": {
"before": "✉ ",
"args": {
"username": "",
"password": ""
}
}
},
"segments": {
"right": [
{
"name": "uptime",
"before": "⇑ ",
"priority": 50,
"divider_highlight_group": "background:divider"
},
{
"name": "external_ip",
"before": "ⓦ ",
"priority": 50,
"divider_highlight_group": "background:divider"
},
@ -35,18 +51,12 @@
{
"name": "date",
"args": {"format": "%H:%M"},
"before": "⌚ ",
"highlight_group": ["time", "date"],
"divider_highlight_group": "time:divider"
},
{
"name": "email_imap_alert",
"before": "✉ ",
"priority": 10,
"args": {
"username": "",
"password": ""
}
},
{
"name": "hostname"

View File

@ -1,86 +0,0 @@
# -*- coding: utf-8 -*-
import importlib
import json
import os
import sys
from powerline.colorscheme import Colorscheme
from powerline.matcher import Matcher
from powerline.lib import underscore_to_camelcase
class Powerline(object):
def __init__(self, ext, renderer_module=None, segment_info=None, renderer_options={}):
config_home = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
config_path = os.path.join(config_home, 'powerline')
plugin_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), 'config_files')
self.search_paths = [config_path, plugin_path]
sys.path[:0] = self.search_paths
# Load main config file
config = self._load_json_config('config')
self.config = config['common']
self.config_ext = config['ext'][ext]
# Load and initialize colorscheme
colorscheme_config = self._load_json_config(os.path.join('colorschemes', ext, self.config_ext['colorscheme']))
colorscheme = Colorscheme(colorscheme_config)
# Load and initialize extension theme
theme_config = self._load_theme_config(ext, self.config_ext.get('theme', 'default'))
self.config['paths'] = [os.path.expanduser(path) for path in self.config.get('paths', [])]
self.get_matcher = Matcher(ext, self.config['paths']).get
theme_kwargs = {
'ext': ext,
'colorscheme': colorscheme,
'common_config': self.config,
'segment_info': segment_info,
}
local_themes = {}
for key, local_theme_name in self.config_ext.get('local_themes', {}).items():
key = self.get_matcher(key)
local_themes[key] = {'config': self._load_theme_config(ext, local_theme_name)}
# Load and initialize extension renderer
renderer_module_name = renderer_module or ext
renderer_module_import = 'powerline.renderers.{0}'.format(renderer_module_name)
renderer_class_name = '{0}Renderer'.format(underscore_to_camelcase(renderer_module_name))
try:
Renderer = getattr(importlib.import_module(renderer_module_import), renderer_class_name)
except ImportError as e:
sys.stderr.write('Error while importing renderer module: {0}\n'.format(e))
sys.exit(1)
options = {'term_truecolor': self.config.get('term_24bit_colors', False)}
options.update(renderer_options)
self.renderer = Renderer(theme_config, local_themes, theme_kwargs, **options)
def add_local_theme(self, key, config):
'''Add local themes at runtime (e.g. during vim session).
Accepts key as first argument (same as keys in config.json:
ext/*/local_themes) and configuration dictionary as the second (has
format identical to themes/*/*.json)
Returns True if theme was added successfully and False if theme with
the same matcher already exists
'''
key = self.get_matcher(key)
try:
self.renderer.add_local_theme(key, {'config': config})
except KeyError:
return False
else:
return True
def _load_theme_config(self, ext, name):
return self._load_json_config(os.path.join('themes', ext, name))
def _load_json_config(self, config_file):
config_file += '.json'
for path in self.search_paths:
config_file_path = os.path.join(path, config_file)
if os.path.isfile(config_file_path):
with open(config_file_path, 'r') as config_file_fp:
return json.load(config_file_fp)
raise IOError('Config file not found in search path: {0}'.format(config_file))

27
powerline/ipython.py Normal file
View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from powerline import Powerline
from powerline.lib import mergedicts
class IpythonPowerline(Powerline):
def __init__(self):
super(IpythonPowerline, self).__init__('ipython')
def get_config_paths(self):
if self.path:
return [self.path]
else:
return super(IpythonPowerline, self).get_config_paths()
def load_main_config(self):
r = super(IpythonPowerline, self).load_main_config()
if self.config_overrides:
mergedicts(r, self.config_overrides)
return r
def load_theme_config(self, name):
r = super(IpythonPowerline, self).load_theme_config(name)
if name in self.theme_overrides:
mergedicts(r, self.theme_overrides[name])
return r

View File

@ -6,3 +6,12 @@ from powerline.lib.url import urllib_read, urllib_urlencode # NOQA
def underscore_to_camelcase(string):
'''Return a underscore_separated_string as CamelCase.'''
return ''.join(word.capitalize() or '_' for word in string.split('_'))
def mergedicts(d1, d2):
'''Recursively merge two dictionaries. First dictionary is modified in-place.
'''
for k in d2:
if k in d1 and type(d1[k]) is dict and type(d2[k]) is dict:
mergedicts(d1[k], d2[k])
else:
d1[k] = d2[k]

View File

@ -4,19 +4,16 @@ from importlib import import_module
import sys
class Matcher(object):
def __init__(self, ext, path):
self.ext = ext
self.path = path
def get(self, match_name):
def gen_matcher_getter(ext, import_paths):
def get(match_name):
match_module, separator, match_function = match_name.rpartition('.')
if not separator:
match_module = 'powerline.matchers.{0}'.format(self.ext)
match_module = 'powerline.matchers.{0}'.format(ext)
match_function = match_name
oldpath = sys.path
sys.path = self.path + sys.path
sys.path = import_paths + sys.path
try:
return getattr(import_module(match_module), match_function)
finally:
sys.path = oldpath
return get

42
powerline/shell.py Normal file
View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
from powerline import Powerline
from powerline.lib import mergedicts
def mergeargs(argvalue):
if not argvalue:
return None
l = argvalue
r = dict([argvalue[0]])
for subval in argvalue[1:]:
mergedicts(r, dict([subval]))
return r
class ShellPowerline(Powerline):
def __init__(self, args):
self.args = args
self.theme_option = mergeargs(args.theme_option) or {}
super(ShellPowerline, self).__init__(args.ext[0], args.renderer_module)
def get_segment_info(self):
return self.args
def load_main_config(self):
r = super(ShellPowerline, self).load_main_config()
if self.args.config:
mergedicts(r, mergeargs(self.args.config))
return r
def load_theme_config(self, name):
r = super(ShellPowerline, self).load_theme_config(name)
if name in self.theme_option:
mergedicts(r, self.theme_option[name])
return r
def get_config_paths(self):
if self.args.config_path:
return [self.args.config_path]
else:
return super(ShellPowerline, self).get_config_paths()

72
powerline/vim.py Normal file
View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from powerline.bindings.vim import vim_get_func
from powerline import Powerline
from powerline.lib import mergedicts
from powerline.matcher import gen_matcher_getter
import vim
vim_exists = vim_get_func('exists', rettype=int)
def _override_from(config, override_varname):
if vim_exists(override_varname):
# FIXME vim.eval has problem with numeric types, vim.bindeval may be
# absent (and requires converting values to python built-in types),
# vim.eval with typed call like the one I implemented in frawor is slow.
# Maybe eval(vime.eval('string({0})'.format(override_varname)))?
overrides = vim.eval(override_varname)
mergedicts(config, overrides)
return config
class VimPowerline(Powerline):
def __init__(self):
super(VimPowerline, self).__init__('vim')
def add_local_theme(self, key, config):
'''Add local themes at runtime (during vim session).
Accepts key as first argument (same as keys in config.json:
ext/*/local_themes) and configuration dictionary as the second (has
format identical to themes/*/*.json)
Returns True if theme was added successfully and False if theme with
the same matcher already exists
'''
key = self.get_matcher(key)
try:
self.renderer.add_local_theme(key, {'config': config})
except KeyError:
return False
else:
return True
def load_main_config(self):
return _override_from(super(VimPowerline, self).load_main_config(), 'g:powerline_config_overrides')
def load_theme_config(self, name):
# Note: themes with non-[a-zA-Z0-9_] names are impossible to override
# (though as far as I know exists() wont throw). Wont fix, use proper
# theme names.
return _override_from(super(VimPowerline, self).load_theme_config(name), 'g:powerline_theme_overrides__'+name)
def get_local_themes(self, local_themes):
self.get_matcher = gen_matcher_getter(self.ext, self.import_paths)
r = {}
for key, local_theme_name in local_themes.items():
key = self.get_matcher(key)
r[key] = {'config': self.load_theme_config(local_theme_name)}
return r
def get_config_paths(self):
if vim_exists('g:powerline_config_path'):
return [vim.eval('g:powerline_config_path')]
else:
return super(VimPowerline, self).get_config_paths()
@staticmethod
def get_segment_info():
return {}

View File

@ -6,11 +6,11 @@ import sys
import json
try:
from powerline.core import Powerline
from powerline.shell import ShellPowerline
except ImportError:
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from powerline.core import Powerline # NOQA
from powerline.shell import ShellPowerline # NOQA
def oval(s):
if '=' not in s:
@ -22,6 +22,20 @@ def oval(s):
val = json.loads(s[idx+1:])
return (o, val)
def odotval(s):
o, val = oval(s)
keys = o.split('.')
if len(keys) > 1:
r = (keys[0], {})
rcur = r[1]
for key in keys[1:-1]:
rcur[key] = {}
rcur = rcur[key]
rcur[keys[-1]] = val
return r
else:
return (o, val)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('ext', nargs=1)
parser.add_argument('side', nargs='?', choices=('left', 'right'))
@ -29,11 +43,13 @@ parser.add_argument('-r', '--renderer_module', metavar='MODULE', type=str)
parser.add_argument('-w', '--width', type=int)
parser.add_argument('--last_exit_code', metavar='INT', type=int)
parser.add_argument('--last_pipe_status', metavar='LIST', default='', type=lambda s: [int(status) for status in s.split()])
parser.add_argument('-o', '--renderer_option', nargs='*', metavar='OPTION=VALUE', type=oval)
parser.add_argument('-c', '--config', metavar='KEY.KEY=VALUE', type=odotval, action='append')
parser.add_argument('-t', '--theme_option', metavar='THEME.KEY.KEY=VALUE', type=odotval, action='append')
parser.add_argument('-p', '--config_path', metavar='PATH')
if __name__ == '__main__':
args = parser.parse_args()
powerline = Powerline(ext=args.ext[0], renderer_module=args.renderer_module, segment_info=args, renderer_options=dict(args.renderer_option or {}))
powerline = ShellPowerline(args)
rendered = powerline.renderer.render(width=args.width, side=args.side)
try:
sys.stdout.write(rendered)