diff --git a/.gitignore b/.gitignore
index 9839280..c2a9c3f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,162 @@
-*.pyc
+## Python gitignore
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
build/
+develop-eggs/
dist/
-colout.egg-info/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+
+## Jetbrains
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# SonarLint
+.idea/sonarlint
+
+# CMake
+cmake-build-*/
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..b7997b5
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,12 @@
+language: python
+python:
+ - "3.5"
+ - "3.6"
+ - "3.7"
+ - "3.8"
+ - "pypy3"
+install:
+ - pip install .
+script:
+ - colout --help
+ - echo heyoo | colout hey yellow
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..1aba38f
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include LICENSE
diff --git a/README.md b/README.md
index 779777b..10bde37 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,24 @@
-colout(1) -- Color Up Arbitrary Command Output
-==============================================
+colout — Color Up Arbitrary Command Output
+==========================================
-## SYNOPSIS
+
+
+
-`colout` [-h] [-r]
+## Synopsis
-`colout` [-g] [-c] [-l] [-a] [-t] [-T] [-P] [-s] PATTERN [COLOR(S) [STYLE(S)]]
+`colout [-h] [-r [RESOURCE]]`
-## DESCRIPTION
+`colout [-g] [-c] [-l min,max] [-a] [-t] [-T DIR] [-P DIR] [-d COLORMAP] [-s] [-e CHAR] [-E CHAR] [--debug] PATTERN [COLOR(S) [STYLE(S)]]`
+
+## Description
`colout` read lines of text stream on the standard input and output characters
-matching a given regular expression *PATTERN* in given and *STYLE*.
+matching a given regular expression *PATTERN* in given *COLOR* and *STYLE*.
If groups are specified in the regular expression pattern, only them are taken
into account, else the whole matching pattern is colored.
@@ -21,26 +29,43 @@ If you ask for fewer colors, the last one will be duplicated across remaining
groups.
Available colors are: blue, black, yellow, cyan, green, magenta, white, red,
-rainbow, random, Random, scale, none, an RGB hexadecimal triplet or any number
-between 0 and 255.
+rainbow, random, Random, Spectrum, spectrum, scale, Scale, hash, Hash, none, an
+RGB hexadecimal triplet (`#11aaff`, for example) or any number between 0 and 255.
Available styles are: normal, bold, faint, italic, underline, blink,
rapid_blink, reverse, conceal or random (some styles may have no effect, depending
on your terminal).
-`rainbow` will cycle over a 8 colors rainbow at each matching pattern.
-`Rainbow` will do the same over 24 colors (this requires a terminal that supports
-the 256 color escape sequences).
+In some case, you can indicate a foreground and a background color, by indicating both colors
+separated by a period (for example: `red.blue`). You can also use this system to combine two styles
+(for example, for a bold style that also blinks: `bold.blink`).
-`Random` will color each matching pattern with a random color among the 255
-available in the ANSI table. `random` will do the same in 8 colors mode.
+`rainbow` will cycle over a the default colormap at each matching pattern.
+`Rainbow` will do the same over the default colormap for the 256-colors mode
+(this requires a terminal that supports the 256 color escape sequences).
-`scale` (8 colors) and `Scale` (36 colors) will parse the matching text as
-a decimal number and apply the rainbow colormap according to its position
-on the scale defined by the `-l` option (see below, "0,100" by default).
+`Random` will color each matching pattern with a random color among the default colormap
+(the 255 available in the ANSI table, by default).
+`random` will do the same in 8 colors mode.
-If the python-pygments library is installed, you can use the name of a
-syntax-coloring "lexer" as a color (for example: "Cpp", "ruby", "xml+django", etc.).
+`spectrum` and `Spectrum` are like rainbows, but with more colors (8 and 36
+colors).
+
+`scale` (8 colors) and `Scale` (256 colors) will parse the numbers characters in
+the matching text as a decimal number and apply the default colormap according
+to its position on the scale defined by the `-l` option (see below, "0,100" by
+default).
+
+`hash` (8 colors) and `Hash` (256 colors) will take a fingerprint of the matching
+text and apply the default colormap according to it. This ensure that matching
+texts appearing several times will always get the same color.
+
+Before interpreting the matched string as a number, colout will remove any
+character not supposed to be used to write down numbers. This permits to apply
+this special color on a large group, while interpreting only its numerical part.
+
+You can use the name of a syntax-coloring ["lexer"](http://pygments.org/docs/lexers/)
+as a color (for example: "Cpp", "ruby", "xml+django", etc.).
If GIMP palettes files (*.gpl) are available, you can also use their names as a
colormap (see the `-P` switch below).
@@ -53,8 +78,7 @@ When not specified, a *COLOR* defaults to _red_ and a *STYLE* defaults to _bold_
`colout` comes with some predefined themes to rapidly color well-known outputs
(see the `-t` switch below).
-If the python-pygments library is available, `colout` can be used as an interface
-to it (see also the `-s` switch below).
+`colout` can be used as an interface to pygments (see also the `--source` switch below).
To have a list of all colors, styles, special colormaps, themes, palettes and lexers,
use the `-r` switch (see below).
@@ -62,37 +86,29 @@ use the `-r` switch (see below).
`colout` is released under the GNU Public License v3.
-## INSTALLATION
+## Installation
- sudo python3 setup.py install
+The recomended method is using pip to install the package for the local user:
-and then soft link `/usr/local/bin/colout` to your colout.py under your installation
-directory, which is usually something like
+```console
+$ pip install --user colout
+```
- /usr/local/lib/python3/dist-packages/colout-0.1-py3.egg/colout/colout.py
+Another method is using [pipsi](https://github.com/mitsuhiko/pipsi)
+(_pipsi is no longer maintained, _)
+```console
+$ pipsi install colout
+```
+There is also a PPA for Ubuntu 16.04 (Xenial)/18.04 (Bionic) (@`0.6.1-3~dist7`, not actively maintained)
-## OTHER INSTALLATION METHOD
+```console
+$ sudo add-apt-repository ppa:csaba-kertesz/random
+$ sudo apt-get update
+$ sudo apt-get/aptitude install colout
+```
-Pypi (the Python Package Index)
-
- sudo pip install colout
-
-or
-
- sudo easy_install colout
-
-Ubuntu 13.04's ppa
-
- sudo add-apt-repository ppa:ciici123/colout
- sudo apt-get update
- sudo apt-get/aptitude install colout
-
-Gentoo
-
- sudo emerge colout
-
-## OPTIONS
+## Options
* `-h`, `--help`:
Show a help message and exit
@@ -104,8 +120,9 @@ Gentoo
Use the given list of comma-separated colors as a colormap (cycle the colors at each match).
* `-l min,max`, `--scale min,max`:
- When using the 'scale' colormap, parse matches as decimal numbers (taking your locale into account)
- and apply the rainbow colormap linearly between the given min,max (0,100, by default).
+ When using the 'scale' colormap, parse matches as decimal numbers (taking your locale into
+ account) or as arithmetic expression (like "1+2/0.9*3") and apply the rainbow colormap linearly
+ between the given min,max (0,100, by default).
* `-a`, `--all`:
Color the whole input at once instead of line per line
@@ -120,21 +137,35 @@ Gentoo
* `-P DIR`, `--palettes-dir DIR`:
Search for additional palettes (*.gpl files) in this directory.
-* `-r`, `--resources`:
- Print the names of all available colors, styles, themes and palettes.
- A bug currently made it mandatory to use an additional dummy argument to this option
- to make it work correctly, use `-r x`.
+* `-d COLORMAP`, `--default COLORMAP`:
+ When using special colormaps (`random`, `scale` or `hash`), use this COLORMAP instead of the default one.
+ This can be either one of the available colormaps or a comma-separated list of colors.
+ WARNING: be sure to specify a default colormap that is compatible with the special colormap's mode,
+ or else the colors may not appear the same.
+ Also, external palettes are converted from RGB to 256-ANSI and will thus not work if you use
+ them as default colormaps for a 8-colors mode special color.
+
+* `-r [TYPE(S)]`, `--resources [TYPE(S)]`:
+ Print the names of available resources. Use a comma-separated list of resources names
+ (styles, colors, special, themes, palettes, colormaps or lexers),
+ use 'all' (or no argument) to print all resources.
* `-s`, `--source`:
Interpret PATTERN as source code readable by the Pygments library. If the first letter of PATTERN
is upper case, use the 256 color mode, if it is lower case, use the 8 colors mode.
In 256 color mode, interpret COLOR as a Pygments style (e.g. "default").
+* `-e CHAR`, `--sep-list CHAR`:
+ Use this character as a separator for list of colors/resources/numbers (instead of comma).
+
+* `-E CHAR`, `--sep-pair CHAR`:
+ Use this character as a separator for foreground/background pairs (instead of period).
+
* `--debug`:
Debug mode: print what's going on internally, if you want to check what features are available.
-## REGULAR EXPRESSIONS
+## Regular expressions
A regular expression (or _regex_) is a pattern that describes a set of strings
that matches it.
@@ -144,21 +175,25 @@ that matches it.
special characters that would be recognize by your shell.
-## DEPENDENCIES
+## Dependencies
-Recommended packages:
+Necessary Python modules:
-* `argparse` for a usable arguments parsing
* `pygments` for the source code syntax coloring
* `babel` for a locale-aware number parsing
-## LIMITATIONS
+## Limitations
-Don't use nested groups or colout will duplicate the corresponding input text with each matching colors.
+Don't use nested groups or colout will duplicate the corresponding input text
+with each matching colors.
+Using a default colormap that is incompatible with the special colormap's mode
+(i.e. number of colors) will end badly.
-## EXAMPLES
+Color pairs (`foreground.background`) work in 8-colors mode for simple coloring, but may fail with `--colormap`.
+
+## Examples
### Simple
@@ -168,7 +203,7 @@ Don't use nested groups or colout will duplicate the corresponding input text wi
* Color in bold violet home directories in _/etc/passwd_:
`colout '/home/[a-z]+' 135 < /etc/passwd`
-* Color in yellow user/groups id, in bold green name and in bold red home directories in _/etc/passwd_:
+* Color in yellow user/groups id, in bold green name and in bold red home directories in `/etc/passwd`:
`colout ':x:([0-9]+:[0-9]+):([^:]+).*(/home/[a-z]+)' yellow,green,red normal,bold < /etc/passwd`
* Color in yellow file permissions with read rights for everyone:
@@ -201,7 +236,7 @@ Don't use nested groups or colout will duplicate the corresponding input text wi
* Color a make output, line numbers in yellow, errors in bold red, warning in magenta, pragma in green and C++ file base names in cyan:
`make 2>&1 | colout ':([0-9]+):[0-9]*' yellow normal | colout error | colout warning magenta | colout pragma green normal | colout '/(\w+)*\.(h|cpp)' cyan normal`
Or using themes:
- `make 2>&³ | colout -t cmake | colout -t g++`
+ `make 2>&1 | colout -t cmake | colout -t g++`
* Color each word in the head of auth.log with a rainbow color map, starting a new colormap at each new line (the
beginning of the command is just bash magic to repeat the string "(\\w+)\\W+":
@@ -219,18 +254,144 @@ Don't use nested groups or colout will duplicate the corresponding input text wi
* Color a source code substring:
`echo "There is an error in 'static void Functor::operator()( EOT& indiv ) { return indiv; }' you should fix it" | colout "'(.*)'" Cpp monokai`
+* Color the percent of progress part of a CMake's makefile output, with a color
+ related to the value of the progress (from 0%=blue to 100%=red):
+ `cmake .. && make | colout "^(\[\s*[0-9]+%\])" Scale`
+
+* Color hosts and users in `auth.log`, with consistent colors:
+ `cat /var/log/auth.log | colout "^(\S+\s+){3}(\S+)\s(\S+\s+){3}(\S+)\s+(\S+\s+){2}(\S+)\s*" none,hash,none,hash,none,hash`
+
### Bash alias
The following bash function color the output of any command with the
-cmake and g77 themes:
+cmake and g++ themes:
- function cm()
- {
- set -o pipefail
- $@ 2>&1 | colout -t cmake | colout -t g++
- }
+```bash
+function cm()
+{
+ set -o pipefail
+ $@ 2>&1 | colout -t cmake | colout -t g++
+}
+```
You then can use the `cm` alias as a prefix to your build command,
for example: `cm make test`
+
+### GDB integration
+
+You can use `colout` within the GNU debuger (`gbd`) to color its output.
+For example, the following script `.gdbinit` configuration will color
+the output of the backtrace command:
+
+```gdb
+set confirm off
+
+# Don't wrap line or the coloring regexp won't work.
+set width 0
+
+# Create a named pipe to get outputs from gdb
+shell test -e /tmp/coloutPipe && rm /tmp/coloutPipe
+shell mkfifo /tmp/coloutPipe
+
+define logging_on
+ # Instead of printing on stdout only, log everything...
+ set logging redirect on
+ # ... in our named pipe.
+ set logging on /tmp/coloutPipe
+end
+
+define logging_off
+ set logging off
+ set logging redirect off
+ # Because both gdb and our commands are writing on the same pipe at the same
+ # time, it is more than probable that gdb will end before our (higher level)
+ # commands. The gdb prompt will thus render before the result of the command,
+ # which is highly akward. To prevent this, we need to wait before displaying
+ # the prompt again. The more your commands are complex, the higher you will
+ # need to set this.
+ shell sleep 0.4s
+end
+
+define hook-backtrace
+ # Note: match path = [path]file[.ext] = (.*/)?(?:$|(.+?)(?:(\.[^.]*)|))
+ # This line color highlights:
+ # – lines that link to source code,
+ # – function call in green,
+ # – arguments names in yellow, values in magenta,
+ # — the parent directory in bold red (assuming that the debug session would be in a "project/build/" directory).
+ shell cat /tmp/coloutPipe | colout "^(#)([0-9]+)\s+(0x\S+ )*(in )*(.*) (\(.*\)) (at) (.*/)?(?:$|(.+?)(?:(\.[^.]*)|)):([0-9]+)" red,red,blue,red,green,magenta,red,none,white,white,yellow normal,bold,normal,normal,normal,normal,normal,bold,bold,bold | colout "([\w\s]*?)(=)([^,]*?)([,\)])" yellow,blue,magenta,blue normal | colout "/($(basename $(dirname $(pwd))))/" red bold &
+ logging_on
+end
+define hookpost-backtrace
+ logging_off
+end
+
+# Don't forget to clean the adhoc pipe.
+define hook-quit
+ set confirm off
+ shell rm -f /tmp/coloutPipe
+end
+```
+
+Take a look at the `example.gdbinit` file distributed with colout for more gdb commands.
+
+
+
+### Themes
+
+You can easily add your own theme to colout.
+A theme is basically a module with a function named `theme` that take the configuration context as
+an argument and return back the (modified) context and a list of triplets.
+Each triplet figures the same arguments than those of the command line interface.
+
+```python
+def theme(context):
+ return context,[ [regexp, colors, styles] ]
+```
+
+With the context dictionary at hand, you have access to the internal configuration of colout, you
+can thus change colormaps for special keywords, the scale, even the available colors, styles or
+themes.
+
+See the cmake them for how to modify an existing colormap if (and only if) the user didn't ask for an alternative one.
+See the ninja theme for how to extend an existing theme with more regexps and a different configuration.
+See the gcc theme for an example of how to use the localization of existing softwares to build translated regexp.
+
+
+### Buffering
+
+Note that when you use colout within real time streams (like `tail -f X | grep Y | colout Z`) of
+commands, you may observe that the lines are printed by large chunks and not one by one, in real
+time.
+This is not due to colout but to the buffering behavior of your shell.
+
+To fix that, use `stdbuf`, for example: `tail -f X | stdbuf -o0 grep Y | colout Z`.
+
+## Authors
+
+* nojhan : original idea, main developer, maintainer.
+* Adrian Sadłocha
+* Alex Burka
+* Brian Foley
+* Charles Lewis
+* DainDwarf
+* Dimitri Merejkowsky
+* Dong Wei Ming
+* Fabien MARTY
+* Jason Green
+* John Anderson
+* Jonathan Poelen
+* Louis-Kenzo Furuya Cahier
+* Mantas
+* Martin Ueding
+* Nicolas Pouillard
+* Nurono
+* Oliver Bristow
+* orzrd <61966225@qq.com>
+* Philippe Daouadi
+* Piotr Staroszczyk
+* Scott Lawrence
+* Xu Di
+* https://github.com/stdedos: maintainer.
diff --git a/bin/colout b/bin/colout
deleted file mode 100644
index 7702d17..0000000
--- a/bin/colout
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/bin/bash
-# Copyright (c) 2013 Martin Ueding
-
-# Small launcher script for the main module.
-
-# Licence: GPL 3
-
-set -e
-set -u
-
-python3 -m colout.colout "$@"
diff --git a/colout/colout.py b/colout/colout.py
index 70e4b16..2d51fac 100755
--- a/colout/colout.py
+++ b/colout/colout.py
@@ -1,29 +1,147 @@
#!/usr/bin/env python3
#encoding: utf-8
-# Color Up Arbitrary Command Ouput
+# Color Up Arbitrary Command Output
# Licensed under the GPL version 3
# 2012 (c) nojhan
-import sys
-import re
-import random
import os
+import re
+import sys
+import copy
import glob
import math
-import importlib
-import logging
+import pprint
+import random
import signal
+import string
+import hashlib
+import logging
+import argparse
+import importlib
+import functools
+import babel.numbers as bn
# set the SIGPIPE handler to kill the program instead of
# ending in a write error when a broken pipe occurs
signal.signal( signal.SIGPIPE, signal.SIG_DFL )
+###############################################################################
+# Global variable(s)
+###############################################################################
+
+context = {}
+debug = False
+
+# Available styles
+context["styles"] = {
+ "normal": 0, "bold": 1, "faint": 2, "italic": 3, "underline": 4,
+ "blink": 5, "rapid_blink": 6,
+ "reverse": 7, "conceal": 8
+}
+
+error_codes = {"UnknownColor": 1, "DuplicatedPalette": 2, "MixedModes": 3, "UnknownLexer": 4, "UnknownResource": 5}
+
+# Available color names in 8-colors mode.
+eight_colors = ["black","red","green","yellow","blue","magenta","cyan","white"]
+# Given in that order, the ASCII code is the index.
+eight_color_codes = {n:i for i,n in enumerate(eight_colors)}
+# One can add synonyms.
+eight_color_codes["orange"] = eight_color_codes["yellow"]
+eight_color_codes["purple"] = eight_color_codes["magenta"]
+
+# Foreground colors has a special "none" item.
+# Note: use copy to avoid having the same reference over fore/background.
+context["colors"] = copy.copy(eight_color_codes)
+context["colors"]["none"] = -1
+
+# Background has the same colors than foreground, but without the none code.
+context["backgrounds"] = copy.copy(eight_color_codes)
+
+context["themes"] = {}
+
+# pre-defined colormaps
+# 8-colors mode should start with a lower-case letter (and can contains either named or indexed colors)
+# 256-colors mode should start with an upper-case letter (and should contains indexed colors)
+context["colormaps"] = {
+ # Rainbows
+ "rainbow" : ["magenta", "blue", "cyan", "green", "yellow", "red"],
+ "Rainbow" : [92, 93, 57, 21, 27, 33, 39, 45, 51, 50, 49, 48, 47, 46, 82, 118, 154, 190, 226, 220, 214, 208, 202, 196],
+
+ # From magenta to red, with white in the middle
+ "spectrum" : ["magenta", "blue", "cyan", "white", "green", "yellow", "red"],
+ "Spectrum" : [91, 92, 56, 57, 21, 27, 26, 32, 31, 37, 36, 35, 41, 40, 41, 77, 83, 84, 120, 121, 157, 194, 231, 254, 255, 231, 230, 229, 228, 227, 226, 220, 214, 208, 202, 196],
+
+ # All the colors are available for the default `random` special
+ "random" : context["colors"],
+ "Random" : list(range(256))
+} # colormaps
+
+context["colormaps"]["scale"] = context["colormaps"]["spectrum"]
+context["colormaps"]["Scale"] = context["colormaps"]["Spectrum"]
+context["colormaps"]["hash"] = context["colormaps"]["rainbow"]
+context["colormaps"]["Hash"] = context["colormaps"]["Rainbow"]
+context["colormaps"]["default"] = context["colormaps"]["spectrum"]
+context["colormaps"]["Default"] = context["colormaps"]["Spectrum"]
+
+context["user_defined_colormaps"] = False
+
+context["colormap_idx"] = 0
+
+context["scale"] = (0,100)
+
+context["lexers"] = []
+
+# Character use as a delimiter
+# between foreground and background.
+context["sep_pair"]="."
+context["sep_list"]=","
+
+class UnknownColor(Exception):
+ pass
+
+class DuplicatedPalette(Exception):
+ pass
+
+class DuplicatedTheme(Exception):
+ pass
+
+class MixedModes(Exception):
+ pass
+
+
###############################################################################
# Ressource parsing helpers
###############################################################################
+def make_colormap( colors, sep_list = context["sep_list"] ):
+ cmap = colors.split(sep_list)
+
+ # Check unicity of mode.
+ modes = [mode(c) for c in cmap]
+ if len(uniq(modes)) > 1:
+ # Format a list of color:mode, for error display.
+ raise MixedModes(", ".join(["%s:%s" % cm for cm in zip(cmap,modes)]))
+
+ return cmap
+
+
+def set_special_colormaps( cmap, sep_list = context["sep_list"] ):
+ """Change all the special colors to a single colormap (which must be a list of colors)."""
+ global context
+ context["colormaps"]["scale"] = cmap
+ context["colormaps"]["Scale"] = cmap
+ context["colormaps"]["hash"] = cmap
+ context["colormaps"]["Hash"] = cmap
+ context["colormaps"]["default"] = cmap
+ context["colormaps"]["Default"] = cmap
+ context["colormaps"]["random"] = cmap
+ context["colormaps"]["Random"] = cmap
+ context["user_defined_colormaps"] = True
+ logging.debug("user-defined special colormap: %s" % sep_list.join([str(i) for i in cmap]) )
+
+
def parse_gimp_palette( filename ):
"""
Parse the given filename as a GIMP palette (.gpl)
@@ -59,7 +177,7 @@ def parse_gimp_palette( filename ):
palette = []
for line in lines:
# skip lines with only a comment
- if re.match("^\s*#.*$", line ):
+ if re.match(r"^\s*#.*$", line ):
continue
# decode the columns-ths codes. Generally [R G B] followed by a comment
colors = [ int(c) for c in line.split()[:columns] ]
@@ -87,7 +205,13 @@ def uniq( lst ):
def rgb_to_ansi( r, g, b ):
"""Convert a RGB color to its closest 256-colors ANSI index"""
- # ansi_max is the higher possible RGB value for ANSI colors
+
+ # Range limits for the *colored* section of ANSI,
+ # this does not include the *gray* section.
+ ansi_min = 16
+ ansi_max = 234
+
+ # ansi_max is the higher possible RGB value for ANSI *colors*
# limit RGB values to ansi_max
red,green,blue = tuple([ansi_max if c>ansi_max else c for c in (r,g,b)])
@@ -118,131 +242,91 @@ def hex_to_rgb(h):
return tuple( int(h[i:i+lh//3], 16) for i in range(0, lh, lh//3) )
-###############################################################################
-# Global variables
-###############################################################################
-
-# Escaped end markers for given color modes
-endmarks = {8: ";", 256: ";38;5;"}
-
-ansi_min = 16
-ansi_max = 234
-
-# Available styles
-styles = {
- "normal": 0, "bold": 1, "faint": 2, "italic": 3, "underline": 4,
- "blink": 5, "rapid_blink": 6,
- "reverse": 7, "conceal": 8
-}
-
-# Available color names in 8-colors mode
-colors = {
- "black": 0, "red": 1, "green": 2, "yellow": 3, "blue": 4,
- "magenta": 5, "cyan": 6, "white": 7, "none": -1
-}
-
-themes = {}
-
-# pre-defined colormaps
-# 8-colors mode should start with a lower-case letter (and can contains either named or indexed colors)
-# 256-colors mode should start with an upper-case letter (and should contains indexed colors)
-colormaps = {
- # Rainbows
- "rainbow" : ["magenta", "blue", "cyan", "green", "yellow", "red"],
- "Rainbow" : [92, 93, 57, 21, 27, 33, 39, 45, 51, 50, 49, 48, 47, 46, 82, 118, 154, 190, 226, 220, 214, 208, 202, 196],
-
- # from magenta to red, with white in the middle
- "spectrum" : ["magenta", "blue", "cyan", "white", "green", "yellow", "red"],
- "Spectrum" : [91, 92, 56, 57, 21, 27, 26, 32, 31, 37, 36, 35, 41, 40, 41, 77, 83, 84, 120, 121, 157, 194, 231, 254, 255, 231, 230, 229, 228, 227, 226, 220, 214, 208, 202, 196]
-} # colormaps
-
-colormap = colormaps["rainbow"] # default colormap to rainbow
-colormap_idx = 0
-
-scale = (0,100)
-
-class UnknownColor(Exception):
- pass
-
-class DuplicatedPalette(Exception):
- pass
-
-class DuplicatedTheme(Exception):
- pass
-
-
###############################################################################
# Load available extern resources
###############################################################################
def load_themes( themes_dir):
- global themes
+ global context
logging.debug("search for themes in: %s" % themes_dir)
- os.chdir( themes_dir )
+ sys.path.append( themes_dir )
# load available themes
- for f in glob.iglob("colout_*.py"):
- module = ".".join(f.split(".")[:-1]) # remove extension
- name = "_".join(module.split("_")[1:]) # remove the prefix
- if name in themes:
+ for f in glob.iglob(os.path.join(themes_dir, "colout_*.py")):
+ basename = os.path.basename(f) # Remove path.
+ module = os.path.splitext(basename)[0] # Remove extension.
+ name = "_".join(module.split("_")[1:]) # Remove the 'colout_' prefix.
+ if name in context["themes"]:
raise DuplicatedTheme(name)
logging.debug("load theme %s" % name)
- themes[name] = importlib.import_module(module)
+ context["themes"][name] = importlib.import_module(module)
-def load_palettes( palettes_dir ):
- global colormaps
+def load_palettes( palettes_dir, ignore_duplicates = True ):
+ global context
logging.debug("search for palettes in: %s" % palettes_dir)
- os.chdir( palettes_dir )
# load available colormaps (GIMP palettes format)
- for p in glob.iglob("*.gpl"):
+ for p in glob.iglob(os.path.join(palettes_dir, "*.gpl")):
try:
name,palette = parse_gimp_palette(p)
except Exception as e:
logging.warning("error while parsing palette %s: %s" % ( p,e ) )
continue
- if name in colormaps:
- raise DuplicatedPalette(name)
+ if name in context["colormaps"]:
+ if ignore_duplicates:
+ logging.warning("ignore this duplicated palette name: %s" % name)
+ else:
+ raise DuplicatedPalette(name)
# Convert the palette to ANSI
ansi_palette = [ rgb_to_ansi(r,g,b) for r,g,b in palette ]
# Compress it so that there isn't two consecutive identical colors
compressed = uniq(ansi_palette)
logging.debug("load %i ANSI colors in palette %s: %s" % (len(compressed), name, compressed))
- colormaps[name] = compressed
+ context["colormaps"][name] = compressed
def load_lexers():
- global lexers
+ global context
# load available pygments lexers
lexers = []
+ global get_lexer_by_name
+ from pygments.lexers import get_lexer_by_name
+
+ global highlight
+ from pygments import highlight
+
+ global Terminal256Formatter
+ from pygments.formatters import Terminal256Formatter
+
+ global TerminalFormatter
+ from pygments.formatters import TerminalFormatter
+
+ from pygments.lexers import get_all_lexers
try:
- global get_lexer_by_name
- from pygments.lexers import get_lexer_by_name
-
- global highlight
- from pygments import highlight
-
- global Terminal256Formatter
- from pygments.formatters import Terminal256Formatter
-
- global TerminalFormatter
- from pygments.formatters import TerminalFormatter
-
- from pygments.lexers import get_all_lexers
- except ImportError:
- logging.warning("the pygments module has not been found, syntax coloring is not available")
- pass
- else:
for lexer in get_all_lexers():
- try:
- lexers.append(lexer[1][0])
- except IndexError:
- logging.warning("cannot load lexer: %s" % lexer[1][0])
- pass
+ l = None
+ # If the tuple has one-word aliases
+ # (which are usually a better option than the long names
+ # for a command line argument).
+ if lexer[1]:
+ l = lexer[1][0] # Take the first one.
else:
- logging.debug("loaded lexer %s" % lexer[1][0])
- lexers.sort()
+ assert(lexer[0])
+ l = lexer[0] # Take the long name, which should alway exists.
+ if not l:
+ logging.warning("cannot load lexer: %s" % lexer[1][0])
+ pass # Forget about this lexer.
+ else:
+ assert(" " not in l) # Should be very rare, but probably a source of bugs.
+ lexers.append(l)
+ except:
+ logging.warning("error while executing the pygment module, syntax coloring is not available")
+
+ lexers.sort()
+ logging.debug("loaded %i lexers: %s" % (len(lexers), ", ".join(lexers)))
+
+ context["lexers"] = lexers
def load_resources( themes_dir, palettes_dir ):
@@ -255,10 +339,184 @@ def load_resources( themes_dir, palettes_dir ):
# Library
###############################################################################
-def colorin(text, color="red", style="normal"):
+def mode( color ):
+ global context
+ if type(color) is int:
+ if 0 <= color and color <= 255 :
+ return 256
+ else:
+ raise UnknownColor(color)
+ elif color in context["colors"]:
+ return 8
+ elif color in context["colormaps"].keys():
+ if color[0].islower():
+ return 8
+ elif color[0].isupper():
+ return 256
+ elif color.lower() in ("scale","hash","random") or color.lower() in context["lexers"]:
+ if color[0].islower():
+ return 8
+ elif color[0].isupper():
+ return 256
+ elif color[0] == "#":
+ return 256
+ elif color.isdigit() and (0 <= int(color) and int(color) <= 255) :
+ return 256
+ else:
+ raise UnknownColor(color)
+
+
+def next_in_map( name ):
+ global context
+ # loop over indices in colormap
+ return (context["colormap_idx"]+1) % len(context["colormaps"][name])
+
+
+def color_random( color ):
+ global context
+ m = mode(color)
+ if m == 8:
+ color_name = random.choice(list(context["colormaps"]["random"]))
+ color_code = context["colors"][color_name]
+ color_code = str(30 + color_code)
+
+ elif m == 256:
+ color_nb = random.choice(context["colormaps"]["Random"])
+ color_code = str(color_nb)
+
+ return color_code
+
+
+def color_in_colormaps( color ):
+ global context
+ m = mode(color)
+ if m == 8:
+ c = context["colormaps"][color][context["colormap_idx"]]
+ if c.isdigit():
+ color_code = str(30 + c)
+ else:
+ color_code = str(30 + context["colors"][c])
+
+ else:
+ color_nb = context["colormaps"][color][context["colormap_idx"]]
+ color_code = str( color_nb )
+
+ context["colormap_idx"] = next_in_map(color)
+
+ return color_code
+
+
+def color_scale( name, text ):
+ # filter out everything that does not seem to be necessary to interpret the string as a number
+ # this permits to transform "[ 95%]" to "95" before number conversion,
+ # and thus allows to color a group larger than the matched number
+ chars_in_numbers = "-+.,e/*"
+ allowed = string.digits + chars_in_numbers
+ nb = "".join([i for i in filter(allowed.__contains__, text)])
+
+ # interpret as decimal
+ f = None
+ try:
+ f = float(bn.parse_decimal(nb))
+ except bn.NumberFormatError:
+ pass
+ if f is not None:
+ # normalize with scale if it's a number
+ f = (f - context["scale"][0]) / (context["scale"][1]-context["scale"][0])
+ else:
+ # interpret as float between 0 and 1 otherwise
+ f = eval(nb)
+
+ # if out of scale, do not color
+ if f < 0 or f > 1:
+ return None
+
+ # normalize and scale over the nb of colors in cmap
+ colormap = context["colormaps"][name]
+ i = int( math.ceil( f * (len(colormap)-1) ) )
+ color = colormap[i]
+
+ # infer mode from the color in the colormap
+ m = mode(color)
+
+ if m == 8:
+ color_code = str(30 + context["colors"][color])
+ else:
+ color_code = str(color)
+
+ return color_code
+
+
+def color_hash( name, text ):
+ hasher = hashlib.md5()
+ hasher.update(text.encode('utf-8'))
+ hash = hasher.hexdigest()
+
+ f = float(functools.reduce(lambda x, y: x+ord(y), hash, 0) % 101)
+
+ # normalize and scale over the nb of colors in cmap
+ colormap = context["colormaps"][name]
+ i = int( math.ceil( (f - context["scale"][0]) / (context["scale"][1]-context["scale"][0]) * (len(colormap)-1) ) )
+ color = colormap[i]
+
+ # infer mode from the color in the colormap
+ m = mode(color)
+
+ if m == 8:
+ color_code = str(30 + context["colors"][color])
+ else:
+ color_code = str(color)
+
+ return color_code
+
+
+def color_map(name):
+ global context
+ # current color
+ color = context["colormaps"][name][ context["colormap_idx"] ]
+
+ m = mode(color)
+ if m == 8:
+ color_code = str(30 + context["colors"][color])
+ else:
+ color_nb = int(color)
+ assert( 0 <= color_nb <= 255 )
+ color_code = str(color_nb)
+
+ context["colormap_idx"] = next_in_map(name)
+
+ return color,color_code
+
+
+def color_lexer( name, style, text ):
+ lexer = get_lexer_by_name(name.lower())
+ # Python => 256 colors, python => 8 colors
+ m = mode(name)
+ if m == 256:
+ try:
+ formatter = Terminal256Formatter(style=style)
+ except: # style not found
+ formatter = Terminal256Formatter()
+ else:
+ if style not in ("light","dark"):
+ style = "dark" # dark color scheme by default
+ formatter = TerminalFormatter(bg=style)
+ # We should return all but the last character,
+ # because Pygments adds a newline char.
+ if not debug:
+ return highlight(text, lexer, formatter)[:-1]
+ else:
+ return "<"+name+">"+ highlight(text, lexer, formatter)[:-1] + ""+name+">"
+
+
+def colorin(text, color="red", style="normal", sep_pair=context["sep_pair"]):
"""
Return the given text, surrounded by the given color ASCII markers.
+ The given color may be either a single name, encoding the foreground color,
+ or a pair of names, delimited by the given sep_pair,
+ encoding foreground and background, e.g. "red.blue".
+
If the given color is a name that exists in available colors,
a 8-colors mode is assumed, else, a 256-colors mode.
@@ -272,153 +530,115 @@ def colorin(text, color="red", style="normal"):
assert( type(color) is str )
- global colormap_idx
global debug
# Special characters.
start = "\033["
stop = "\033[0m"
+ # Escaped end markers for given color modes
+ endmarks = {8: ";", 256: ";38;5;"}
+
color_code = ""
style_code = ""
+ background_code = ""
+ style_codes = []
# Convert the style code
if style == "random" or style == "Random":
- style = random.choice(list(styles.keys()))
+ style = random.choice(list(context["styles"].keys()))
else:
- if style in styles:
- style_code = str(styles[style])
+ styles = style.split(sep_pair)
+ for astyle in styles:
+ if astyle in context["styles"]:
+ style_codes.append(str(context["styles"][astyle]))
+ style_code = ";".join(style_codes)
- if color == "none":
+ color_pair = color.strip().split(sep_pair)
+ color = color_pair[0]
+ background = color_pair[1] if len(color_pair) == 2 else "none"
+
+ if color == "none" and background == "none":
# if no color, style cannot be applied
if not debug:
return text
else:
return ""+text+""
- elif color == "random":
- mode = 8
- color_code = random.choice(list(colors.values()))
- color_code = str(30 + color_code)
-
- elif color == "Random":
- mode = 256
- color_nb = random.randint(0, 255)
- color_code = str(color_nb)
-
- elif color in colormaps.keys():
- if color[0].islower(): # lower case first letter
- mode = 8
- c = colormaps[color][colormap_idx]
- if c.isdigit():
- color_code = str(30 + c)
- else:
- color_code = str(30 + colors[c])
-
- else: # upper case
- mode = 256
- color_nb = colormaps[color][colormap_idx]
- color_code = str( color_nb )
-
- if colormap_idx < len(colormaps[color])-1:
- colormap_idx += 1
- else:
- colormap_idx = 0
+ elif color.lower() == "random":
+ color_code = color_random( color )
elif color.lower() == "scale": # "scale" or "Scale"
- try:
- import babel.numbers as bn
- f = float(bn.parse_decimal(text))
- except ImportError:
- f = float(text)
+ color_code = color_scale( color, text )
- # if out of scale, do not color
- if f < scale[0] or f > scale[1]:
- return text
+ # "hash" or "Hash"; useful to randomly but consistently color strings
+ elif color.lower() == "hash":
+ color_code = color_hash( color, text )
- if color[0].islower():
- mode = 8
- cmap = colormaps["spectrum"]
-
- # normalize and scale over the nb of colors in cmap
- i = int( math.ceil( (f - scale[0]) / (scale[1]-scale[0]) * (len(cmap)-1) ) )
-
- color = cmap[i]
- color_code = str(30 + colors[color])
-
- else:
- mode = 256
- cmap = colormaps["Spectrum"]
- i = int( math.ceil( (f - scale[0]) / (scale[1]-scale[0]) * (len(cmap)-1) ) )
- color = cmap[i]
- color_code = str(color)
-
- # Really useful only when using colout as a library
- # thus you can change the "colormap" variable to your favorite one before calling colorin
+ # The user can change the "colormap" variable to its favorite one before calling colorin.
elif color == "colormap":
- color = colormap[colormap_idx]
- if color in colors:
- mode = 8
- color_code = str(30 + colors[color])
- else:
- mode = 256
- color_nb = int(color)
- assert(0 <= color_nb <= 255)
- color_code = str(color_nb)
+ # "default" should have been set to the user-defined colormap.
+ color,color_code = color_map("default")
- if colormap_idx < len(colormap)-1:
- colormap_idx += 1
- else:
- colormap_idx = 0
+ # Registered colormaps should be tested after special colors,
+ # because special tags are also registered as colormaps,
+ # but do not have the same simple behavior.
+ elif color in context["colormaps"].keys():
+ color_code = color_in_colormaps( color )
# 8 colors modes
- elif color in colors:
- mode = 8
- color_code = str(30 + colors[color])
+ elif color in context["colors"]:
+ color_code = str(30 + context["colors"][color])
# hexadecimal color
elif color[0] == "#":
- mode = 256
color_nb = rgb_to_ansi(*hex_to_rgb(color))
assert(0 <= color_nb <= 255)
color_code = str(color_nb)
# 256 colors mode
elif color.isdigit():
- mode = 256
color_nb = int(color)
assert(0 <= color_nb <= 255)
color_code = str(color_nb)
# programming language
- elif color.lower() in lexers:
- lexer = get_lexer_by_name(color.lower())
- # Python => 256 colors, python => 8 colors
- ask_256 = color[0].isupper()
- if ask_256:
- try:
- formatter = Terminal256Formatter(style=style)
- except: # style not found
- formatter = Terminal256Formatter()
- else:
- if style not in ("light","dark"):
- style = "dark" # dark color scheme by default
- formatter = TerminalFormatter(bg=style)
- # We should return all but the last character,
- # because Pygments adds a newline char.
- if not debug:
- return highlight(text, lexer, formatter)[:-1]
- else:
- return "<"+color+">"+ highlight(text, lexer, formatter)[:-1] + ""+color+">"
+ elif color.lower() in context["lexers"]:
+ # bypass color encoding and return text colored by the lexer
+ return color_lexer(color,style,text)
# unrecognized
else:
raise UnknownColor(color)
- if not debug:
- return start + style_code + endmarks[mode] + color_code + "m" + text + stop
+ m = mode(color)
+
+ if background in context["backgrounds"] and m == 8:
+ background_code = endmarks[m] + str(40 + context["backgrounds"][background])
+ elif background == "none":
+ background_code = ""
else:
- return start + style_code + endmarks[mode] + color_code + "m<" + color + ">" + text + "" + color + ">" + stop
+ raise UnknownColor(background)
+
+ if color_code is not None:
+ if not debug:
+ return start + style_code + endmarks[m] + color_code + background_code + "m" + text + stop
+ else:
+ return start + style_code + endmarks[m] + color_code + background_code + "m" \
+ + "" \
+ + text + "" + stop
+ else:
+ if not debug:
+ return text
+ else:
+ return "" + text + ""
def colorout(text, match, prev_end, color="red", style="normal", group=0):
@@ -434,7 +654,7 @@ def colorout(text, match, prev_end, color="red", style="normal", group=0):
return colored_text, end
-def colorup(text, pattern, color="red", style="normal", on_groups=False):
+def colorup(text, pattern, color="red", style="normal", on_groups=False, sep_list=context["sep_list"]):
"""
Color up every characters that match the given regexp patterns.
If groups are specified, only color up them and not the whole pattern.
@@ -443,21 +663,10 @@ def colorup(text, pattern, color="red", style="normal", on_groups=False):
in which case the different matching groups may be formatted differently.
If there is less colors/styles than groups, the last format is used
for the additional groups.
-
- >>> colorup("Fetchez la vache", "vache", "red", "bold")
- 'Fetchez la \x1b[1;31mvache\x1b[0m'
- >>> colorup("Faites chier la vache", "[Fv]a", "red", "bold")
- '\x1b[1;31mFa\x1b[0mites chier la \x1b[1;31mva\x1b[0mche'
- >>> colorup("Faites Chier la Vache", "[A-Z](\S+)\s", "red", "bold")
- 'F\x1b[1;31maites\x1b[0m C\x1b[1;31mhier\x1b[0m la Vache'
- >>> colorup("Faites Chier la Vache", "([A-Z])(\S+)\s", "red,green", "bold")
- '\x1b[1;31mF\x1b[0m\x1b[1;32maites\x1b[0m \x1b[1;31mC\x1b[0m\x1b[1;32mhier\x1b[0m la Vache'
- >>> colorup("Faites Chier la Vache", "([A-Z])(\S+)\s", "green")
- '\x1b[0;32mF\x1b[0m\x1b[0;32maites\x1b[0m \x1b[0;32mC\x1b[0m\x1b[0;32mhier\x1b[0m la Vache'
- >>> colorup("Faites Chier la Vache", "([A-Z])(\S+)\s", "blue", "bold,italic")
- '\x1b[1;34mF\x1b[0m\x1b[3;34maites\x1b[0m \x1b[1;34mC\x1b[0m\x1b[3;34mhier\x1b[0m la Vache'
"""
- global colormap_idx
+
+ global context
+ global debug
if not debug:
regex = re.compile(pattern)
@@ -481,17 +690,17 @@ def colorup(text, pattern, color="red", style="normal", on_groups=False):
# Build a list of colors that match the number of grouped,
# if there is not enough colors, duplicate the last one.
- colors_l = color.split(",")
+ colors_l = color.split(sep_list)
group_colors = colors_l + [colors_l[-1]] * (nb_groups - len(colors_l))
# Same for styles
- styles_l = style.split(",")
+ styles_l = style.split(sep_list)
group_styles = styles_l + [styles_l[-1]] * (nb_groups - len(styles_l))
# If we want to iterate colormaps on groups instead of patterns
if on_groups:
# Reset the counter at the beginning of each match
- colormap_idx = 0
+ context["colormap_idx"] = 0
# For each group index.
# Note that match.groups returns a tuple (thus being indexed in [0,n[),
@@ -556,6 +765,8 @@ def map_write( stream_in, stream_out, function, *args ):
while True:
try:
item = stream_in.readline()
+ except UnicodeDecodeError:
+ continue
except KeyboardInterrupt:
break
if not item:
@@ -563,7 +774,7 @@ def map_write( stream_in, stream_out, function, *args ):
write( function(item, *args), stream_out )
-def colorgen(stream, pattern, color="red", style="normal", on_groups=False):
+def colorgen(stream, pattern, color="red", style="normal", on_groups=False, sep_list=context["sep_list"]):
"""
A generator that colors the items given in an iterable input.
@@ -579,71 +790,14 @@ def colorgen(stream, pattern, color="red", style="normal", on_groups=False):
break
if not item:
break
- yield colorup(item, pattern, color, style, on_groups)
+ yield colorup(item, pattern, color, style, on_groups, sep_list)
######################
# Command line tools #
######################
-def __args_dirty__(argv, usage=""):
- """
- Roughly extract options from the command line arguments.
- To be used only when argparse is not available.
-
- Returns a tuple of (pattern,color,style,on_stderr).
-
- >>> colout.__args_dirty__(["colout","pattern"],"usage")
- ('pattern', 'red', 'normal', False)
- >>> colout.__args_dirty__(["colout","pattern","colors","styles"],"usage")
- ('pattern', 'colors', 'styles', False)
- >>> colout.__args_dirty__(["colout","pattern","colors","styles","True"],"usage")
- ('pattern', 'colors', 'styles', True)
- """
-
- # Use a dirty argument picker
- # Check for bad usage or an help flag
- if len(argv) < 2 \
- or len(argv) > 10 \
- or argv[1] == "--help" \
- or argv[1] == "-h":
- print(usage+"\n")
- print("Usage:", argv[0], " [