From fa556ebcd84c199a3021eb3d33dc7adb46222131 Mon Sep 17 00:00:00 2001 From: Matt Wozniski Date: Mon, 15 Sep 2008 07:30:35 -0400 Subject: [PATCH] Completed beta release of CSApprox.vim --- doc/CSApprox.txt | 356 ++++++++++++++++++++++++ plugin/CSApprox.vim | 663 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1019 insertions(+) diff --git a/doc/CSApprox.txt b/doc/CSApprox.txt index e69de29..e7c3fa0 100644 --- a/doc/CSApprox.txt +++ b/doc/CSApprox.txt @@ -0,0 +1,356 @@ +*CSApprox.txt* Bringing GVim colorschemes to the terminal! + + *csapprox* *csapprox.vim* + + _____ ____ ___ ~ + / ___// __// _ | ___ ___ ____ ___ __ __ ~ + / /__ _\ \ / __ | / _ \ / _ \ / __// _ \ \ \ / ~ + \___//___//_/ |_|/ .__// .__//_/ \___//_\_\ ~ + /_/ /_/ ~ + For Vim version 7.0 or newer + Last changed 12 Sep 2008 + + By Matt Wozniski + mjw@drexel.edu + + Reference Manual~ + + *csapprox-toc* + +1. Description |csapprox-intro| +2. Requirements |csapprox-requirements| +3. Configuration |csapprox-configure| +4. Rationale/Design |csapprox-design| +5. Known Bugs and Limitations |csapprox-limitations| +6. Appendix - Terminals and Palettes |csapprox-terminal-list| +7. Changelog |csapprox-changelog| +8. Contact Info |csapprox-author| + +The functionality mentioned here is a plugin, see |add-plugin|. +You can avoid loading this plugin by setting the "CSApprox_loaded" global +variable in your |vimrc| file: > + :let g:CSApprox_loaded = 1 + +============================================================================== +1. Description *csapprox-intro* + +There is a wealth of colorschemes available for Vim. Unfortunately, since +traditional terminal emulators have only supported 2, 8 or 16 colors, +colorscheme authors have tended to avoid writing colorschemes for terminal +Vim, sticking instead to GVim. Even now that nearly every popular terminal +supports either 88 or 256 colors, few colorschemes are written to support +them. This may be because the terminal color codes are just numbers from 0 to +87 or 255 with no semantic meaning, or because the same number doesn't yield +the same color in all terminals, or simply because the colorscheme author +doesn't use the terminal and doesn't want to take the time to support +terminals. + +Whatever the reason, this leaves users of many modern terminal emulators in +the awkward position of having a terminal emulator that supports many colors, +but having very few colorschemes that were written to utilize those colors. + +This is where CSApprox comes in. It attempts to fill this void by making GVim +colorschemes transparently backwards compatible with terminal Vim in a high +color terminal. Basically, whenever a colorscheme sets some colors for the +GUI, this script runs and tries to figure out the closest color available in +the terminal's color palette to the color the colorscheme author wanted. +Unfortunately, this does not work well all the time, and it has some +limitations (see |csapprox-limitations|). Most of the time, however, this +gives a very close approximation to the GVim colors without requiring any +changes to the colorscheme, or any user interaction. + +============================================================================== +2. Requirements *csapprox-requirements* + +For CSApprox to work, there are 2 major requirements that must be met. + +a) GUI support *csapprox-gui-support* *csapprox-+gui* + +The "vim" binary must be built with GUI support (see |csapprox-limitations| +for an explanation). Unfortunately, several Linux distributions only include +GUI support in their "gvim" binary, and not in their "vim" binary. You can +check if GUI support is available by doing: > + :echo has('gui') + +If you are in an environment where the "vim" binary does not include GUI +support, but where a "gvim" binary is available, you can probably launch Vim +with GUI support anyway using the |-v| flag in the shell: > + gvim -v + +If that does not work and no package with GUI support is available, you will +need to compile Vim yourself and ensure that GUI support is included. If this +is inconvenient for you, make sure that the Vim maintainer for your +distribution knows it. + +b) Properly configured terminal *csapprox-terminal* + +As said above, many modern terminals support 88 or 256 colors, but most of +these default to setting $TERM to something generic (usually "xterm"). Since +Vim uses the value of the "colors" attribute for the current $TERM in terminfo +to figure out the number of colors used internally as 't_Co', this plugin will +either need for 't_Co' to be set to 88 or 256 in |vimrc|, or for $TERM to be +set to something that implies high color support. Possible choices include +"xterm-256color" for 256 color support and "rxvt-unicode" for 88 color +support. + +Also, there are three different 256 color cube palettes available and CSApprox +has no way to tell which you're using unless $TERM is set to something that is +specific to the terminal, like "konsole" or "Eterm". Because of this, the +most sane behavior is assuming the user is using the most popular palette, +which is used by all but Konsole and Eterm, whenever $TERM is set to something +generic like 'xterm' or 'screen'. You can provide a different default, +however - see |csapprox-configuration|. + + *csapprox-terminal-example* +To turn on high color support without fixing $TERM, you can put something like +this into your |vimrc|: > + if &term == 'xterm' && $HOSTNAME == 'my-machine' + " On my machine, I use Konsole with 256 color support + set t_Co=256 + let g:CSApprox_konsole = 1 + endif + +============================================================================== +3. Configuration *csapprox-configure* + +There are several global variables that can be set to configure the behavior +of CSApprox. They are listed roughly based on the likelihood that the end +user might want to know about them. + +g:CSApprox_loaded *g:CSApprox_loaded* + If set in your |vimrc|, CSApprox is not loaded. + +g:CSApprox_verbose_level *g:CSApprox_verbose_level* + While running, CSApprox sets 'verbose' to this value (default 1). This + allows CSApprox to default to warning whenever something might be wrong, + but allows the user to disable this behavior if he wants. The most + important messages will be shown at verbosity level 1, some less important + ones will be shown at verbosity level 2. Internal errors and + configuration errors are shown regardless of verbosity level. + +g:CSApprox_eterm *g:CSApprox_eterm* + If set, CSApprox will use the Eterm palette when 'term' is "xterm" or + "screen*". Otherwise, the xterm palette would be used. + +g:CSApprox_konsole *g:CSApprox_konsole* + If set, CSApprox will use the Konsole palette when 'term' is "xterm" or + "screen*". Otherwise, the xterm palette would be used. + +g:CSApprox_attr_map *g:CSApprox_attr_map* + Since some attributes (like 'guisp') can't be used in a terminal, and + others (like 'italic') are often very ugly in terminals, a generic way to + map between a requested attribute and another attribute is included. This + variable should be set to a Dictionary, where the keys are strings + representing the attributes the author wanted set, and the values are the + strings that the user wants set instead. If a value is '', it means the + attribute should just be ignored. The default is to replace 'italic' with + 'underline', and to use 'fg' instead of 'sp': > + let g:CSApprox_attr_map = { 'italic' : 'underline', 'sp' : 'fg' } +< + Your author prefers disabling bold and italic entirely, so uses this: > + let g:CSApprox_attr_map = { 'bold' : '', 'italic' : '', 'sp' : 'fg' } +< + Note: You can only map an attribute representing a color to another + attribute representing a color; likewise with boolean attributes. + After all, sp -> bold and italic -> fg would be nonsensical. + +g:CSApprox_extra_rgb_txt_dirs *g:CSApprox_extra_rgb_txt_dirs* + When the colorscheme author uses a color by name, CSApprox needs to figure + out what #rrggbb value it stands for. It does this by parsing rgb.txt, + but first needs to locate it. It has a default search path included, but + should it fail to find rgb.txt, this variable can be set to a List of + other directories that ought to be searched. Default search path: > + [ /usr/local/share/X11, /usr/share/X11, /etc/X11, /usr/local/lib/X11, + /usr/lib/X11, /usr/local/X11R6/lib/X11, /usr/X11R6/lib/X11] + +g:CSApprox_approximator_function *g:CSApprox_approximator_function* + If the default approximation function doesn't work well enough, the user + (or another author wishing to extend this plugin) can write another + approximation function. This function should take three numbers, + representing r, g, and b in decimal, and return the index on the color + cube that best matches those colors. Assigning a |Funcref| to this + variable will override the default approximator with the one the Funcref + references. + +g:CSApprox_redirfallback *g:CSApprox_redirfallback* + As of Vim 7.2.018, there is a bug in the Vim function synIDattr() that + stops it from correctly reporting information for the 'guisp' attribute. + CSApprox includes a workaround for this problem, as well as a test that + ought to disable this workaround once synIDattr() is working properly. + If this test should happen to give improper results somehow, the user can + force the behavior with this variable. When set to 1, the workaround will + always be used, and when set to 0, synIDattr() is blindly used. + +============================================================================== +4. Rationale/Design *csapprox-design* + +Ideally, the aim is for CSApprox to be completely transparent to the user. +This is why the approach I take is entirely different from the GuiColorScheme +script, which will break on any but the simplest colorschemes. Unfortunately, +given the difficulty of determining exactly which terminal emulator the user +is running, and what features it supports, and which color palette it's using, +perfect transparency is difficult. So, to this end, I've attempted to default +to settings that make it unlikely that this script ever makes things worse +(this is why I chose not to override t_Co to 256 myself), and I've attempted +to make it easy to override my choice of defaults when necessary (through +g:CSApprox_approximator_function, g:CSApprox_konsole, g:CSApprox_eterm, +g:CSApprox_attr_map, etc). If any of my choices seem to be causing extra work +with no real advantages, though, I'd like to hear about it. Feel free to +email me with any improvements upon my design you might want to suggest. + +============================================================================== +5. Known Bugs and Limitations *csapprox-limitations* + +GUI support is required. + + There is nothing I can do about this given my chosen design. CSApprox works + by being notified every time a colorscheme sets some GUI colors, then + approximating those colors to similar terminal colors. Unfortunately, when + Vim is not built with GUI support, it doesn't bother to store the GUI + colors, so querying for them fails. This leaves me completely unable to + tell what the colorscheme was trying to do. See |csapprox-+gui| for some + potential work arounds if your distribution doesn't provide a Vim with +gui. + +User intervention is sometimes required for information about the terminal. + + This is really an insurmountable problem. Unfortunately, most terminal + emulators default to setting $TERM to 'xterm', even when they're not really + compatible with an xterm. $TERM is really the only reliable way to + find anything at all out about the terminal you're running in, so there's no + way to know if the terminal supports 88 or 256 colors without either the + terminal telling me (using $TERM) or the user telling me (using t_Co). + Similarly, unless $TERM is set to something that implies a certain color + palette ought to be used, there's no way for me to know, so I'm forced to + default to the most common, xterm's palette, and allow the user to override + my choice with |g:CSApprox_konsole| or |g:CSApprox_eterm|. An example of + configuring Vim to work around a terminal where $TERM is set to something + generic without configuring the terminal properly is shown at + |csapprox-terminal-example|. + +Some colorschemes could fail if they try to be too smart. + A colorscheme could decide to only set colors for the mode Vim is running + in. If a scheme only sets GUI colors when the GUI is running, instead of + using the usual approach of setting all colors and letting Vim choose which + to use, my approach falls apart. My method for figuring out what the scheme + author wants the scheme to look like absolutely depends upon him setting the + GUI colors in all modes. Fortunately, the few colorschemes that do this + seem to be, by and large, intended for 256 color terminals already, meaning + that skipping them is the proper behavior. + +It's slow. + For me, it takes Vim's startup time from 0.15 seconds to 0.45 seconds. This + is probably still acceptable, but it is definitely worth trying to cut down + on this time in future versions. + +============================================================================== +6. Appendix - Terminals and Palettes *csapprox-terminal-list* + +What follows is a list of terminals known to have and known not to have high +color support. This list is certainly incomplete; feel free to contact me +with more to add to either list. + + *csapprox-terminals-good* +------------------------------- Good Terminals ------------------------------- + +The most recent versions of each of these terminals can be compiled with +either 88 or 256 color support. + + *csapprox-xterm* +xterm: + 256 color palette + Colors composed of: [ 0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF ] + Greys composed of: [ 0x08, 0x12, 0x1C, 0x26, 0x30, 0x3A, 0x44, 0x4E, + 0x58, 0x62, 0x6C, 0x76, 0x80, 0x8A, 0x94, 0x9E, + 0xA8, 0xB2, 0xBC, 0xC6, 0xD0, 0xDA, 0xE4, 0xEE ] + + *csapprox-urxvt* +rxvt-unicode (urxvt): + 88 colors by default (but a patch is available to use xterm's palette) + Colors composed of: [ 0x00, 0x8B, 0xCD, 0xFF ] + Greys composed of: [ 0x2E, 0x5C, 0x73, 0x8B, 0xA2, 0xB9, 0xD0, 0xE7 ] + + *csapprox-pterm* *csapprox-putty* +PuTTY (pterm; putty.exe): + 256 colors; same palette as xterm + + *csapprox-mrxvt* +Mrxvt (mrxvt): + 256 colors; same palette as xterm + + *csapprox-gnome-terminal* +GNOME Terminal (gnome-terminal): + 256 colors; same palette as xterm + + *csapprox-roxterm* +ROXTerm (roxterm): + 256 colors; same palette as xterm + + *csapprox-xfce4-terminal* +Terminal (xfce4-terminal): + 256 colors; same palette as xterm + + *csapprox-iterm.app* +iTerm (iTerm.app): + 256 colors; same palette as xterm + *csapprox-konsole* +Konsole (konsole): + 256 color palette + Colors composed of: [ 0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF ] + Same greyscales as xterm + You should set the g:CSApprox_konsole variable unless $TERM begins with + 'konsole', case insensitive + + *csapprox-eterm* +eterm (Eterm): + 256 color palette + Colors composed of: [ 0x00, 0x2A, 0x55, 0x7F, 0xAA, 0xD4 ] + Same greyscales as xterm + You should set the g:CSApprox_eterm variable unless $TERM begins with + 'eterm', case insensitive + + *csapprox-screen* +GNU Screen (screen): + 256 color support. Internally, uses the xterm palette, but this is only + relevant when running screen inside a terminal with fewer than 256 colors, + in which case screen will attempt to map between its own 256 color cube + and the colors supported by the real terminal to the best of its ability. + + *csapprox-terminals-bad* +-------------------------------- Bad Terminals ------------------------------- +This is a list of terminals known *_not_* to have high color support: + + *csapprox-terminal.app* +Terminal.app (as of OS X 10.5.2) + + *csapprox-aterm* +aterm (as of version 1.00.01) + + *csapprox-xiterm* +xiterm (as of version 0.5) + + *csapprox-wterm* +wterm (as of version 6.2.9) + + *csapprox-mlterm* +mlterm (as of version 2.9.4) + + *csapprox-kterm* +kterm (as of version 6.2.0) + +============================================================================== +7. Changelog *csapprox-changelog* + + 0.90 14 Sep 2008 Initial beta release + +============================================================================== +8. Contact Info *csapprox-author* + +Your author, a Vim nerd with some free time, was sick of seeing terminals +always get the short end of the stick. He'd like to be notified of any +problems you find - after all, he took the time to write all this lovely +documentation, and this plugin, which took more time than you could possibly +imagine to get working transparently for every colorscheme he could get his +hands on. You can contact him with any problems or praises at mjw@drexel.edu + +============================================================================== +vim:tw=78:fo=tcq2:isk=!-~,^*,^\|,^\":ts=8:ft=help:norl: diff --git a/plugin/CSApprox.vim b/plugin/CSApprox.vim index e69de29..177673a 100644 --- a/plugin/CSApprox.vim +++ b/plugin/CSApprox.vim @@ -0,0 +1,663 @@ +" CSApprox: Approximate gvim colorschemes to suitable terminal colors +" Maintainer: Matthew Wozniski (mjw@drexel.edu) +" Date: Sun, 14 Sep 2008 12:43:33 -0400 +" Version: 0.90 +" History: :help csapprox-changelog + +" Whenever you change colorschemes using the :colorscheme command, this script +" will be executed. If you're running in 256 color terminal or an 88 color +" terminal, as reported by the command ":set t_Co?", it will take the colors +" that the scheme specified for use in the gui and use an approximation +" algorithm to try to gracefully degrade them to the closest color available. +" If you are running in a gui or if t_Co is reported as less than 88 colors, +" no changes are made. Also, no changes will be made if the colorscheme seems +" to have been high color already. + +" {>1} Basic plugin setup + +" {>2} Check preconditions +" Quit if the user doesn't want or need us or is missing the gui feature. We +" need +gui to be able to check the gui color settings; vim doesn't bother to +" store them if it is not built with +gui. +if has("gui_running") || ! has("gui") || exists('g:CSApprox_loaded') + " XXX This depends upon knowing the default for g:CSApprox_verbose_level + if has('gui_running') + \ && exists("g:CSApprox_verbose_level") && g:CSApprox_verbose_level > 1 + echomsg "Not loading CSApprox in gui mode." + elseif ! has('gui') + \ && (!exists("g:CSApprox_verbose_level") || g:CSApprox_verbose_level) + echomsg "CSApprox needs gui support - not loading." + endif + + finish +endif + +" {1} Mark us as loaded, and disable all compatibility options for now. +let g:CSApprox_loaded = 1 + +let s:savecpo = &cpo +set cpo&vim + +" {>1} Built-in approximation algorithm + +" {>2} Cube definitions +let s:xterm_colors = [ 0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF ] +let s:eterm_colors = [ 0x00, 0x2A, 0x55, 0x7F, 0xAA, 0xD4 ] +let s:konsole_colors = [ 0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF ] +let s:xterm_greys = [ 0x08, 0x12, 0x1C, 0x26, 0x30, 0x3A, + \ 0x44, 0x4E, 0x58, 0x62, 0x6C, 0x76, + \ 0x80, 0x8A, 0x94, 0x9E, 0xA8, 0xB2, + \ 0xBC, 0xC6, 0xD0, 0xDA, 0xE4, 0xEE ] + +let s:urxvt_colors = [ 0x00, 0x8B, 0xCD, 0xFF ] +let s:urxvt_greys = [ 0x2E, 0x5C, 0x73, 0x8B, + \ 0xA2, 0xB9, 0xD0, 0xE7 ] + +" {>2} Integer comparator +" Used to sort the complete list of possible colors +function! s:IntCompare(i1, i2) + return a:i1 == a:i2 ? 0 : a:i1 > a:i2 ? 1 : -1 +endfunc + +" {>2} Approximator +" Takes 3 decimal values for r, g, and b, and returns the closest cube number. +" Uses &term to determine which cube should be used, though if &term is set to +" "xterm" the variables g:CSApprox_eterm and g:CSApprox_konsole can be used to +" change the default palette. +" +" This approximator considers closeness based upon the individiual components. +" For each of r, g, and b, it finds the closest cube component available on +" the cube. If the three closest matches can combine to form a valid color, +" this color is used, otherwise we repeat the search with the greys removed, +" meaning that the three new matches must make a valid color when combined. +function! s:ApproximatePerComponent(r,g,b) + let greys = (&t_Co == 88 ? s:urxvt_greys : s:xterm_greys) + + if &t_Co == 88 + let colors = s:urxvt_colors + elseif ((&term ==# 'xterm' || &term =~# '^screen') + \ && exists('g:CSApprox_konsole')) + \ || &term =~? '^konsole' + let colors = s:konsole_colors + elseif ((&term ==# 'xterm' || &term =~# '^screen') + \ && exists('g:CSApprox_eterm')) + \ || &term =~? '^eterm' + let colors = s:eterm_colors + else + let colors = s:xterm_colors + endif + + let greyscolors = sort(greys + colors, "s:IntCompare") + let r = s:NearestElemInList(a:r, greyscolors) + let g = s:NearestElemInList(a:g, greyscolors) + let b = s:NearestElemInList(a:b, greyscolors) + + let len = len(colors) + if (r == g && g == b && index(greys, r) > 0) + return 16 + len * len * len + index(greys, r) + else + let r = s:NearestElemInList(a:r, colors) + let g = s:NearestElemInList(a:g, colors) + let b = s:NearestElemInList(a:b, colors) + return index(colors, r) * len * len + \ + index(colors, g) * len + \ + index(colors, b) + \ + 16 + endif +endfunction + +" {>2} Color comparator +" Finds the nearest element to the given element in the given list +function! s:NearestElemInList(elem, list) + let len = len(a:list) + for i in range(len) + if (i == len - 1) || (a:elem <= (a:list[i] + a:list[i+1]) / 2) + return a:list[i] + endif + endfor +endfunction + +" {>1} Collect info for the set highlights + +" {>2} Determine if synIDattr is usable +" As of 7.2.018, synIDattr() can't be used to check 'guisp', and no official +" patch has been released despite my suggesting one. So, in an attempt to be +" forward compatible, I've included a test to see if synIDattr() works +" properly. If synIDattr() works properly, we'll use it to check the 'guisp' +" attribute, otherwise we'll fall back onto using :redir and checking the +" output of :highlight. This test can be overridden by setting the global +" variable g:CSApprox_redirfallback to 1 (to force use of :redir) or to 0 (to +" force use of synIDattr()). +function! s:NeedRedirFallback() + if !exists("g:CSApprox_redirfallback") + hi CSApproxTest guisp=Red gui=standout + if synIDattr(hlID('CSApproxTest'), 'sp', 'gui') == '1' + " We requested the 'sp' attribute, but vim thought we wanted 'standout' + " So, reporting of the guisp attribute is broken. Fall back on :redir + let g:CSApprox_redirfallback=1 + else + " Reporting guisp works, use synIDattr + let g:CSApprox_redirfallback=0 + endif + endif + return g:CSApprox_redirfallback +endfunction + +" {>2} Collect and store the highlights +" Get a dictionary containing information for every highlight group not merely +" linked to another group. Return value is a dictionary, with highlight group +" numbers for keys and values that are dictionaries with four keys each, +" 'name', 'term', 'cterm', and 'gui'. 'name' holds the group name, and each +" of the others holds highlight information for that particular mode. +function! s:Highlights() + let rv = {} + + let i = 1 + while 1 + if synIDtrans(i) == 0 + break + endif + + if !has_key(rv, synIDtrans(i)) + let group = {} + let group.name = synIDattr(synIDtrans(i), "name") + + for where in [ "term", "cterm", "gui" ] + let group[where] = {} + for attr in [ "fg", "bg", "sp", "bold", "italic", + \ "reverse", "underline", "undercurl" ] + let group[where][attr] = synIDattr(synIDtrans(i), attr, where) + endfor + + if s:NeedRedirFallback() + redir => temp + exe 'sil hi ' . group.name + redir END + let temp = matchstr(temp, where.'sp=\zs.*') + if len(temp) == 0 || temp[0] =~ '\s' + let temp = "" + else + " Make sure we can handle guisp='dark red' + let temp = substitute(temp, '[\x00].*', '', '') + let temp = substitute(temp, '\s*\(c\=term\|gui\).*', '', '') + let temp = substitute(temp, '\s*$', '', '') + endif + let group[where]["sp"] = temp + endif + endfor + + let rv[synIDtrans(i)] = group + endif + + let i += 1 + endwhile + + return rv +endfunction + +" {>1} Handle color names + +" Place to store rgb.txt name to color mappings - lazy loaded if needed +let s:rgb = {} + +" {>2} Builtin gui color names +" gui_x11.c and gui_gtk_x11.c have some default colors names that are searched +" if a color is not in rgb.txt. We'll pretend they're in rgb.txt with these +" values, and overwrite them with a different value if we find them... +let s:rgb_defaults = { "lightred" : "#FFBBBB", + \ "lightgreen" : "#88FF88", + \ "lightmagenta" : "#FFBBFF", + \ "darkcyan" : "#008888", + \ "darkblue" : "#0000BB", + \ "darkred" : "#BB0000", + \ "darkmagenta" : "#BB00BB", + \ "darkgrey" : "#BBBBBB", + \ "darkyellow" : "#BBBB00", + \ "gray10" : "#1A1A1A", + \ "grey10" : "#1A1A1A", + \ "gray20" : "#333333", + \ "grey20" : "#333333", + \ "gray30" : "#4D4D4D", + \ "grey30" : "#4D4D4D", + \ "gray40" : "#666666", + \ "grey40" : "#666666", + \ "gray50" : "#7F7F7F", + \ "grey50" : "#7F7F7F", + \ "gray60" : "#999999", + \ "grey60" : "#999999", + \ "gray70" : "#B3B3B3", + \ "grey70" : "#B3B3B3", + \ "gray80" : "#CCCCCC", + \ "grey80" : "#CCCCCC", + \ "gray90" : "#E5E5E5", + \ "grey90" : "#E5E5E5" } + +" {>2} Find and parse rgb.txt +" Search for an rgb.txt in a set of default directories. If the user wishes +" to override the default search path, he can specify a list of other +" directories to search first in g:CSApprox_extra_rgb_txt_dirs. When rgb.txt +" has been located, and verified to be good (by having enough non-blank +" non-comment correctly formatted lines), the parsed information is stored to +" the dictionary s:rgb - the keys are color names (in lowercase), the values +" are strings representing color values (as '#rrggbb'). +function! s:UpdateRgbHash() + " Pattern for ignored lines - all blanks, or blanks then ! + let ignorepat = '^\s*\%(!.*\)\=$' + " fmt is (blanks?)(red)(blanks)(green)(blanks)(blue)(blanks)(name) + let parsepat = '^\s*\(\d\+\)\s\+\(\d\+\)\s\+\(\d\+\)\s\+\(.*\)$' + + let user = [] + if exists("g:CSApprox_extra_rgb_txt_dirs") + if type(g:CSApprox_extra_rgb_txt_dirs) == type([]) + let user = g:CSApprox_extra_rgb_txt_dirs + else + let user = [ g:CSApprox_extra_rgb_txt_dirs ] + endif + endif + + for dir in user + [ '/usr/local/share/X11', + \ '/usr/share/X11', + \ '/etc/X11', + \ '/usr/local/lib/X11', + \ '/usr/lib/X11', + \ '/usr/local/X11R6/lib/X11', + \ '/usr/X11R6/lib/X11' ] + let s:rgb = copy(s:rgb_defaults) + sil! let lines = readfile(dir . '/rgb.txt') + + for line in lines + if line =~ ignorepat + continue " Line is blank, entirely spaces, or a comment + endif + let v = matchlist(line, parsepat) + if len(v) > 0 + let s:rgb[tolower(v[4])] = printf("%02x%02x%02x", v[1], v[2], v[3]) + endif + endfor + + if len(s:rgb) > 50 + return 0 " Long enough, must have been valid + endif + endfor + + let s:rgb = {} + throw "Failed to find a valid rgb.txt!" +endfunction + +" {>1} Derive and set cterm attributes + +" {>2} Attribute overrides +" Allow the user to override a specified attribute with another attribute. +" For example, the default is to map 'italic' to 'underline' (since many +" terminals cannot display italic text, and gvim itself will replace italics +" with underlines where italicizing is impossible), and to replace 'sp' with +" 'fg' (since terminals can't use one color for the underline and another for +" the foreground, we color the entire word). This default can of course be +" overridden by the user, by setting g:CSApprox_attr_map. This map must be +" a dictionary of string keys, representing the same attributes that synIDattr +" can look up, to string values, representing the attribute mapped to or an +" empty string to disable the given attribute entirely. +function! s:attr_map(attr) + let attr = tolower(a:attr) + + if attr == 'inverse' + let attr = 'reverse' + endif + + let valid_attrs = [ 'bg', 'fg', 'sp', 'bold', 'italic', + \ 'reverse', 'underline', 'undercurl' ] + + if index(valid_attrs, attr) == -1 + throw "Looking up invalid attribute '" . attr . "'" + endif + + if !exists("g:CSApprox_attr_map") || type(g:CSApprox_attr_map) != type({}) + let g:CSApprox_attr_map = { 'italic' : 'underline', 'sp' : 'fg' } + endif + + let rv = get(g:CSApprox_attr_map, attr, attr) + + if index(valid_attrs, rv) == -1 && rv != '' + " The user mapped 'attr' to something invalid + throw "Bad attr map: '" . attr . "' to unknown attribute '" . rv . "'" + endif + + let colorattrs = [ 'fg', 'bg', 'sp' ] + if rv != '' && !!(index(colorattrs, attr)+1) != !!(index(colorattrs, rv)+1) + " The attribute the user mapped to was valid, but of a different type. + throw "Bad attr map: Can't map color attr to boolean (".attr."->".rv.")" + endif + + if rv == 'inverse' + let rv = 'reverse' " Internally always use 'reverse' instead of 'inverse' + elseif rv == 'sp' + " Terminals can't handle the guisp attribute; disable it if it was left on + let rv = '' + endif + + return rv +endfunction + +" {>2} Map gui settings to cterm settings +" Given information about a highlight group, replace the cterm settings with +" the mapped gui settings, applying any attribute overrides along the way. In +" particular, this gives special treatment to the 'reverse' attribute and the +" 'guisp' attribute. In particular, if the 'reverse' attribute is set for +" gvim, we unset it for the terminal and instead set ctermfg to match guibg +" and vice versa, since terminals can consider a 'reverse' flag to mean using +" default-bg-on-default-fg instead of current-bg-on-current-fg. We also +" ensure that the 'sp' attribute is never set for cterm, since no terminal can +" handle that particular highlight. If the user wants to display the guisp +" color, he should map it to either 'fg' or 'bg' using g:CSApprox_attr_map. +function! s:FixupCtermInfo(hl) + let hl = a:hl + + " Find attributes to be set in the terminal + for attr in [ "bold", "italic", "reverse", "underline", "undercurl" ] + let hl.cterm[attr] = '' + if hl.gui[attr] == 1 + if s:attr_map(attr) != '' + let hl.cterm[ s:attr_map(attr) ] = 1 + endif + endif + endfor + + for color in [ "bg", "fg" ] + let eff_color = color + if hl.cterm['reverse'] + let eff_color = (color == 'bg' ? 'fg' : 'bg') + endif + + let hl.cterm[color] = get(hl.gui, s:attr_map(eff_color), '') + endfor + + if hl.gui['sp'] != '' && s:attr_map('sp') != '' + let hl.cterm[s:attr_map('sp')] = hl.gui['sp'] + endif + + if hl.cterm['reverse'] && hl.cterm.bg == '' + let hl.cterm.bg = 'fg' + endif + + if hl.cterm['reverse'] && hl.cterm.fg == '' + let hl.cterm.fg = 'bg' + endif + + if hl.cterm['reverse'] + let hl.cterm.reverse = '' + endif +endfunction + +" {>2} Set cterm colors for a highlight group +" Given the information for a single highlight group (ie, the value of +" one of the items in s:Highlights()), uses s:FixupCtermInfo to parse the +" structure and normalize it for use on a cterm, then handles matching the +" gvim colors to the closest cterm colors by calling the approximator +" specified with g:CSApprox_approximator_function and sets the colors and +" attributes appropriately to match the gui. +function! s:SetCtermFromGui(hl) + let hl = a:hl + + call s:FixupCtermInfo(hl) + + " Set up the default approximator function, if needed + if !exists("g:CSApprox_approximator_function") + let g:CSApprox_approximator_function=function("s:ApproximatePerComponent") + endif + + " Clear existing highlights + exe 'hi ' . hl.name . ' cterm=NONE ctermbg=NONE ctermfg=NONE' + + for which in [ 'bg', 'fg' ] + let val = hl.cterm[which] + + " Skip unset colors + if val == -1 || val == "" + continue + endif + + " Try translating anything but 'fg', 'bg', #rrggbb, and rrggbb from an + " rgb.txt color to a #rrggbb color + if val !~? '^[fb]g$' && val !~ '^#\=\x\{6}$' + if empty(s:rgb) + call s:UpdateRgbHash() + endif + try + let val = s:rgb[tolower(val)] + catch /^/ + if &verbose + echomsg "CSApprox: Colorscheme uses unknown color \"" . val . "\"" + endif + continue + endtry + endif + + if val =~? '^[fb]g$' + exe 'hi ' . hl.name . ' cterm' . which . '=' . val + let hl.cterm[which] = val + elseif val =~ '^#\=\x\{6}$' + let val = substitute(val, '^#', '', '') + let r = str2nr(val[0] . val[1], 16) + let g = str2nr(val[2] . val[3], 16) + let b = str2nr(val[4] . val[5], 16) + let hl.cterm[which] = g:CSApprox_approximator_function(r, g, b) + exe 'hi ' . hl.name . ' cterm' . which . '=' . hl.cterm[which] + else + throw "Internal error handling color: " . val + endif + endfor + + " Finally, set the attributes + let attributes = [] + for attribute in [ 'bold', 'italic', 'underline', 'undercurl' ] + if hl.cterm[attribute] == 1 + let attributes += [ attribute ] + endif + endfor + + if !empty(attributes) + exe 'hi ' . hl.name . ' cterm=' . join(attributes, ',') + endif +endfunction + + +" {>1} Top-level control + +" {>2} Variable storing highlights between runs +" This allows us to remember what the highlights looked like when we last ran, +" and compare against it the next time we're called. Using this means that we +" can avoid work when we'd just be duplicating our own work if we tried to +" match the gui colors to cterm colors again. More subtly, it also allows us +" to support composite colorschemes that start with a :colorscheme to load an +" existing colorscheme, and then add or modify highlights that the sourced +" scheme provides. Since we get called twice by such a scheme, things would +" fall apart without info saved between runs - the first call would set some +" high colors, and the second would bail because some high colors are set; it +" would think that the scheme was already 256 color even though it wasn't. +let s:highlights = {} + +" {>2} Builtin cterm color names above 15 +" Vim defines some color name to high color mappings internally (see +" syntax.c:do_highlight). Since we don't want to overwrite a colorscheme that +" was actually written for a high color terminal with our choices, but have no +" way to tell if a colorscheme was written for a high color terminal, we fall +" back on guessing. If any highlight group has a cterm color set to 16 or +" higher, and it wasn't set by this script, we assume that the user has used +" a high color colorscheme - unless that color is one of the below, which vim +" can set internally when a color is requested by name. +let s:presets_88 = [] +let s:presets_88 += [32] " Brown +let s:presets_88 += [72] " DarkYellow +let s:presets_88 += [84] " Gray +let s:presets_88 += [84] " Grey +let s:presets_88 += [82] " DarkGray +let s:presets_88 += [82] " DarkGrey +let s:presets_88 += [43] " LightBlue +let s:presets_88 += [61] " LightGreen +let s:presets_88 += [63] " LightCyan +let s:presets_88 += [74] " LightRed +let s:presets_88 += [75] " LightMagenta +let s:presets_88 += [78] " LightYellow + +let s:presets_256 = [] +let s:presets_256 += [130] " Brown +let s:presets_256 += [130] " DarkYellow +let s:presets_256 += [248] " Gray +let s:presets_256 += [248] " Grey +let s:presets_256 += [242] " DarkGray +let s:presets_256 += [242] " DarkGrey +let s:presets_256 += [ 81] " LightBlue +let s:presets_256 += [121] " LightGreen +let s:presets_256 += [159] " LightCyan +let s:presets_256 += [224] " LightRed +let s:presets_256 += [225] " LightMagenta +let s:presets_256 += [229] " LightYellow + +" {>2} Highlight comparator +" Comparator that sorts numbers matching the highlight id of the 'Normal' +" group before anything else; all others stay in random order. This allows us +" to ensure that the Normal group is the first group we set. If it weren't, +" we could get E419 or E420 if a later color used guibg=bg or the likes. +function! s:SortNormalFirst(num1, num2) + if a:num1 == hlID('Normal') && a:num1 != a:num2 + return -1 + elseif a:num2 == hlID('Normal') && a:num1 != a:num2 + return 1 + else + return 0 + endif +endfunction + +" {>2} Main function +" Wrapper around the actual implementation to make it easier to ensure that +" all temporary settings are restored by the time we return, whether or not +" something was thrown. Additionally, sets the 'verbose' option to +" g:CSApprox_verbose_level (default 1) for the duration of the main function. +" This allows us to default to a message whenever any error, even +" a recoverable one, occurs, meaning the user quickly finds out when +" something's wrong, but makes it very easy for the user to make us silent. +function! s:CSApprox() + try + let savelz = &lz + let savevbs = &vbs + + set lz + + " colors_name must be unset and reset, or vim will helpfully reload the + " colorscheme when we set the background for the Normal group. + " See the help entries ':hi-normal-cterm' and 'g:colors_name' + if exists("g:colors_name") + let colors_name = g:colors_name + unlet g:colors_name + endif + + " Set up our verbosity level, if needed. + " Default to 1, so the user can know if something's wrong. + if !exists("g:CSApprox_verbose_level") + let g:CSApprox_verbose_level = 1 + endif + sil! let &verbose=g:CSApprox_verbose_level + + call s:CSApproxImpl() + finally + if exists("colors_name") + let g:colors_name = colors_name + endif + + let &lz = savelz + let &vbs = savevbs + endtry +endfunction + +" {>2} CSApprox implementation +" Verifies that the user has not started the gui, and that vim recognizes his +" terminal as having enough colors for us to go on, then gathers the existing +" highlights, removes the ones that match what were set on the last run +" through, and sets the cterm colors to match the gui colors for all modified +" highlights. +function! s:CSApproxImpl() + " Return if not running in an 88/256 color terminal + if has('gui_running') || (&t_Co != 256 && &t_Co != 88) + if &verbose && &t_Co != 256 && &t_Co != 88 + echomsg "CSApprox skipped; terminal only has" &t_Co "colors, not 88/256" + endif + + return + endif + + " Get the current highlight colors + let highlights = s:Highlights() + + " If the Normal group is cleared, set it to gvim's default, black on white + " Though this would be a really weird thing for a scheme to do... *shrug* + if highlights[hlID('Normal')].gui.bg == '' + let highlights[hlID('Normal')].gui.bg = 'white' + endif + + if highlights[hlID('Normal')].gui.fg == '' + let highlights[hlID('Normal')].gui.fg = 'black' + endif + + " Create a list of colors that have changed since the last iteration + let modified = [] + for hlid in keys(highlights) + if !has_key(s:highlights, hlid) + \ || s:highlights[hlid].cterm != highlights[hlid].cterm + \ || s:highlights[hlid].gui != highlights[hlid].gui + let modified += [hlid] + endif + endfor + + " Make sure that the script is not already 256 color by checking to make + " sure that no modified groups are set to a value above 256, unless the + " color they're set to can be set internally by vim (gotten by scraping + " color_numbers_{88,256} in syntax.c:do_highlight) + for hlid in modified + let val = highlights[hlid] + if ( val.cterm.bg > 15 + \ && index(s:presets_{&t_Co}, str2nr(val.cterm.bg)) < 0 + \ && val.cterm.bg != + \ get(get(get(s:highlights, hlid, {}), 'cterm', {}), 'bg', '')) + \ || ( val.cterm.fg > 15 + \ && index(s:presets_{&t_Co}, str2nr(val.cterm.fg)) < 0 + \ && val.cterm.fg != + \ get(get(get(s:highlights, hlid, {}), 'cterm', {}), 'fg', '')) + " The value is set above 15, and wasn't set by us or vim. + if &verbose >= 2 + echomsg 'CSApprox: Exiting - high color found for' val.name + endif + return + endif + endfor + + " Then, set all the modified colors to approximate the gui colors. + call sort(modified, "s:SortNormalFirst") + + " And finally set each modified color's cterm attributes to match gui + for hlid in modified + call s:SetCtermFromGui(highlights[hlid]) + endfor + + " And store the new highlights for use in the next iteration + let s:highlights = s:Highlights() +endfunction + +" {>1} Hooks + +" {>2} Autocmds +" Set up an autogroup to hook us on the completion of any :colorscheme command +augroup CSApprox + au! + au ColorScheme * call s:CSApprox() +augroup END + +" {>2} Execute +" The last thing to do when sourced is to run and actually fix up the colors. +call s:CSApprox() + +" {>1} Restore compatibility options +let &cpo = s:savecpo +unlet s:savecpo + + +" {0} vim:sw=2:sts=2:et:fdm=expr:fde=substitute(matchstr(getline(v\:lnum),'^\\s*"\\s*{\\zs.\\{-}\\ze}'),'^$','=','')