Merge pull request #958 from ZyX-I/theme-hierarchy

Implement theme hierarchy
This commit is contained in:
Nikolai Aleksandrovich Pavlov 2014-08-06 13:04:50 +04:00
commit 22fdc10534
28 changed files with 1290 additions and 398 deletions

View File

@ -17,9 +17,11 @@ Powerline provides default configurations in the following locations:
:file:`powerline/config.json` :file:`powerline/config.json`
:ref:`Colorschemes <config-colors>` :ref:`Colorschemes <config-colors>`
:file:`powerline/colorschemes/{name}.json`, :file:`powerline/colorschemes/{name}.json`,
:file:`powerline/colorscheme/__main__.json`, :file:`powerline/colorscheme/{extension}/__main__.json`,
:file:`powerline/colorschemes/{extension}/{name}.json` :file:`powerline/colorschemes/{extension}/{name}.json`
:ref:`Themes <config-themes>` :ref:`Themes <config-themes>`
:file:`powerline/themes/{top_theme}.json`,
:file:`powerline/themes/{extension}/__main__.json`,
:file:`powerline/themes/{extension}/default.json` :file:`powerline/themes/{extension}/default.json`
The default configuration files are stored in the main package. User The default configuration files are stored in the main package. User
@ -48,6 +50,10 @@ overrides <local-configuration-overrides>`.
corresponding values are both dictionaries in which case these dictionaries corresponding values are both dictionaries in which case these dictionaries
are merged and key is assigned the result of the merge. are merged and key is assigned the result of the merge.
.. note:: Some configuration files (i.e. themes and colorschemes) have two level
of merging: first happens merging described above, second theme- or
colorscheme-specific merging happens.
.. _quick-guide: .. _quick-guide:
Quick setup guide Quick setup guide

View File

@ -58,15 +58,6 @@ Common configuration is a subdictionary that is a value of ``common`` key in
codes thus rendering powerline prompt colorless. Valid values: ``"tmux"``, codes thus rendering powerline prompt colorless. Valid values: ``"tmux"``,
``"screen"``, ``null`` (default). ``"screen"``, ``null`` (default).
``dividers``
Defines the dividers used in all Powerline extensions. This option
should usually only be changed if you don't have a patched font, or if
you use a font patched with the legacy font patcher.
The ``hard`` dividers are used to divide segments with different
background colors, while the ``soft`` dividers are used to divide
segments with the same background color.
.. _config-common-paths: .. _config-common-paths:
``paths`` ``paths``
@ -95,6 +86,12 @@ Common configuration is a subdictionary that is a value of ``common`` key in
Boolean, determines whether configuration should be reloaded at all. Boolean, determines whether configuration should be reloaded at all.
Defaults to ``True``. Defaults to ``True``.
.. _config-common-default_top_theme:
``default_top_theme``
String, determines which top-level theme will be used as the default.
Defaults to ``powerline``. See `Themes`_ section for more details.
Extension-specific configuration Extension-specific configuration
-------------------------------- --------------------------------
@ -109,6 +106,12 @@ Common configuration is a subdictionary that is a value of ``ext`` key in
Defines the theme used for this extension. Defines the theme used for this extension.
``top_theme``
.. _config-ext-top_theme:
Defines the top-level theme used for this extension. See `Themes`_ section
for more details.
``local_themes`` ``local_themes``
.. _config-ext-local_themes: .. _config-ext-local_themes:
@ -155,7 +158,7 @@ Colorschemes
============ ============
:Location: :file:`powerline/colorschemes/{name}.json`, :Location: :file:`powerline/colorschemes/{name}.json`,
:file:`powerline/colorscheme/__main__.json`, :file:`powerline/colorschemes/__main__.json`,
:file:`powerline/colorschemes/{extension}/{name}.json` :file:`powerline/colorschemes/{extension}/{name}.json`
Colorscheme files are processed in order given: definitions from each next file Colorscheme files are processed in order given: definitions from each next file
@ -213,7 +216,28 @@ override those from each previous file. It is required that either
Themes Themes
====== ======
:Location: :file:`powerline/themes/{extension}/{name}.json` :Location: :file:`powerline/themes/{top_theme}.json`,
:file:`powerline/themes/__main__.json`,
:file:`powerline/themes/{extension}/{name}.json`
Theme files are processed in order given: definitions from each next file
override those from each previous file. It is required that file
:file:`powerline/themes/{extension}/{name}.json` exists.
`{top_theme}` component of the file name is obtained either from :ref:`top_theme
extension-specific key <config-ext-top_theme>` or from :ref:`default_top_theme
common configuration key <config-common-default_top_theme>`. Powerline ships
with the following top themes:
========================== ====================================================
Theme Description
========================== ====================================================
powerline Default powerline theme with fancy powerline symbols
unicode Theme without any symbols from private use area
unicode_terminus Theme containing only symbols from terminus PCF font
unicode_terminus_condensed Like above, but occupies as less space as possible
ascii Theme without any unicode characters at all
========================== ====================================================
``name`` ``name``
Name of the theme. Name of the theme.
@ -223,23 +247,45 @@ Themes
``default_module`` ``default_module``
Python module where segments will be looked by default. Python module where segments will be looked by default.
``spaces``
Defines number of spaces just before the divider (on the right side) or just
after it (on the left side). These spaces will not be added if divider is
not drawn.
``dividers``
Defines the dividers used in all Powerline extensions. This option
should usually only be changed if you don't have a patched font, or if
you use a font patched with the legacy font patcher.
The ``hard`` dividers are used to divide segments with different
background colors, while the ``soft`` dividers are used to divide
segments with the same background color.
.. _config-themes-segment_data: .. _config-themes-segment_data:
``segment_data`` ``segment_data``
A dict where keys are segment names or strings ``{module}.{name}``. Used to A dict where keys are segment names or strings ``{module}.{name}``. Used to
specify default values for various keys: specify default values for various keys:
:ref:`after <config-themes-seg-after>`, :ref:`after <config-themes-seg-after>`,
:ref:`args <config-themes-seg-args>` (only for function segments),
:ref:`before <config-themes-seg-before>`, :ref:`before <config-themes-seg-before>`,
:ref:`contents <config-themes-seg-contents>` (only for string segments :ref:`contents <config-themes-seg-contents>` (only for string segments
if :ref:`name <config-themes-seg-name>` is defined), if :ref:`name <config-themes-seg-name>` is defined),
:ref:`display <config-themes-seg-display>`. :ref:`display <config-themes-seg-display>`.
Key :ref:`args <config-themes-seg-args>` (only for function and
segments_list segments) is handled specially: unlike other values it is
merged with all other values, except that a single ``{module}.{name}`` key
if found prevents merging all ``{name}`` values.
When using :ref:`local themes <config-ext-local_themes>` values of these When using :ref:`local themes <config-ext-local_themes>` values of these
keys are first searched in the segment description, then in ``segment_data`` 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 key of a local theme, then in ``segment_data`` key of a :ref:`default theme
<config-ext-theme>`. For the :ref:`default theme <config-ext-theme>` itself <config-ext-theme>`. For the :ref:`default theme <config-ext-theme>` itself
step 2 is obviously avoided. step 2 is obviously avoided.
.. note:: Top-level themes are out of equation here: they are merged
before the above merging process happens.
``segments`` ``segments``
A dict with a ``left`` and a ``right`` lists, consisting of segment A dict with a ``left`` and a ``right`` lists, consisting of segment
dictionaries. Shell themes may also contain ``above`` list of dictionaries. dictionaries. Shell themes may also contain ``above`` list of dictionaries.
@ -259,8 +305,8 @@ Themes
Each segment dictionary has the following options: Each segment dictionary has the following options:
``type`` ``type``
The segment type. Can be one of ``function`` (default), ``string`` The segment type. Can be one of ``function`` (default), ``string``,
or ``filler``: ``filler`` or ``segments_list``:
``function`` ``function``
The segment contents is the return value of the function defined The segment contents is the return value of the function defined
@ -367,11 +413,12 @@ Themes
*not* included in any modes, *except* for the modes in this list. *not* included in any modes, *except* for the modes in this list.
``display`` ``display``
.. _config-themes-seg-display: .. _config-themes-seg-display:
Boolean. If false disables displaying of the segment. Boolean. If false disables displaying of the segment.
Defaults to ``True``. Defaults to ``True``.
``segments`` ``segments``
.. _config-themes-seg-segments:
A list of subsegments. A list of subsegments.

View File

@ -215,6 +215,7 @@ def finish_common_config(common_config):
paths. paths.
''' '''
common_config = common_config.copy() common_config = common_config.copy()
common_config.setdefault('default_top_theme', 'powerline')
common_config.setdefault('paths', []) common_config.setdefault('paths', [])
common_config.setdefault('watcher', 'auto') common_config.setdefault('watcher', 'auto')
common_config.setdefault('log_level', 'WARNING') common_config.setdefault('log_level', 'WARNING')
@ -345,13 +346,15 @@ class Powerline(object):
if load_main: if load_main:
self._purge_configs('main') self._purge_configs('main')
config = self.load_main_config() config = self.load_main_config()
self.common_config = config['common'] self.common_config = finish_common_config(config['common'])
if self.common_config != self.prev_common_config: if self.common_config != self.prev_common_config:
common_config_differs = True common_config_differs = True
self.prev_common_config = self.common_config load_theme = (load_theme
or not self.prev_common_config
or self.prev_common_config['default_top_theme'] != self.common_config['default_top_theme'])
self.common_config = finish_common_config(self.common_config) self.prev_common_config = self.common_config
self.import_paths = self.common_config['paths'] self.import_paths = self.common_config['paths']
@ -386,6 +389,8 @@ class Powerline(object):
if interval is not None and not self.config_loader.is_alive(): if interval is not None and not self.config_loader.is_alive():
self.config_loader.start() self.config_loader.start()
self.default_top_theme = self.common_config['default_top_theme']
self.ext_config = config['ext'][self.ext] self.ext_config = config['ext'][self.ext]
if self.ext_config != self.prev_ext_config: if self.ext_config != self.prev_ext_config:
ext_config_differs = True ext_config_differs = True
@ -445,30 +450,20 @@ class Powerline(object):
''' '''
return get_config_paths() return get_config_paths()
def _load_config(self, cfg_path, type): def _load_config(self, cfg_path, cfg_type):
'''Load configuration and setup watches.''' '''Load configuration and setup watches.'''
return load_config( return load_config(
cfg_path, cfg_path,
self.find_config_files, self.find_config_files,
self.config_loader, self.config_loader,
self.cr_callbacks[type] self.cr_callbacks[cfg_type]
) )
def _purge_configs(self, type): def _purge_configs(self, cfg_type):
function = self.cr_callbacks[type] function = self.cr_callbacks[cfg_type]
self.config_loader.unregister_functions(set((function,))) self.config_loader.unregister_functions(set((function,)))
self.config_loader.unregister_missing(set(((self.find_config_files, function),))) self.config_loader.unregister_missing(set(((self.find_config_files, function),)))
def load_theme_config(self, name):
'''Get theme configuration.
:param str name:
Name of the theme to load.
:return: dictionary with :ref:`theme configuration <config-themes>`
'''
return self._load_config(os.path.join('themes', self.ext, name), 'theme')
def load_main_config(self): def load_main_config(self):
'''Get top-level configuration. '''Get top-level configuration.
@ -476,6 +471,47 @@ class Powerline(object):
''' '''
return self._load_config('config', 'main') return self._load_config('config', 'main')
def _load_hierarhical_config(self, cfg_type, levels, ignore_levels):
'''Load and merge multiple configuration files
:param str cfg_type:
Type of the loaded configuration files (e.g. ``colorscheme``,
``theme``).
:param list levels:
Configuration names resembling levels in hierarchy, sorted by
priority. Configuration file names with higher priority should go
last.
:param set ignore_levels:
If only files listed in this variable are present then configuration
file is considered not loaded: at least one file on the level not
listed in this variable must be present.
'''
config = {}
loaded = 0
exceptions = []
for i, cfg_path in enumerate(levels):
try:
lvl_config = self._load_config(cfg_path, cfg_type)
except IOError as e:
if sys.version_info < (3,):
tb = sys.exc_info()[2]
exceptions.append((e, tb))
else:
exceptions.append(e)
else:
if i not in ignore_levels:
loaded += 1
mergedicts(config, lvl_config)
if not loaded:
for exception in exceptions:
if type(exception) is tuple:
e = exception[0]
else:
e = exception
self.exception('Failed to load %s: {0}' % cfg_type, e, exception=exception)
raise e
return config
def load_colorscheme_config(self, name): def load_colorscheme_config(self, name):
'''Get colorscheme. '''Get colorscheme.
@ -484,40 +520,27 @@ class Powerline(object):
:return: dictionary with :ref:`colorscheme configuration <config-colorschemes>`. :return: dictionary with :ref:`colorscheme configuration <config-colorschemes>`.
''' '''
# TODO Make sure no colorscheme name ends with __ (do it in
# powerline-lint)
levels = ( levels = (
os.path.join('colorschemes', name), os.path.join('colorschemes', name),
os.path.join('colorschemes', self.ext, '__main__'), os.path.join('colorschemes', self.ext, '__main__'),
os.path.join('colorschemes', self.ext, name), os.path.join('colorschemes', self.ext, name),
) )
config = {} return self._load_hierarhical_config('colorscheme', levels, (1,))
loaded = 0
exceptions = [] def load_theme_config(self, name):
for cfg_path in levels: '''Get theme configuration.
try:
lvl_config = self._load_config(cfg_path, 'colorscheme') :param str name:
except IOError as e: Name of the theme to load.
if sys.version_info < (3,):
tb = sys.exc_info()[2] :return: dictionary with :ref:`theme configuration <config-themes>`
exceptions.append((e, tb)) '''
else: levels = (
exceptions.append(e) os.path.join('themes', self.ext_config.get('top_theme') or self.default_top_theme),
else: os.path.join('themes', self.ext, '__main__'),
if not cfg_path.endswith('__'): os.path.join('themes', self.ext, name),
loaded += 1 )
# TODO Either make sure `attr` list is always present or make return self._load_hierarhical_config('theme', levels, (0, 1,))
# mergedicts not merge group definitions.
mergedicts(config, lvl_config)
if not loaded:
for exception in exceptions:
if type(exception) is tuple:
e = exception[0]
else:
e = exception
self.exception('Failed to load colorscheme: {0}', e, exception=exception)
raise e
return config
def load_colors_config(self): def load_colors_config(self):
'''Get colorscheme. '''Get colorscheme.

View File

@ -1,17 +1,6 @@
{ {
"common": { "common": {
"term_truecolor": false, "term_truecolor": false
"dividers": {
"left": {
"hard": " ",
"soft": " "
},
"right": {
"hard": " ",
"soft": " "
}
},
"spaces": 1
}, },
"ext": { "ext": {
"ipython": { "ipython": {

View File

@ -0,0 +1,108 @@
{
"dividers": {
"left": {
"hard": " ",
"soft": "| "
},
"right": {
"hard": " ",
"soft": " |"
}
},
"spaces": 1,
"segment_data": {
"branch": {
"before": "BR "
},
"line_current_symbol": {
"contents": "LN "
},
"powerline.segments.common.cwd": {
"args": {
"ellipsis": "..."
}
},
"powerline.segments.common.network_load": {
"args": {
"recv_format": "DL {value:>8}",
"sent_format": "UL {value:>8}"
}
},
"powerline.segments.common.now_playing": {
"args": {
"state_symbols": {
"fallback": "",
"play": ">",
"pause": "~",
"stop": "X"
}
}
},
"powerline.segments.common.battery": {
"args": {
"full_heart": "O",
"empty_heart": "O"
}
},
"powerline.segments.common.uptime": {
"before": "UP "
},
"powerline.segments.common.date": {
"before": ""
},
"powerline.segments.common.email_imap_alert": {
"before": "MAIL "
},
"powerline.segments.common.virtualenv": {
"before": "(e) "
},
"powerline.segments.common.hostname": {
"before": "H "
},
"powerline.segments.vim.mode": {
"args": {
"override": {
"n": "NORMAL",
"no": "N-OPER",
"v": "VISUAL",
"V": "V-LINE",
"^V": "V-BLCK",
"s": "SELECT",
"S": "S-LINE",
"^S": "S-BLCK",
"i": "INSERT",
"R": "REPLACE",
"Rv": "V-RPLCE",
"c": "COMMND",
"cv": "VIM EX",
"ce": "EX",
"r": "PROMPT",
"rm": "MORE",
"r?": "CONFIRM",
"!": "SHELL"
}
}
},
"powerline.segments.vim.visual_range": {
"args": {
"CTRL_V_text": "{rows} x {vcols}",
"v_text_oneline": "C:{vcols}",
"v_text_multiline": "L:{rows}",
"V_text": "L:{rows}"
}
},
"powerline.segments.vim.readonly_indicator": {
"args": {
"text": "RO"
}
},
"powerline.segments.vim.modified_indicator": {
"args": {
"text": "+"
}
}
}
}

View File

@ -0,0 +1,108 @@
{
"dividers": {
"left": {
"hard": " ",
"soft": " "
},
"right": {
"hard": " ",
"soft": " "
}
},
"spaces": 1,
"segment_data": {
"branch": {
"before": " "
},
"line_current_symbol": {
"contents": " "
},
"powerline.segments.common.cwd": {
"args": {
"ellipsis": "⋯"
}
},
"powerline.segments.common.network_load": {
"args": {
"recv_format": "⬇ {value:>8}",
"sent_format": "⬆ {value:>8}"
}
},
"powerline.segments.common.now_playing": {
"args": {
"state_symbols": {
"fallback": "♫",
"play": "▶",
"pause": "▮▮",
"stop": "■"
}
}
},
"powerline.segments.common.battery": {
"args": {
"full_heart": "♥",
"empty_heart": "♥"
}
},
"powerline.segments.common.uptime": {
"before": "⇑ "
},
"powerline.segments.common.date": {
"before": "⌚ "
},
"powerline.segments.common.email_imap_alert": {
"before": "✉ "
},
"powerline.segments.common.virtualenv": {
"before": "ⓔ "
},
"powerline.segments.common.hostname": {
"before": " "
},
"powerline.segments.vim.mode": {
"args": {
"override": {
"n": "NORMAL",
"no": "N·OPER",
"v": "VISUAL",
"V": "V·LINE",
"^V": "V·BLCK",
"s": "SELECT",
"S": "S·LINE",
"^S": "S·BLCK",
"i": "INSERT",
"R": "REPLACE",
"Rv": "V·RPLCE",
"c": "COMMND",
"cv": "VIM EX",
"ce": "EX",
"r": "PROMPT",
"rm": "MORE",
"r?": "CONFIRM",
"!": "SHELL"
}
}
},
"powerline.segments.vim.visual_range": {
"args": {
"CTRL_V_text": "{rows} × {vcols}",
"v_text_oneline": "C:{vcols}",
"v_text_multiline": "L:{rows}",
"V_text": "L:{rows}"
}
},
"powerline.segments.vim.readonly_indicator": {
"args": {
"text": ""
}
},
"powerline.segments.vim.modified_indicator": {
"args": {
"text": "+"
}
}
}
}

View File

@ -0,0 +1,14 @@
{
"segment_data": {
"hostname": {
"args": {
"only_if_ssh": true
}
},
"powerline.segments.common.cwd": {
"args": {
"dir_limit_depth": 3
}
}
}
}

View File

@ -1,19 +1,5 @@
{ {
"default_module": "powerline.segments.common", "default_module": "powerline.segments.common",
"segment_data": {
"hostname": {
"before": " ",
"args": {
"only_if_ssh": true
}
},
"virtualenv": {
"before": "ⓔ "
},
"branch": {
"before": " "
}
},
"segments": { "segments": {
"left": [ "left": [
{ {
@ -30,10 +16,7 @@
"name": "virtualenv" "name": "virtualenv"
}, },
{ {
"name": "cwd", "name": "cwd"
"args": {
"dir_limit_depth": 3
}
}, },
{ {
"module": "powerline.segments.shell", "module": "powerline.segments.shell",

View File

@ -1,19 +1,5 @@
{ {
"default_module": "powerline.segments.common", "default_module": "powerline.segments.common",
"segment_data": {
"hostname": {
"before": " ",
"args": {
"only_if_ssh": true
}
},
"virtualenv": {
"before": "ⓔ "
},
"branch": {
"before": " "
}
},
"segments": { "segments": {
"left": [ "left": [
{ {
@ -29,10 +15,7 @@
"name": "branch" "name": "branch"
}, },
{ {
"name": "cwd", "name": "cwd"
"args": {
"dir_limit_depth": 3
}
}, },
{ {
"module": "powerline.segments.shell", "module": "powerline.segments.shell",

View File

@ -1,13 +1,5 @@
{ {
"default_module": "powerline.segments.common", "default_module": "powerline.segments.common",
"segment_data": {
"uptime": {
"before": "⇑ "
},
"date": {
"before": "⌚ "
}
},
"segments": { "segments": {
"right": [ "right": [
{ {
@ -19,7 +11,8 @@
"priority": 50 "priority": 50
}, },
{ {
"name": "date" "name": "date",
"before": ""
}, },
{ {
"name": "date", "name": "date",

View File

@ -0,0 +1,108 @@
{
"dividers": {
"left": {
"hard": "▌ ",
"soft": "│ "
},
"right": {
"hard": " ▐",
"soft": " │"
}
},
"spaces": 1,
"segment_data": {
"branch": {
"before": "⎇ "
},
"line_current_symbol": {
"contents": "␤ "
},
"powerline.segments.common.cwd": {
"args": {
"ellipsis": "⋯"
}
},
"powerline.segments.common.network_load": {
"args": {
"recv_format": "⬇ {value:>8}",
"sent_format": "⬆ {value:>8}"
}
},
"powerline.segments.common.now_playing": {
"args": {
"state_symbols": {
"fallback": "♫",
"play": "▶",
"pause": "▮▮",
"stop": "■"
}
}
},
"powerline.segments.common.battery": {
"args": {
"full_heart": "♥",
"empty_heart": "♥"
}
},
"powerline.segments.common.uptime": {
"before": "⇑ "
},
"powerline.segments.common.date": {
"before": "⌚ "
},
"powerline.segments.common.email_imap_alert": {
"before": "✉ "
},
"powerline.segments.common.virtualenv": {
"before": "ⓔ "
},
"powerline.segments.common.hostname": {
"before": "⌂ "
},
"powerline.segments.vim.mode": {
"args": {
"override": {
"n": "NORMAL",
"no": "N·OPER",
"v": "VISUAL",
"V": "V·LINE",
"^V": "V·BLCK",
"s": "SELECT",
"S": "S·LINE",
"^S": "S·BLCK",
"i": "INSERT",
"R": "REPLACE",
"Rv": "V·RPLCE",
"c": "COMMND",
"cv": "VIM EX",
"ce": "EX",
"r": "PROMPT",
"rm": "MORE",
"r?": "CONFIRM",
"!": "SHELL"
}
}
},
"powerline.segments.vim.visual_range": {
"args": {
"CTRL_V_text": "{rows} × {vcols}",
"v_text_oneline": "C:{vcols}",
"v_text_multiline": "L:{rows}",
"V_text": "L:{rows}"
}
},
"powerline.segments.vim.readonly_indicator": {
"args": {
"text": "⊗"
}
},
"powerline.segments.vim.modified_indicator": {
"args": {
"text": "+"
}
}
}
}

View File

@ -0,0 +1,108 @@
{
"dividers": {
"left": {
"hard": "▌ ",
"soft": "│ "
},
"right": {
"hard": " ▐",
"soft": " │"
}
},
"spaces": 1,
"segment_data": {
"branch": {
"before": "BR "
},
"line_current_symbol": {
"contents": "␤ "
},
"powerline.segments.common.cwd": {
"args": {
"ellipsis": "…"
}
},
"powerline.segments.common.network_load": {
"args": {
"recv_format": "⇓ {value:>8}",
"sent_format": "⇑ {value:>8}"
}
},
"powerline.segments.common.now_playing": {
"args": {
"state_symbols": {
"fallback": "♫",
"play": "▶",
"pause": "▮▮",
"stop": "■"
}
}
},
"powerline.segments.common.battery": {
"args": {
"full_heart": "♥",
"empty_heart": "♥"
}
},
"powerline.segments.common.uptime": {
"before": "↑ "
},
"powerline.segments.common.date": {
"before": ""
},
"powerline.segments.common.email_imap_alert": {
"before": "MAIL "
},
"powerline.segments.common.virtualenv": {
"before": "(e) "
},
"powerline.segments.common.hostname": {
"before": "⌂ "
},
"powerline.segments.vim.mode": {
"args": {
"override": {
"n": "NORMAL",
"no": "N·OPER",
"v": "VISUAL",
"V": "V·LINE",
"^V": "V·BLCK",
"s": "SELECT",
"S": "S·LINE",
"^S": "S·BLCK",
"i": "INSERT",
"R": "REPLACE",
"Rv": "V·RPLCE",
"c": "COMMND",
"cv": "VIM EX",
"ce": "EX",
"r": "PROMPT",
"rm": "MORE",
"r?": "CONFIRM",
"!": "SHELL"
}
}
},
"powerline.segments.vim.visual_range": {
"args": {
"CTRL_V_text": "{rows} × {vcols}",
"v_text_oneline": "C:{vcols}",
"v_text_multiline": "L:{rows}",
"V_text": "L:{rows}"
}
},
"powerline.segments.vim.readonly_indicator": {
"args": {
"text": "RO"
}
},
"powerline.segments.vim.modified_indicator": {
"args": {
"text": "+"
}
}
}
}

View File

@ -0,0 +1,109 @@
{
"dividers": {
"left": {
"hard": "▌",
"soft": "│"
},
"right": {
"hard": "▐",
"soft": "│"
}
},
"spaces": 0,
"segment_data": {
"branch": {
"before": "B "
},
"line_current_symbol": {
"contents": "␤"
},
"powerline.segments.common.cwd": {
"args": {
"use_path_separator": true,
"ellipsis": "…"
}
},
"powerline.segments.common.network_load": {
"args": {
"recv_format": "⇓{value:>8}",
"sent_format": "⇑{value:>8}"
}
},
"powerline.segments.common.now_playing": {
"args": {
"state_symbols": {
"fallback": "♫",
"play": "▶",
"pause": "▮▮",
"stop": "■"
}
}
},
"powerline.segments.common.battery": {
"args": {
"full_heart": "♥",
"empty_heart": "♥"
}
},
"powerline.segments.common.uptime": {
"before": "↑"
},
"powerline.segments.common.date": {
"before": ""
},
"powerline.segments.common.email_imap_alert": {
"before": "M "
},
"powerline.segments.common.virtualenv": {
"before": "E "
},
"powerline.segments.common.hostname": {
"before": "⌂"
},
"powerline.segments.vim.mode": {
"args": {
"override": {
"n": "NML",
"no": "NOP",
"v": "VIS",
"V": "VLN",
"^V": "VBL",
"s": "SEL",
"S": "SLN",
"^S": "SBL",
"i": "INS",
"R": "REP",
"Rv": "VRP",
"c": "CMD",
"cv": "VEX",
"ce": " EX",
"r": "PRT",
"rm": "MOR",
"r?": "CON",
"!": " SH"
}
}
},
"powerline.segments.vim.visual_range": {
"args": {
"CTRL_V_text": "{rows}×{vcols}",
"v_text_oneline": "C:{vcols}",
"v_text_multiline": "L:{rows}",
"V_text": "L:{rows}"
}
},
"powerline.segments.vim.readonly_indicator": {
"args": {
"text": "RO"
}
},
"powerline.segments.vim.modified_indicator": {
"args": {
"text": "+"
}
}
}
}

View File

@ -0,0 +1,10 @@
{
"segment_data": {
"line_percent": {
"args": {
"gradient": true
},
"after": "%"
}
}
}

View File

@ -1,19 +1,4 @@
{ {
"segment_data": {
"branch": {
"before": " "
},
"modified_indicator": {
"args": { "text": "+" }
},
"line_percent": {
"args": { "gradient": true },
"after": "%"
},
"line_current_symbol": {
"contents": " "
}
},
"segments": { "segments": {
"left": [ "left": [
{ {

View File

@ -7,19 +7,18 @@
"priority": 50 "priority": 50
}, },
{ {
"name": "date" "name": "date",
"before": ""
}, },
{ {
"name": "date", "name": "date",
"args": { "args": {
"format": "%H:%M", "format": "%H:%M",
"istime": true "istime": true
}, }
"before": "⌚ "
}, },
{ {
"name": "email_imap_alert", "name": "email_imap_alert",
"before": "✉ ",
"priority": 10, "priority": 10,
"args": { "args": {
"username": "", "username": "",

View File

@ -2,9 +2,11 @@
from __future__ import absolute_import from __future__ import absolute_import
from powerline.lib.monotonic import monotonic
from threading import Thread, Lock, Event from threading import Thread, Lock, Event
from types import MethodType
from powerline.lib.monotonic import monotonic
from powerline.segments import Segment
class MultiRunnedThread(object): class MultiRunnedThread(object):
@ -28,12 +30,14 @@ class MultiRunnedThread(object):
return None return None
class ThreadedSegment(MultiRunnedThread): class ThreadedSegment(Segment, MultiRunnedThread):
min_sleep_time = 0.1 min_sleep_time = 0.1
update_first = True update_first = True
interval = 1 interval = 1
daemon = False daemon = False
argmethods = ('render', 'set_state')
def __init__(self): def __init__(self):
super(ThreadedSegment, self).__init__() super(ThreadedSegment, self).__init__()
self.run_once = True self.run_once = True
@ -145,10 +149,34 @@ class ThreadedSegment(MultiRunnedThread):
def debug(self, *args, **kwargs): def debug(self, *args, **kwargs):
self.pl.debug(prefix=self.__class__.__name__, *args, **kwargs) self.pl.debug(prefix=self.__class__.__name__, *args, **kwargs)
def argspecobjs(self):
for name in self.argmethods:
try:
yield name, getattr(self, name)
except AttributeError:
pass
def additional_args(self):
return (('interval', self.interval),)
def omitted_args(self, name, method):
if isinstance(getattr(self, name, None), MethodType):
omitted_indexes = (0,)
else:
omitted_indexes = ()
if name.startswith('render'):
if omitted_indexes:
omitted_indexes += (1,)
else:
omitted_indexes = (0,)
return omitted_indexes
class KwThreadedSegment(ThreadedSegment): class KwThreadedSegment(ThreadedSegment):
update_first = True update_first = True
argmethods = ('render', 'set_state', 'key', 'render_one')
def __init__(self): def __init__(self):
super(KwThreadedSegment, self).__init__() super(KwThreadedSegment, self).__init__()
self.updated = True self.updated = True

View File

@ -431,9 +431,6 @@ class WithPath(object):
def check_matcher_func(ext, match_name, data, context, echoerr): def check_matcher_func(ext, match_name, data, context, echoerr):
import_paths = [os.path.expanduser(path) for path in context[0][1].get('common', {}).get('paths', [])] import_paths = [os.path.expanduser(path) for path in context[0][1].get('common', {}).get('paths', [])]
if match_name == '__tabline__':
return True, False
match_module, separator, match_function = match_name.rpartition('.') match_module, separator, match_function = match_name.rpartition('.')
if not separator: if not separator:
match_module = 'powerline.matchers.{0}'.format(ext) match_module = 'powerline.matchers.{0}'.format(ext)
@ -512,23 +509,30 @@ def check_config(d, theme, data, context, echoerr):
return True, False, False return True, False, False
def check_top_theme(theme, data, context, echoerr):
if theme not in data['configs']['top_themes']:
echoerr(context='Error while checking extension configuration (key {key})'.format(key=context_key(context)),
context_mark=context[-2][0].mark,
problem='failed to find top theme {0}'.format(theme),
problem_mark=theme.mark)
return True, False, True
return True, False, False
divider_spec = Spec().type(unicode).len('le', 3, divider_spec = Spec().type(unicode).len('le', 3,
lambda value: 'Divider {0!r} is too large!'.format(value)).copy lambda value: 'Divider {0!r} is too large!'.format(value)).copy
divside_spec = Spec( ext_theme_spec = Spec().type(unicode).func(lambda *args: check_config('themes', *args)).copy
hard=divider_spec(), top_theme_spec = Spec().type(unicode).func(check_top_theme).copy
soft=divider_spec(), ext_spec = Spec(
colorscheme=Spec().type(unicode).func(
(lambda *args: check_config('colorschemes', *args))
),
theme=ext_theme_spec(),
top_theme=top_theme_spec().optional(),
).copy ).copy
colorscheme_spec = Spec().type(unicode).func(lambda *args: check_config('colorschemes', *args)).copy
theme_spec = Spec().type(unicode).func(lambda *args: check_config('themes', *args)).copy
main_spec = (Spec( main_spec = (Spec(
common=Spec( common=Spec(
dividers=Spec( default_top_theme=top_theme_spec().optional(),
left=divside_spec(),
right=divside_spec(),
),
spaces=Spec().unsigned().cmp(
'le', 2, lambda value: 'Are you sure you need such a big ({0}) number of spaces?'.format(value)
),
term_truecolor=Spec().type(bool).optional(), term_truecolor=Spec().type(bool).optional(),
# Python is capable of loading from zip archives. Thus checking path # Python is capable of loading from zip archives. Thus checking path
# only for existence of the path, not for it being a directory # only for existence of the path, not for it being a directory
@ -556,36 +560,29 @@ main_spec = (Spec(
watcher=Spec().type(unicode).oneof(set(('auto', 'inotify', 'stat'))).optional(), watcher=Spec().type(unicode).oneof(set(('auto', 'inotify', 'stat'))).optional(),
).context_message('Error while loading common configuration (key {key})'), ).context_message('Error while loading common configuration (key {key})'),
ext=Spec( ext=Spec(
vim=Spec( vim=ext_spec().update(
colorscheme=colorscheme_spec(), local_themes=Spec(
theme=theme_spec(), __tabline__=ext_theme_spec(),
local_themes=Spec().unknown_spec( ).unknown_spec(
lambda *args: check_matcher_func('vim', *args), theme_spec() lambda *args: check_matcher_func('vim', *args), ext_theme_spec()
), ),
).optional(), ).optional(),
ipython=Spec( ipython=ext_spec().update(
colorscheme=colorscheme_spec(),
theme=theme_spec(),
local_themes=Spec( local_themes=Spec(
in2=theme_spec(), in2=ext_theme_spec(),
out=theme_spec(), out=ext_theme_spec(),
rewrite=theme_spec(), rewrite=ext_theme_spec(),
), ),
).optional(), ).optional(),
shell=Spec( shell=ext_spec().update(
colorscheme=colorscheme_spec(),
theme=theme_spec(),
local_themes=Spec( local_themes=Spec(
continuation=theme_spec(), continuation=ext_theme_spec(),
select=theme_spec(), select=ext_theme_spec(),
), ),
).optional(), ).optional(),
).unknown_spec( ).unknown_spec(
check_ext, check_ext,
Spec( ext_spec(),
colorscheme=colorscheme_spec(),
theme=theme_spec(),
)
).context_message('Error while loading extensions configuration (key {key})'), ).context_message('Error while loading extensions configuration (key {key})'),
).context_message('Error while loading main configuration')) ).context_message('Error while loading main configuration'))
@ -771,7 +768,7 @@ type_keys = {
} }
required_keys = { required_keys = {
'function': set(('name',)), 'function': set(('name',)),
'string': set(('contents',)), 'string': set(()),
'filler': set(), 'filler': set(),
'segment_list': set(('name', 'segments',)), 'segment_list': set(('name', 'segments',)),
} }
@ -845,11 +842,11 @@ def check_full_segment_data(segment, data, context, echoerr):
ext = data['ext'] ext = data['ext']
theme_segment_data = context[0][1].get('segment_data', {}) theme_segment_data = context[0][1].get('segment_data', {})
top_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None) main_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None)
if not top_theme_name or data['theme'] == top_theme_name: if not main_theme_name or data['theme'] == main_theme_name:
top_segment_data = {} top_segment_data = {}
else: else:
top_segment_data = data['ext_theme_configs'].get(top_theme_name, {}).get('segment_data', {}) top_segment_data = data['ext_theme_configs'].get(main_theme_name, {}).get('segment_data', {})
names = [segment['name']] names = [segment['name']]
if segment.get('type', 'function') == 'function': if segment.get('type', 'function') == 'function':
@ -977,12 +974,16 @@ def check_segment_name(name, data, context, echoerr):
return True, False, hadproblem return True, False, hadproblem
elif context[-2][1].get('type') != 'segment_list': elif context[-2][1].get('type') != 'segment_list':
if name not in context[0][1].get('segment_data', {}): if name not in context[0][1].get('segment_data', {}):
top_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None) main_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None)
if data['theme'] == top_theme_name: if data['theme'] == main_theme_name:
top_theme = {} main_theme = {}
else: else:
top_theme = data['ext_theme_configs'].get(top_theme_name, {}) main_theme = data['ext_theme_configs'].get(main_theme_name, {})
if name not in top_theme.get('segment_data', {}): if (
name not in main_theme.get('segment_data', {})
and name not in data['ext_theme_configs'].get('__main__', {}).get('segment_data', {})
and not any(((name in theme.get('segment_data', {})) for theme in data['top_themes'].values()))
):
echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)), echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)),
problem='found useless use of name key (such name is not present in theme/segment_data)', problem='found useless use of name key (such name is not present in theme/segment_data)',
problem_mark=name.mark) problem_mark=name.mark)
@ -1070,34 +1071,49 @@ def check_highlight_groups(hl_groups, data, context, echoerr):
return True, False, False return True, False, False
def check_segment_data_key(key, data, context, echoerr): def list_themes(data, context):
theme_type = data['theme_type']
ext = data['ext'] ext = data['ext']
top_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None) main_theme_name = data['main_config'].get('ext', {}).get(ext, {}).get('theme', None)
is_top_theme = (data['theme'] == top_theme_name) is_main_theme = (data['theme'] == main_theme_name)
if is_top_theme: if theme_type == 'top':
themes = data['ext_theme_configs'].values() return list(itertools.chain(*[
[(ext, theme) for theme in theme_configs.values()]
for ext, theme_configs in data['theme_configs'].items()
]))
elif theme_type == 'main' or is_main_theme:
return [(ext, theme) for theme in data['ext_theme_configs'].values()]
else: else:
themes = [context[0][1]] return [(ext, context[0][1])]
for theme in themes:
def check_segment_data_key(key, data, context, echoerr):
has_module_name = '.' in key
found = False
for ext, theme in list_themes(data, context):
for segments in theme.get('segments', {}).values(): for segments in theme.get('segments', {}).values():
found = False
for segment in segments: for segment in segments:
if 'name' in segment: if 'name' in segment:
if key == segment['name']: if has_module_name:
found = True module = segment.get('module', theme.get('default_module', 'powerline.segments.' + ext))
module = segment.get('module', theme.get('default_module', 'powerline.segments.' + ext)) full_name = unicode(module) + '.' + unicode(segment['name'])
if key == unicode(module) + '.' + unicode(segment['name']): if key == full_name:
found = True found = True
break
else:
if key == segment['name']:
found = True
break
if found: if found:
break break
if found: if found:
break break
else: else:
echoerr(context='Error while checking segment data', if data['theme_type'] != 'top':
problem='found key {0} that cannot be associated with any segment'.format(key), echoerr(context='Error while checking segment data',
problem_mark=key.mark) problem='found key {0} that cannot be associated with any segment'.format(key),
return True, False, True problem_mark=key.mark)
return True, False, True
return True, False, False return True, False, False
@ -1109,8 +1125,8 @@ threaded_args_specs = {
} }
def check_args_variant(segment, args, data, context, echoerr): def check_args_variant(func, args, data, context, echoerr):
argspec = getconfigargspec(segment) argspec = getconfigargspec(func)
present_args = set(args) present_args = set(args)
all_args = set(argspec.args) all_args = set(argspec.args)
required_args = set(argspec.args[:-len(argspec.defaults)]) required_args = set(argspec.args[:-len(argspec.defaults)])
@ -1132,7 +1148,7 @@ def check_args_variant(segment, args, data, context, echoerr):
problem_mark=next(iter(present_args - all_args)).mark) problem_mark=next(iter(present_args - all_args)).mark)
hadproblem = True hadproblem = True
if isinstance(segment, ThreadedSegment): if isinstance(func, ThreadedSegment):
for key in set(threaded_args_specs) & present_args: for key in set(threaded_args_specs) & present_args:
proceed, khadproblem = threaded_args_specs[key].match( proceed, khadproblem = threaded_args_specs[key].match(
args[key], args[key],
@ -1149,13 +1165,13 @@ def check_args_variant(segment, args, data, context, echoerr):
return hadproblem return hadproblem
def check_args(get_segment_variants, args, data, context, echoerr): def check_args(get_functions, args, data, context, echoerr):
new_echoerr = DelayedEchoErr(echoerr) new_echoerr = DelayedEchoErr(echoerr)
count = 0 count = 0
hadproblem = False hadproblem = False
for segment in get_segment_variants(data, context, new_echoerr): for func in get_functions(data, context, new_echoerr):
count += 1 count += 1
shadproblem = check_args_variant(segment, args, data, context, echoerr) shadproblem = check_args_variant(func, args, data, context, echoerr)
if shadproblem: if shadproblem:
hadproblem = True hadproblem = True
@ -1171,7 +1187,7 @@ def check_args(get_segment_variants, args, data, context, echoerr):
return True, False, hadproblem return True, False, hadproblem
def get_one_segment_variant(data, context, echoerr): def get_one_segment_function(data, context, echoerr):
name = context[-2][1].get('name') name = context[-2][1].get('name')
if name: if name:
func = import_segment(name, data, context, echoerr) func = import_segment(name, data, context, echoerr)
@ -1179,7 +1195,7 @@ def get_one_segment_variant(data, context, echoerr):
yield func yield func
def get_all_possible_segments(data, context, echoerr): def get_all_possible_functions(data, context, echoerr):
name = context[-2][0] name = context[-2][0]
module, name = name.rpartition('.')[::2] module, name = name.rpartition('.')[::2]
if module: if module:
@ -1187,13 +1203,13 @@ def get_all_possible_segments(data, context, echoerr):
if func: if func:
yield func yield func
else: else:
for theme_config in data['ext_theme_configs'].values(): for ext, theme_config in list_themes(data, context):
for segments in theme_config.get('segments', {}).values(): for segments in theme_config.get('segments', {}).values():
for segment in segments: for segment in segments:
if segment.get('type', 'function') == 'function': if segment.get('type', 'function') == 'function':
module = segment.get( module = segment.get(
'module', 'module',
context[0][1].get('default_module', 'powerline.segments.' + data['ext']) theme_config.get('default_module', 'powerline.segments.' + data['ext'])
) )
func = import_segment(name, data, context, echoerr, module=module) func = import_segment(name, data, context, echoerr, module=module)
if func: if func:
@ -1222,7 +1238,7 @@ segment_spec = Spec(
before=Spec().type(unicode).optional(), before=Spec().type(unicode).optional(),
width=Spec().either(Spec().unsigned(), Spec().cmp('eq', 'auto')).optional(), width=Spec().either(Spec().unsigned(), Spec().cmp('eq', 'auto')).optional(),
align=Spec().oneof(set('lr')).optional(), align=Spec().oneof(set('lr')).optional(),
args=args_spec().func(lambda *args, **kwargs: check_args(get_one_segment_variant, *args, **kwargs)), args=args_spec().func(lambda *args, **kwargs: check_args(get_one_segment_function, *args, **kwargs)),
contents=Spec().type(unicode).optional(), contents=Spec().type(unicode).optional(),
highlight_group=Spec().list( highlight_group=Spec().list(
highlight_group_spec().re( highlight_group_spec().re(
@ -1245,20 +1261,52 @@ segdict_spec=Spec(
(lambda value, *args: (True, True, not (('left' in value) or ('right' in value)))), (lambda value, *args: (True, True, not (('left' in value) or ('right' in value)))),
(lambda value: 'segments dictionary must contain either left, right or both keys') (lambda value: 'segments dictionary must contain either left, right or both keys')
).context_message('Error while loading segments (key {key})').copy ).context_message('Error while loading segments (key {key})').copy
theme_spec = (Spec( divside_spec = Spec(
default_module=segment_module_spec(), hard=divider_spec(),
soft=divider_spec(),
).copy
segment_data_value_spec = Spec(
after=Spec().type(unicode).optional(),
before=Spec().type(unicode).optional(),
display=Spec().type(bool).optional(),
args=args_spec().func(lambda *args, **kwargs: check_args(get_all_possible_functions, *args, **kwargs)),
contents=Spec().type(unicode).optional(),
).copy
dividers_spec = Spec(
left=divside_spec(),
right=divside_spec(),
).copy
spaces_spec = Spec().unsigned().cmp(
'le', 2, (lambda value: 'Are you sure you need such a big ({0}) number of spaces?'.format(value))
).copy
common_theme_spec = Spec(
default_module=segment_module_spec().optional(),
).context_message('Error while loading theme').copy
top_theme_spec = common_theme_spec().update(
dividers=dividers_spec(),
spaces=spaces_spec(),
segment_data=Spec().unknown_spec( segment_data=Spec().unknown_spec(
Spec().func(check_segment_data_key), Spec().func(check_segment_data_key),
Spec( segment_data_value_spec(),
after=Spec().type(unicode).optional(), ).optional().context_message('Error while loading segment data (key {key})'),
before=Spec().type(unicode).optional(), )
display=Spec().type(bool).optional(), main_theme_spec = common_theme_spec().update(
args=args_spec().func(lambda *args, **kwargs: check_args(get_all_possible_segments, *args, **kwargs)), dividers=dividers_spec().optional(),
contents=Spec().type(unicode).optional(), spaces=spaces_spec().optional(),
), segment_data=Spec().unknown_spec(
Spec().func(check_segment_data_key),
segment_data_value_spec(),
).optional().context_message('Error while loading segment data (key {key})'),
)
theme_spec = common_theme_spec().update(
dividers=dividers_spec().optional(),
spaces=spaces_spec().optional(),
segment_data=Spec().unknown_spec(
Spec().func(check_segment_data_key),
segment_data_value_spec(),
).optional().context_message('Error while loading segment data (key {key})'), ).optional().context_message('Error while loading segment data (key {key})'),
segments=segdict_spec().update(above=Spec().list(segdict_spec()).optional()), segments=segdict_spec().update(above=Spec().list(segdict_spec()).optional()),
).context_message('Error while loading theme')) )
def generate_json_config_loader(lhadproblem): def generate_json_config_loader(lhadproblem):
@ -1315,6 +1363,8 @@ def check(paths=None, debug=False):
hadproblem = True hadproblem = True
sys.stderr.write('Path {0} is supposed to be a directory, but it is not\n'.format(d)) sys.stderr.write('Path {0} is supposed to be a directory, but it is not\n'.format(d))
hadproblem = False
configs = defaultdict(lambda: defaultdict(lambda: {})) configs = defaultdict(lambda: defaultdict(lambda: {}))
for typ in ('themes', 'colorschemes'): for typ in ('themes', 'colorschemes'):
for ext in paths[typ]: for ext in paths[typ]:
@ -1324,12 +1374,17 @@ def check(paths=None, debug=False):
name = subp[:-5] name = subp[:-5]
if name != '__main__': if name != '__main__':
lists[typ].add(name) lists[typ].add(name)
if name.startswith('__') or name.endswith('__'):
hadproblem = True
sys.stderr.write('File name is not supposed to start or end with “__”: {0}'.format(
os.path.join(d, subp)
))
configs[typ][ext][name] = os.path.join(d, subp) configs[typ][ext][name] = os.path.join(d, subp)
for path in paths['top_' + typ]: for path in paths['top_' + typ]:
name = os.path.basename(path)[:-5] name = os.path.basename(path)[:-5]
configs['top_' + typ][name] = path configs['top_' + typ][name] = path
diff = set(configs['themes']) ^ set(configs['colorschemes']) diff = set(configs['colorschemes']) - set(configs['themes'])
if diff: if diff:
hadproblem = True hadproblem = True
for ext in diff: for ext in diff:
@ -1341,7 +1396,6 @@ def check(paths=None, debug=False):
typ, typ,
)) ))
hadproblem = False
try: try:
main_config = load_config('config', find_config_files, config_loader) main_config = load_config('config', find_config_files, config_loader)
except IOError: except IOError:
@ -1469,17 +1523,53 @@ def check(paths=None, debug=False):
hadproblem = True hadproblem = True
theme_configs[ext][theme] = config theme_configs[ext][theme] = config
top_theme_configs = {}
for top_theme, top_theme_file in configs['top_themes'].items():
with open_file(top_theme_file) as config_file_fp:
try:
config, lhadproblem = load(config_file_fp)
except MarkedError as e:
sys.stderr.write(str(e) + '\n')
hadproblem = True
continue
if lhadproblem:
hadproblem = True
top_theme_configs[top_theme] = config
for ext, configs in theme_configs.items(): for ext, configs in theme_configs.items():
data = { data = {
'ext': ext, 'ext': ext,
'colorscheme_configs': colorscheme_configs, 'colorscheme_configs': colorscheme_configs,
'import_paths': import_paths, 'import_paths': import_paths,
'main_config': main_config, 'main_config': main_config,
'top_themes': top_theme_configs,
'ext_theme_configs': configs, 'ext_theme_configs': configs,
'colors_config': colors_config 'colors_config': colors_config
} }
for theme, config in configs.items(): for theme, config in configs.items():
data['theme'] = theme data['theme'] = theme
if theme_spec.match(config, context=(('', config),), data=data, echoerr=ee)[1]: if theme == '__main__':
data['theme_type'] = 'main'
spec = main_theme_spec
else:
data['theme_type'] = 'regular'
spec = theme_spec
if spec.match(config, context=(('', config),), data=data, echoerr=ee)[1]:
hadproblem = True hadproblem = True
for top_theme, config in top_theme_configs.items():
data = {
'ext': ext,
'colorscheme_configs': colorscheme_configs,
'import_paths': import_paths,
'main_config': main_config,
'theme_configs': theme_configs,
'ext_theme_configs': configs,
'colors_config': colors_config
}
data['theme_type'] = 'top'
data['theme'] = top_theme
if top_theme_spec.match(config, context=(('', config),), data=data, echoerr=ee)[1]:
hadproblem = True
return hadproblem return hadproblem

View File

@ -1,62 +1,63 @@
# vim:fileencoding=utf-8:noet # vim:fileencoding=utf-8:noet
from __future__ import absolute_import from __future__ import absolute_import
from inspect import ArgSpec, getargspec from inspect import ArgSpec, getargspec
from powerline.lib.threaded import ThreadedSegment, KwThreadedSegment
from itertools import count from itertools import count
from powerline.lib.threaded import ThreadedSegment, KwThreadedSegment
from powerline.segments import Segment
def getconfigargspec(obj): def getconfigargspec(obj):
if isinstance(obj, ThreadedSegment): if hasattr(obj, 'powerline_origin'):
args = ['interval'] obj = obj.powerline_origin
defaults = [getattr(obj, 'interval', 1)]
if obj.update_first:
args.append('update_first')
defaults.append(True)
methods = ['render', 'set_state']
if isinstance(obj, KwThreadedSegment):
methods += ['key', 'render_one']
for method in methods:
if hasattr(obj, method):
# Note: on <python-2.6 it may return simple tuple, not
# ArgSpec instance.
argspec = getargspec(getattr(obj, method))
for i, arg in zip(count(1), reversed(argspec.args)):
if (arg == 'self' or
(arg == 'segment_info' and
getattr(obj, 'powerline_requires_segment_info', None)) or
(arg == 'create_watcher' and
getattr(obj, 'powerline_requires_filesystem_watcher', None)) or
(arg == 'pl') or
(method.startswith('render') and (1 if argspec.args[0] == 'self' else 0) + i == len(argspec.args)) or
arg in args):
continue
if argspec.defaults and len(argspec.defaults) >= i:
default = argspec.defaults[-i]
defaults.append(default)
args.append(arg)
else:
args.insert(0, arg)
argspec = ArgSpec(args=args, varargs=None, keywords=None, defaults=tuple(defaults))
else: else:
if hasattr(obj, 'powerline_origin'): obj = obj
obj = obj.powerline_origin
else:
obj = obj
argspec = getargspec(obj) args = []
args = [] defaults = []
defaults = []
for i, arg in zip(count(1), reversed(argspec.args)): if isinstance(obj, Segment):
if ((arg == 'segment_info' and getattr(obj, 'powerline_requires_segment_info', None)) or additional_args = obj.additional_args()
arg == 'pl'): argspecobjs = obj.argspecobjs()
get_omitted_args = obj.omitted_args
else:
additional_args = ()
argspecobjs = ((None, obj),)
get_omitted_args = lambda *args: ()
for arg in additional_args:
args.append(arg[0])
if len(arg) > 1:
defaults.append(arg[1])
requires_segment_info = getattr(obj, 'powerline_requires_segment_info', False)
requires_filesystem_watcher = getattr(obj, 'powerline_requires_filesystem_watcher', False)
for name, method in argspecobjs:
argspec = getargspec(method)
omitted_args = get_omitted_args(name, method)
largs = len(argspec.args)
for i, arg in enumerate(reversed(argspec.args)):
if (
largs - (i + 1) in omitted_args
or arg == 'pl'
or (arg == 'create_watcher' and requires_filesystem_watcher)
or (arg == 'segment_info' and requires_segment_info)
):
continue continue
if argspec.defaults and len(argspec.defaults) >= i: if argspec.defaults and len(argspec.defaults) > i:
default = argspec.defaults[-i] if arg in args:
idx = args.index(arg)
if len(args) - idx > len(defaults):
args.pop(idx)
else:
continue
default = argspec.defaults[-(i + 1)]
defaults.append(default) defaults.append(default)
args.append(arg) args.append(arg)
else: else:
args.insert(0, arg) if arg not in args:
argspec = ArgSpec(args=args, varargs=argspec.varargs, keywords=argspec.keywords, defaults=tuple(defaults)) args.insert(0, arg)
return argspec return ArgSpec(args=args, varargs=None, keywords=None, defaults=tuple(defaults))

View File

@ -1,25 +1,57 @@
# vim:fileencoding=utf-8:noet # vim:fileencoding=utf-8:noet
from __future__ import absolute_import, unicode_literals, division, print_function from __future__ import absolute_import, unicode_literals, division, print_function
from powerline.lib.file_watcher import create_file_watcher
import sys import sys
from powerline.lib.file_watcher import create_file_watcher
def get_segment_key(segment, theme_configs, key, module=None, default=None):
def list_segment_key_values(segment, theme_configs, key, module=None, default=None):
try: try:
return segment[key] yield segment[key]
except KeyError: except KeyError:
if 'name' in segment: pass
name = segment['name'] try:
for theme_config in theme_configs: name = segment['name']
if 'segment_data' in theme_config: except KeyError:
for segment_key in ((module + '.' + name, name) if module else (name,)): pass
try: else:
return theme_config['segment_data'][segment_key][key] found_module_key = False
except KeyError: for theme_config in theme_configs:
pass try:
return default segment_data = theme_config['segment_data']
except KeyError:
pass
else:
if module:
try:
yield segment_data[module + '.' + name][key]
found_module_key = True
except KeyError:
pass
if not found_module_key:
try:
yield segment_data[name][key]
except KeyError:
pass
yield default
def get_segment_key(merge, *args, **kwargs):
if merge:
ret = None
for value in list_segment_key_values(*args, **kwargs):
if ret is None:
ret = value
elif isinstance(ret, dict) and isinstance(value, dict):
old_ret = ret
ret = value.copy()
ret.update(old_ret)
else:
return ret
return ret
else:
return next(list_segment_key_values(*args, **kwargs))
def get_function(data, segment): def get_function(data, segment):
@ -33,7 +65,7 @@ def get_function(data, segment):
def get_string(data, segment): def get_string(data, segment):
return data['get_key'](segment, None, 'contents'), None, None return data['get_key'](False, segment, None, 'contents'), None, None
def get_filler(data, segment): def get_filler(data, segment):
@ -129,8 +161,8 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module=Non
'path': common_config['paths'], 'path': common_config['paths'],
} }
def get_key(segment, module, key, default=None): def get_key(merge, segment, module, key, default=None):
return get_segment_key(segment, theme_configs, key, module, default) return get_segment_key(merge, segment, theme_configs, key, module, default)
data['get_key'] = get_key data['get_key'] = get_key
def get(segment, side): def get(segment, side):
@ -146,7 +178,7 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module=Non
pl.exception('Failed to generate segment from {0!r}: {1}', segment, str(e), prefix='segment_generator') pl.exception('Failed to generate segment from {0!r}: {1}', segment, str(e), prefix='segment_generator')
return None return None
if not get_key(segment, module, 'display', True): if not get_key(False, segment, module, 'display', True):
return None return None
if segment_type == 'function': if segment_type == 'function':
@ -155,7 +187,7 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module=Non
highlight_group = segment.get('highlight_group') or segment.get('name') highlight_group = segment.get('highlight_group') or segment.get('name')
if segment_type in ('function', 'segment_list'): if segment_type in ('function', 'segment_list'):
args = dict(((str(k), v) for k, v in get_key(segment, module, 'args', {}).items())) args = dict(((str(k), v) for k, v in get_key(True, segment, module, 'args', {}).items()))
if segment_type == 'segment_list': if segment_type == 'segment_list':
# Handle startup and shutdown of _contents_func? # Handle startup and shutdown of _contents_func?
@ -219,8 +251,8 @@ def gen_segment_getter(pl, ext, common_config, theme_configs, default_module=Non
'type': segment_type, 'type': segment_type,
'highlight_group': highlight_group, 'highlight_group': highlight_group,
'divider_highlight_group': None, 'divider_highlight_group': None,
'before': get_key(segment, module, 'before', ''), 'before': get_key(False, segment, module, 'before', ''),
'after': get_key(segment, module, 'after', ''), 'after': get_key(False, segment, module, 'after', ''),
'contents_func': contents_func, 'contents_func': contents_func,
'contents': contents, 'contents': contents,
'priority': segment.get('priority', None), 'priority': segment.get('priority', None),

View File

@ -1,2 +1,51 @@
# vim:fileencoding=utf-8:noet
from __future__ import absolute_import
import sys
from pkgutil import extend_path from pkgutil import extend_path
from types import MethodType
__path__ = extend_path(__path__, __name__) __path__ = extend_path(__path__, __name__)
class Segment(object):
'''Base class for any segment that is not a function
Required for powerline.lint.inspect to work properly.
'''
if sys.version_info < (3, 4):
def argspecobjs(self):
yield '__call__', self.__call__
else:
def argspecobjs(self): # NOQA
yield '__call__', self
argspecobjs.__doc__ = (
'''Return a list of valid arguments for inspect.getargspec
Used to determine function arguments.
'''
)
def omitted_args(self, name, method):
'''List arguments which should be omitted
Returns a tuple with indexes of omitted arguments.
.. note::``segment_info``, ``create_watcher`` and ``pl`` will be omitted
regardless of the below return (for ``segment_info`` and
``create_watcher``: only if object was marked to require segment
info or filesystem watcher).
'''
if isinstance(self.__call__, MethodType):
return (0,)
else:
return ()
@staticmethod
def additional_args():
'''Returns a list of (additional argument name[, default value]) tuples.
'''
return ()

View File

@ -19,6 +19,7 @@ from powerline.lib.monotonic import monotonic
from powerline.lib.humanize_bytes import humanize_bytes from powerline.lib.humanize_bytes import humanize_bytes
from powerline.lib.unicode import u from powerline.lib.unicode import u
from powerline.theme import requires_segment_info, requires_filesystem_watcher from powerline.theme import requires_segment_info, requires_filesystem_watcher
from powerline.segments import Segment
from collections import namedtuple from collections import namedtuple
@ -896,29 +897,30 @@ Highlight groups used: ``email_alert_gradient`` (gradient), ``email_alert``.
''') ''')
class NowPlayingSegment(object): STATE_SYMBOLS = {
STATE_SYMBOLS = { 'fallback': '',
'fallback': '', 'play': '',
'play': '', 'pause': '▮▮',
'pause': '▮▮', 'stop': '',
'stop': '', }
}
def __call__(self, player='mpd', format='{state_symbol} {artist} - {title} ({total})', **kwargs):
class NowPlayingSegment(Segment):
def __call__(self, player='mpd', format='{state_symbol} {artist} - {title} ({total})', state_symbols=STATE_SYMBOLS, **kwargs):
player_func = getattr(self, 'player_{0}'.format(player)) player_func = getattr(self, 'player_{0}'.format(player))
stats = { stats = {
'state': None, 'state': 'fallback',
'state_symbol': self.STATE_SYMBOLS['fallback'],
'album': None, 'album': None,
'artist': None, 'artist': None,
'title': None, 'title': None,
'elapsed': None, 'elapsed': None,
'total': None, 'total': None,
} }
func_stats = player_func(**kwargs) func_stats = player_func(state_symbol=state_symbols, **kwargs)
if not func_stats: if not func_stats:
return None return None
stats.update(func_stats) stats.update(func_stats)
stats['state_symbol'] = state_symbols.get(stats['state'])
return format.format(**stats) return format.format(**stats)
@staticmethod @staticmethod
@ -965,7 +967,6 @@ class NowPlayingSegment(object):
state = self._convert_state(now_playing.get('status')) state = self._convert_state(now_playing.get('status'))
return { return {
'state': state, 'state': state,
'state_symbol': self.STATE_SYMBOLS.get(state),
'album': now_playing.get('album'), 'album': now_playing.get('album'),
'artist': now_playing.get('artist'), 'artist': now_playing.get('artist'),
'title': now_playing.get('title'), 'title': now_playing.get('title'),
@ -998,7 +999,6 @@ class NowPlayingSegment(object):
client.disconnect() client.disconnect()
return { return {
'state': status.get('state'), 'state': status.get('state'),
'state_symbol': self.STATE_SYMBOLS.get(status.get('state')),
'album': now_playing.get('album'), 'album': now_playing.get('album'),
'artist': now_playing.get('artist'), 'artist': now_playing.get('artist'),
'title': now_playing.get('title'), 'title': now_playing.get('title'),
@ -1027,7 +1027,6 @@ class NowPlayingSegment(object):
state = self._convert_state(status) state = self._convert_state(status)
return { return {
'state': state, 'state': state,
'state_symbol': self.STATE_SYMBOLS.get(state),
'album': info.get('xesam:album'), 'album': info.get('xesam:album'),
'artist': info.get('xesam:artist')[0], 'artist': info.get('xesam:artist')[0],
'title': info.get('xesam:title'), 'title': info.get('xesam:title'),
@ -1074,7 +1073,6 @@ class NowPlayingSegment(object):
return None return None
return { return {
'state': state, 'state': state,
'state_symbol': self.STATE_SYMBOLS.get(state),
'album': spotify_status[1], 'album': spotify_status[1],
'artist': spotify_status[2], 'artist': spotify_status[2],
'title': spotify_status[3], 'title': spotify_status[3],

View File

@ -31,13 +31,13 @@ class Theme(object):
top_theme_config=None, top_theme_config=None,
run_once=False, run_once=False,
shutdown_event=None): shutdown_event=None):
self.dividers = theme_config.get('dividers', common_config['dividers']) self.dividers = theme_config['dividers']
self.dividers = dict(( self.dividers = dict((
(key, dict((k, u(v)) (key, dict((k, u(v))
for k, v in val.items())) for k, v in val.items()))
for key, val in self.dividers.items() for key, val in self.dividers.items()
)) ))
self.spaces = theme_config.get('spaces', common_config['spaces']) self.spaces = theme_config['spaces']
self.segments = [] self.segments = []
self.EMPTY_SEGMENT = { self.EMPTY_SEGMENT = {
'contents': None, 'contents': None,

View File

@ -17,17 +17,6 @@ CONFIG_DIR = 'tests/config'
root_config = lambda: { root_config = lambda: {
'common': { 'common': {
'dividers': {
'left': {
'hard': '#>',
'soft': '|>',
},
'right': {
'hard': '<#',
'soft': '<|',
},
},
'spaces': 0,
'interval': None, 'interval': None,
'watcher': 'auto', 'watcher': 'auto',
}, },
@ -76,12 +65,41 @@ theme_config = lambda: {
} }
} }
top_theme_config = lambda: {
'dividers': {
'left': {
'hard': '#>',
'soft': '|>',
},
'right': {
'hard': '<#',
'soft': '<|',
},
},
'spaces': 0,
}
main_tree = lambda: { main_tree = lambda: {
'1/config': root_config(), '1/config': root_config(),
'1/colors': colors_config(), '1/colors': colors_config(),
'1/colorschemes/default': colorscheme_config(), '1/colorschemes/default': colorscheme_config(),
'1/themes/test/default': theme_config(), '1/themes/test/default': theme_config(),
'1/themes/powerline': top_theme_config(),
'1/themes/other1': mdc(top_theme_config(), {
'dividers': {
'left': {
'hard': '!>',
}
}
}),
'1/themes/other2': mdc(top_theme_config(), {
'dividers': {
'left': {
'hard': '>>',
}
}
}),
} }
@ -151,11 +169,7 @@ class TestMerging(TestCase):
with WithConfigTree(mdc(main_tree(), { with WithConfigTree(mdc(main_tree(), {
'2/config': { '2/config': {
'common': { 'common': {
'dividers': { 'default_top_theme': 'other1',
'left': {
'hard': '!>',
}
}
} }
}, },
})) as p: })) as p:
@ -163,36 +177,26 @@ class TestMerging(TestCase):
with WithConfigTree(mdc(main_tree(), { with WithConfigTree(mdc(main_tree(), {
'2/config': { '2/config': {
'common': { 'common': {
'dividers': { 'default_top_theme': 'other1',
'left': {
'hard': '!>',
}
}
} }
}, },
'3/config': { '3/config': {
'common': { 'common': {
'dividers': { 'default_top_theme': 'other2',
'left': {
'hard': '>>',
}
}
} }
}, },
})) as p: })) as p:
self.assertRenderEqual(p, '{12} bt{2-}>>{--}') self.assertRenderEqual(p, '{12} bt{2-}>>{--}')
def test_top_theme_merging(self):
with WithConfigTree(mdc(main_tree(), { with WithConfigTree(mdc(main_tree(), {
'2/config': { '2/themes/powerline': {
'common': { 'spaces': 1,
'spaces': 1,
}
}, },
'3/config': { '3/themes/powerline': {
'common': { 'dividers': {
'dividers': { 'left': {
'left': { 'hard': '>>',
'hard': '>>',
}
} }
} }
}, },

View File

@ -12,17 +12,6 @@ from tests.lib.config_mock import get_powerline, add_watcher_events
config = { config = {
'config': { 'config': {
'common': { 'common': {
'dividers': {
"left": {
"hard": ">>",
"soft": ">",
},
"right": {
"hard": "<<",
"soft": "<",
},
},
'spaces': 0,
'interval': 0, 'interval': 0,
'watcher': 'test', 'watcher': 'test',
}, },
@ -73,6 +62,32 @@ config = {
], ],
}, },
}, },
'themes/powerline': {
'dividers': {
"left": {
"hard": ">>",
"soft": ">",
},
"right": {
"hard": "<<",
"soft": "<",
},
},
'spaces': 0,
},
'themes/other': {
'dividers': {
"left": {
"hard": ">>",
"soft": ">",
},
"right": {
"hard": "<<",
"soft": "<",
},
},
'spaces': 1,
},
'themes/test/2': { 'themes/test/2': {
'segments': { 'segments': {
"left": [ "left": [
@ -116,7 +131,7 @@ class TestConfigReload(TestCase):
def test_noreload(self, config): def test_noreload(self, config):
with get_powerline(config, run_once=True) as p: with get_powerline(config, run_once=True) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default') self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['config']['common']['spaces'] = 1 config['config']['common']['spaces'] = 1
add_watcher_events(p, 'config', wait=False, interval=0.05) add_watcher_events(p, 'config', wait=False, interval=0.05)
# When running once thread should not start # When running once thread should not start
@ -128,25 +143,30 @@ class TestConfigReload(TestCase):
def test_reload_main(self, config): def test_reload_main(self, config):
with get_powerline(config, run_once=False) as p: with get_powerline(config, run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default') self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['config']['common']['spaces'] = 1 config['config']['common']['default_top_theme'] = 'other'
add_watcher_events(p, 'config') add_watcher_events(p, 'config')
p.render()
self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>><None None None>')
self.assertAccessEvents(p, 'config') self.assertAccessEvents(p, 'config', 'themes/other', 'check:themes/test/__main__', 'themes/test/default')
self.assertEqual(p.logger._pop_msgs(), []) self.assertEqual(p.logger._pop_msgs(), [])
config['config']['ext']['test']['theme'] = 'nonexistent' config['config']['ext']['test']['theme'] = 'nonexistent'
add_watcher_events(p, 'config') add_watcher_events(p, 'config')
self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'check:themes/test/nonexistent') self.assertAccessEvents(p, 'config', 'check:themes/test/nonexistent', 'themes/other', 'check:themes/test/__main__')
# It should normally handle file missing error # It should normally handle file missing error
self.assertEqual(p.logger._pop_msgs(), ['exception:test:powerline:Failed to create renderer: themes/test/nonexistent']) self.assertEqual(p.logger._pop_msgs(), [
'exception:test:powerline:Failed to load theme: themes/test/__main__',
'exception:test:powerline:Failed to load theme: themes/test/nonexistent',
'exception:test:powerline:Failed to create renderer: themes/test/nonexistent'
])
config['config']['ext']['test']['theme'] = 'default' config['config']['ext']['test']['theme'] = 'default'
add_watcher_events(p, 'config') add_watcher_events(p, 'config')
self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s <2 4 False>>><3 4 4>g <4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'themes/test/default') self.assertAccessEvents(p, 'config', 'themes/test/default', 'themes/other', 'check:themes/test/__main__')
self.assertEqual(p.logger._pop_msgs(), []) self.assertEqual(p.logger._pop_msgs(), [])
config['config']['ext']['test']['colorscheme'] = 'nonexistent' config['config']['ext']['test']['colorscheme'] = 'nonexistent'
@ -170,7 +190,7 @@ class TestConfigReload(TestCase):
config['config']['ext']['test']['theme'] = '2' config['config']['ext']['test']['theme'] = '2'
add_watcher_events(p, 'config') add_watcher_events(p, 'config')
self.assertEqual(p.render(), '<2 3 1> t <3 4 False>>><1 4 4>b <4 False False>>><None None None>') self.assertEqual(p.render(), '<2 3 1> t <3 4 False>>><1 4 4>b <4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'themes/test/2') self.assertAccessEvents(p, 'config', 'themes/test/2', 'themes/other', 'check:themes/test/__main__')
self.assertEqual(p.logger._pop_msgs(), []) self.assertEqual(p.logger._pop_msgs(), [])
self.assertEqual(p.renderer.local_themes, None) self.assertEqual(p.renderer.local_themes, None)
@ -185,7 +205,7 @@ class TestConfigReload(TestCase):
def test_reload_unexistent(self, config): def test_reload_unexistent(self, config):
with get_powerline(config, run_once=False) as p: with get_powerline(config, run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default') self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['config']['ext']['test']['colorscheme'] = 'nonexistentraise' config['config']['ext']['test']['colorscheme'] = 'nonexistentraise'
add_watcher_events(p, 'config') add_watcher_events(p, 'config')
@ -222,7 +242,7 @@ class TestConfigReload(TestCase):
def test_reload_colors(self, config): def test_reload_colors(self, config):
with get_powerline(config, run_once=False) as p: with get_powerline(config, run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default') self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['colors']['colors']['col1'] = 5 config['colors']['colors']['col1'] = 5
add_watcher_events(p, 'colors') add_watcher_events(p, 'colors')
@ -234,7 +254,7 @@ class TestConfigReload(TestCase):
def test_reload_colorscheme(self, config): def test_reload_colorscheme(self, config):
with get_powerline(config, run_once=False) as p: with get_powerline(config, run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default') self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['colorschemes/test/default']['groups']['str1']['bg'] = 'col3' config['colorschemes/test/default']['groups']['str1']['bg'] = 'col3'
add_watcher_events(p, 'colorschemes/test/default') add_watcher_events(p, 'colorschemes/test/default')
@ -246,12 +266,24 @@ class TestConfigReload(TestCase):
def test_reload_theme(self, config): def test_reload_theme(self, config):
with get_powerline(config, run_once=False) as p: with get_powerline(config, run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default') self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['themes/test/default']['segments']['left'][0]['contents'] = 'col3' config['themes/test/default']['segments']['left'][0]['contents'] = 'col3'
add_watcher_events(p, 'themes/test/default') add_watcher_events(p, 'themes/test/default')
self.assertEqual(p.render(), '<1 2 1> col3<2 4 False>>><3 4 4>g<4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> col3<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'themes/test/default') self.assertAccessEvents(p, 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
self.assertEqual(p.logger._pop_msgs(), [])
@with_new_config
def test_reload_top_theme(self, config):
with get_powerline(config, run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['themes/powerline']['dividers']['left']['hard'] = '|>'
add_watcher_events(p, 'themes/powerline')
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>|><3 4 4>g<4 False False>|><None None None>')
self.assertAccessEvents(p, 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
self.assertEqual(p.logger._pop_msgs(), []) self.assertEqual(p.logger._pop_msgs(), [])
@with_new_config @with_new_config
@ -259,12 +291,12 @@ class TestConfigReload(TestCase):
config['config']['common']['interval'] = None config['config']['common']['interval'] = None
with get_powerline(config, run_once=False) as p: with get_powerline(config, run_once=False) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default') self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['themes/test/default']['segments']['left'][0]['contents'] = 'col3' config['themes/test/default']['segments']['left'][0]['contents'] = 'col3'
add_watcher_events(p, 'themes/test/default', wait=False) add_watcher_events(p, 'themes/test/default', wait=False)
self.assertEqual(p.render(), '<1 2 1> col3<2 4 False>>><3 4 4>g<4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> col3<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'themes/test/default') self.assertAccessEvents(p, 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
self.assertEqual(p.logger._pop_msgs(), []) self.assertEqual(p.logger._pop_msgs(), [])
self.assertTrue(p._watcher._calls) self.assertTrue(p._watcher._calls)
@ -273,7 +305,7 @@ class TestConfigReload(TestCase):
config['config']['common']['interval'] = None config['config']['common']['interval'] = None
with get_powerline(config, run_once=True) as p: with get_powerline(config, run_once=True) as p:
self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>') self.assertEqual(p.render(), '<1 2 1> s<2 4 False>>><3 4 4>g<4 False False>>><None None None>')
self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default') self.assertAccessEvents(p, 'config', 'colors', 'check:colorschemes/default', 'check:colorschemes/test/__main__', 'colorschemes/test/default', 'themes/test/default', 'themes/powerline', 'check:themes/test/__main__')
config['themes/test/default']['segments']['left'][0]['contents'] = 'col3' config['themes/test/default']['segments']['left'][0]['contents'] = 'col3'
add_watcher_events(p, 'themes/test/default', wait=False) add_watcher_events(p, 'themes/test/default', wait=False)

View File

@ -22,17 +22,6 @@ def highlighted_string(s, group, **kwargs):
config = { config = {
'config': { 'config': {
'common': { 'common': {
'dividers': {
'left': {
'hard': '>>',
'soft': '>',
},
'right': {
'hard': '<<',
'soft': '<',
},
},
'spaces': 0,
'interval': 0, 'interval': 0,
'watcher': 'test', 'watcher': 'test',
}, },
@ -104,6 +93,26 @@ config = {
], ],
}, },
}, },
'themes/powerline': {
'dividers': {
'left': {
'hard': '>>',
'soft': '>',
},
'right': {
'hard': '<<',
'soft': '<',
},
},
'spaces': 0,
},
'themes/test/__main__': {
'dividers': {
'right': {
'soft': '|',
},
},
},
'themes/vim/default': { 'themes/vim/default': {
'default_module': 'powerline.segments.common', 'default_module': 'powerline.segments.common',
'segments': { 'segments': {
@ -147,9 +156,9 @@ class TestRender(TestCase):
class TestLines(TestRender): class TestLines(TestRender):
@add_args @add_args
def test_without_above(self, p, config): def test_without_above(self, p, config):
self.assertRenderEqual(p, '{121} s{24}>>{344}g{34}>{34}<{344}f {--}') self.assertRenderEqual(p, '{121} s{24}>>{344}g{34}>{34}|{344}f {--}')
self.assertRenderEqual(p, '{121} s {24}>>{344}g{34}>{34}<{344}f {--}', width=10) self.assertRenderEqual(p, '{121} s {24}>>{344}g{34}>{34}|{344}f {--}', width=10)
# self.assertRenderEqual(p, '{121} s {24}>>{344}g{34}>{34}<{344} f {--}', width=11) # self.assertRenderEqual(p, '{121} s {24}>>{344}g{34}>{34}|{344} f {--}', width=11)
self.assertEqual(list(p.render_above_lines()), []) self.assertEqual(list(p.render_above_lines()), [])
@with_new_config @with_new_config
@ -158,21 +167,21 @@ class TestLines(TestRender):
config['themes/test/default']['segments']['above'] = [old_segments] config['themes/test/default']['segments']['above'] = [old_segments]
with get_powerline(config, run_once=True, simpler_renderer=True) as p: with get_powerline(config, run_once=True, simpler_renderer=True) as p:
self.assertRenderLinesEqual(p, [ self.assertRenderLinesEqual(p, [
'{121} s{24}>>{344}g{34}>{34}<{344}f {--}', '{121} s{24}>>{344}g{34}>{34}|{344}f {--}',
]) ])
self.assertRenderLinesEqual(p, [ self.assertRenderLinesEqual(p, [
'{121} s {24}>>{344}g{34}>{34}<{344}f {--}', '{121} s {24}>>{344}g{34}>{34}|{344}f {--}',
], width=10) ], width=10)
config['themes/test/default']['segments']['above'] = [old_segments] * 2 config['themes/test/default']['segments']['above'] = [old_segments] * 2
with get_powerline(config, run_once=True, simpler_renderer=True) as p: with get_powerline(config, run_once=True, simpler_renderer=True) as p:
self.assertRenderLinesEqual(p, [ self.assertRenderLinesEqual(p, [
'{121} s{24}>>{344}g{34}>{34}<{344}f {--}', '{121} s{24}>>{344}g{34}>{34}|{344}f {--}',
'{121} s{24}>>{344}g{34}>{34}<{344}f {--}', '{121} s{24}>>{344}g{34}>{34}|{344}f {--}',
]) ])
self.assertRenderLinesEqual(p, [ self.assertRenderLinesEqual(p, [
'{121} s {24}>>{344}g{34}>{34}<{344}f {--}', '{121} s {24}>>{344}g{34}>{34}|{344}f {--}',
'{121} s {24}>>{344}g{34}>{34}<{344}f {--}', '{121} s {24}>>{344}g{34}>{34}|{344}f {--}',
], width=10) ], width=10)
@ -299,6 +308,82 @@ class TestColorschemesHierarchy(TestRender):
self.assertEqual(p.logger._pop_msgs(), []) self.assertEqual(p.logger._pop_msgs(), [])
class TestThemeHierarchy(TestRender):
@add_args
def test_hierarchy(self, p, config):
self.assertRenderEqual(p, '{121} s{24}>>{344}g{34}>{34}|{344}f {--}')
@add_args
def test_no_main(self, p, config):
del config['themes/test/__main__']
self.assertRenderEqual(p, '{121} s{24}>>{344}g{34}>{34}<{344}f {--}')
self.assertEqual(p.logger._pop_msgs(), [])
@add_args
def test_no_powerline(self, p, config):
config['themes/test/__main__']['dividers'] = config['themes/powerline']['dividers']
config['themes/test/__main__']['spaces'] = 1
del config['themes/powerline']
self.assertRenderEqual(p, '{121} s {24}>>{344}g {34}>{34}<{344} f {--}')
self.assertEqual(p.logger._pop_msgs(), [])
@add_args
def test_no_default(self, p, config):
del config['themes/test/default']
self.assertRenderEqual(p, 'themes/test/default')
self.assertEqual(p.logger._pop_msgs(), [
'exception:test:powerline:Failed to load theme: themes/test/default',
'exception:test:powerline:Failed to create renderer: themes/test/default',
'exception:test:powerline:Failed to render: themes/test/default',
])
@add_args
def test_only_default(self, p, config):
config['themes/test/default']['dividers'] = config['themes/powerline']['dividers']
config['themes/test/default']['spaces'] = 1
del config['themes/test/__main__']
del config['themes/powerline']
self.assertRenderEqual(p, '{121} s {24}>>{344}g {34}>{34}<{344} f {--}')
@add_args
def test_only_main(self, p, config):
del config['themes/test/default']
del config['themes/powerline']
self.assertRenderEqual(p, 'themes/test/default')
self.assertEqual(p.logger._pop_msgs(), [
'exception:test:powerline:Failed to load theme: themes/powerline',
'exception:test:powerline:Failed to load theme: themes/test/default',
'exception:test:powerline:Failed to create renderer: themes/test/default',
'exception:test:powerline:Failed to render: themes/test/default',
])
@add_args
def test_only_powerline(self, p, config):
del config['themes/test/default']
del config['themes/test/__main__']
self.assertRenderEqual(p, 'themes/test/default')
self.assertEqual(p.logger._pop_msgs(), [
'exception:test:powerline:Failed to load theme: themes/test/__main__',
'exception:test:powerline:Failed to load theme: themes/test/default',
'exception:test:powerline:Failed to create renderer: themes/test/default',
'exception:test:powerline:Failed to render: themes/test/default',
])
@add_args
def test_nothing(self, p, config):
del config['themes/test/default']
del config['themes/powerline']
del config['themes/test/__main__']
self.assertRenderEqual(p, 'themes/test/default')
self.assertEqual(p.logger._pop_msgs(), [
'exception:test:powerline:Failed to load theme: themes/powerline',
'exception:test:powerline:Failed to load theme: themes/test/__main__',
'exception:test:powerline:Failed to load theme: themes/test/default',
'exception:test:powerline:Failed to create renderer: themes/test/default',
'exception:test:powerline:Failed to render: themes/test/default',
])
class TestVim(TestCase): class TestVim(TestCase):
def test_environ_update(self): def test_environ_update(self):
# Regression test: test that segment obtains environment from vim, not # Regression test: test that segment obtains environment from vim, not

View File

@ -1,6 +1,6 @@
#!/usr/bin/vim -S #!/usr/bin/vim -S
let g:powerline_config_path = expand('<sfile>:p:h:h') . '/powerline/config_files' let g:powerline_config_path = expand('<sfile>:p:h:h') . '/powerline/config_files'
let g:powerline_config_overrides = {'common': {'dividers': {'left': {'hard': ' ', 'soft': ' > '}, 'right': {'hard': ' ', 'soft': ' < '}}}} let g:powerline_config_overrides = {'common': {'default_top_theme': 'ascii'}}
let g:powerline_theme_overrides__default = {'segment_data': {'line_current_symbol': {'contents': 'LN '}, 'branch': {'before': 'B '}}} let g:powerline_theme_overrides__default = {'segment_data': {'line_current_symbol': {'contents': 'LN '}, 'branch': {'before': 'B '}}}
try try
python import powerline.vim python import powerline.vim

View File

@ -13,7 +13,7 @@ catch
endtry endtry
if result isnot# '%#Pl_240_5789784_235_2500134_NONE# 1 %#Pl_240_5789784_235_2500134_NONE#./%#Pl_244_8421504_235_2500134_bold#abc %#Pl_244_8421504_235_2500134_NONE# %#Pl_240_5789784_235_2500134_NONE#2 %#Pl_240_5789784_235_2500134_NONE#./%#Pl_244_8421504_235_2500134_bold#def %#Pl_235_2500134_240_5789784_NONE# %#Pl_250_12369084_240_5789784_NONE#./%#Pl_231_16777215_240_5789784_bold#ghi %#Pl_240_5789784_236_3158064_NONE# %#Pl_231_16777215_236_3158064_NONE#                                           %#Pl_252_13684944_236_3158064_NONE# %#Pl_235_2500134_252_13684944_bold# Tabs ' if result isnot# '%#Pl_240_5789784_235_2500134_NONE# 1 %#Pl_240_5789784_235_2500134_NONE#./%#Pl_244_8421504_235_2500134_bold#abc %#Pl_244_8421504_235_2500134_NONE# %#Pl_240_5789784_235_2500134_NONE#2 %#Pl_240_5789784_235_2500134_NONE#./%#Pl_244_8421504_235_2500134_bold#def %#Pl_235_2500134_240_5789784_NONE# %#Pl_250_12369084_240_5789784_NONE#./%#Pl_231_16777215_240_5789784_bold#ghi %#Pl_240_5789784_236_3158064_NONE# %#Pl_231_16777215_236_3158064_NONE#                                           %#Pl_252_13684944_236_3158064_NONE# %#Pl_235_2500134_252_13684944_bold# Tabs '
call writefile(['Unexpected result', result], 'message.fail') call writefile(['Unexpected tabline', result], 'message.fail')
cquit cquit
endif endif
@ -27,7 +27,7 @@ catch
endtry endtry
if result isnot# '%#Pl_240_5789784_235_2500134_NONE# ./%#Pl_244_8421504_235_2500134_bold#abc %#Pl_244_8421504_235_2500134_NONE# %#Pl_240_5789784_235_2500134_NONE#./%#Pl_244_8421504_235_2500134_bold#def %#Pl_235_2500134_240_5789784_NONE# %#Pl_250_12369084_240_5789784_NONE#./%#Pl_231_16777215_240_5789784_bold#ghi %#Pl_240_5789784_236_3158064_NONE# %#Pl_231_16777215_236_3158064_NONE#                                               %#Pl_252_13684944_236_3158064_NONE# %#Pl_235_2500134_252_13684944_bold# Bufs ' if result isnot# '%#Pl_240_5789784_235_2500134_NONE# ./%#Pl_244_8421504_235_2500134_bold#abc %#Pl_244_8421504_235_2500134_NONE# %#Pl_240_5789784_235_2500134_NONE#./%#Pl_244_8421504_235_2500134_bold#def %#Pl_235_2500134_240_5789784_NONE# %#Pl_250_12369084_240_5789784_NONE#./%#Pl_231_16777215_240_5789784_bold#ghi %#Pl_240_5789784_236_3158064_NONE# %#Pl_231_16777215_236_3158064_NONE#                                               %#Pl_252_13684944_236_3158064_NONE# %#Pl_235_2500134_252_13684944_bold# Bufs '
call writefile(['Unexpected result (2)', result], 'message.fail') call writefile(['Unexpected tabline (2)', result], 'message.fail')
cquit cquit
endif endif