Rainbow
[my-vim-dotfolder.git] / plugin / CSApprox.vim
blob0cb727faba73894cbaf9e526a13619e967a3c0b3
1 " CSApprox:    Make gvim-only colorschemes Just Work terminal vim
2 " Maintainer:  Matthew Wozniski (mjw@drexel.edu)
3 " Date:        Wed, 01 Apr 2009 22:10:19 -0400
4 " Version:     3.50
5 " History:     :help csapprox-changelog
7 " Long Description:
8 " It's hard to find colorschemes for terminal Vim.  Most colorschemes are
9 " written to only support GVim, and don't work at all in terminal Vim.
11 " This plugin makes GVim-only colorschemes Just Work in terminal Vim, as long
12 " as the terminal supports 88 or 256 colors - and most do these days.  This
13 " usually requires no user interaction (but see below for what to do if things
14 " don't Just Work).  After getting this plugin happily installed, any time you
15 " use :colorscheme it will do its magic and make the colorscheme Just Work.
17 " Whenever you change colorschemes using the :colorscheme command this script
18 " will be executed.  It will take the colors that the scheme specified for use
19 " in the GUI and use an approximation algorithm to try to gracefully degrade
20 " them to the closest color available in your terminal.  If you are running in
21 " a GUI or if your terminal doesn't support 88 or 256 colors, no changes are
22 " made.  Also, no changes will be made if the colorscheme seems to have been
23 " high color already.
25 " License:
26 " Copyright (c) 2009, Matthew J. Wozniski
27 " All rights reserved.
29 " Redistribution and use in source and binary forms, with or without
30 " modification, are permitted provided that the following conditions are met:
31 "     * Redistributions of source code must retain the above copyright notice,
32 "       this list of conditions and the following disclaimer.
33 "     * Redistributions in binary form must reproduce the above copyright
34 "       notice, this list of conditions and the following disclaimer in the
35 "       documentation and/or other materials provided with the distribution.
36 "     * The names of the contributors may not be used to endorse or promote
37 "       products derived from this software without specific prior written
38 "       permission.
40 " THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS
41 " OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
42 " OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
43 " NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT,
44 " INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
45 " LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
46 " OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
47 " LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
48 " NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
49 " EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
51 " {>1} Basic plugin setup
53 " {>2} Check preconditions
54 " Quit if the user doesn't want or need us or is missing the gui feature.  We
55 " need +gui to be able to check the gui color settings; vim doesn't bother to
56 " store them if it is not built with +gui.
57 if !has('gui') || exists('g:CSApprox_loaded')
58   " XXX This depends upon knowing the default for g:CSApprox_verbose_level
59   let s:verbose = 1
60   if exists("g:CSApprox_verbose_level")
61     let s:verbose  = g:CSApprox_verbose_level
62   endif
64   if ! has('gui') && s:verbose > 0
65     echomsg "CSApprox needs gui support - not loading."
66     echomsg "  See :help |csapprox-+gui| for possible workarounds."
67   endif
69   unlet s:verbose
71   finish
72 endif
74 " {1} Mark us as loaded, and disable all compatibility options for now.
75 let g:CSApprox_loaded = 1
77 let s:savecpo = &cpo
78 set cpo&vim
80 " {>1} Built-in approximation algorithm
82 " {>2} Cube definitions
83 let s:xterm_colors   = [ 0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF ]
84 let s:eterm_colors   = [ 0x00, 0x2A, 0x55, 0x7F, 0xAA, 0xD4 ]
85 let s:konsole_colors = [ 0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF ]
86 let s:xterm_greys    = [ 0x08, 0x12, 0x1C, 0x26, 0x30, 0x3A,
87                        \ 0x44, 0x4E, 0x58, 0x62, 0x6C, 0x76,
88                        \ 0x80, 0x8A, 0x94, 0x9E, 0xA8, 0xB2,
89                        \ 0xBC, 0xC6, 0xD0, 0xDA, 0xE4, 0xEE ]
91 let s:urxvt_colors   = [ 0x00, 0x8B, 0xCD, 0xFF ]
92 let s:urxvt_greys    = [ 0x2E, 0x5C, 0x73, 0x8B,
93                        \ 0xA2, 0xB9, 0xD0, 0xE7 ]
95 " {>2} Integer comparator
96 " Used to sort the complete list of possible colors
97 function! s:IntCompare(i1, i2)
98   return a:i1 == a:i2 ? 0 : a:i1 > a:i2 ? 1 : -1
99 endfunc
101 " {>2} Approximator
102 " Takes 3 decimal values for r, g, and b, and returns the closest cube number.
103 " Uses &term to determine which cube should be used, though if &term is set to
104 " "xterm" or begins with "screen", the variables g:CSApprox_eterm and
105 " g:CSApprox_konsole can be used to select a different palette.
107 " This approximator considers closeness based upon the individiual components.
108 " For each of r, g, and b, it finds the closest cube component available on
109 " the cube.  If the three closest matches can combine to form a valid color,
110 " this color is used, otherwise we repeat the search with the greys removed,
111 " meaning that the three new matches must make a valid color when combined.
112 function! s:ApproximatePerComponent(r,g,b)
113   let hex = printf("%02x%02x%02x", a:r, a:g, a:b)
115   let greys  = (&t_Co == 88 ? s:urxvt_greys : s:xterm_greys)
117   if &t_Co == 88
118     let colors = s:urxvt_colors
119     let type = 'urxvt'
120   elseif ((&term ==# 'xterm' || &term =~# '^screen' || &term==# 'builtin_gui')
121        \   && exists('g:CSApprox_konsole') && g:CSApprox_konsole)
122        \ || &term =~? '^konsole'
123     let colors = s:konsole_colors
124     let type = 'konsole'
125   elseif ((&term ==# 'xterm' || &term =~# '^screen' || &term==# 'builtin_gui')
126        \   && exists('g:CSApprox_eterm') && g:CSApprox_eterm)
127        \ || &term =~? '^eterm'
128     let colors = s:eterm_colors
129     let type = 'eterm'
130   else
131     let colors = s:xterm_colors
132     let type = 'xterm'
133   endif
135   if !exists('s:approximator_cache_'.type)
136     let s:approximator_cache_{type} = {}
137   endif
139   let rv = get(s:approximator_cache_{type}, hex, -1)
140   if rv != -1
141     return rv
142   endif
144   " Only obtain sorted list once
145   if !exists("s:".type."_greys_colors")
146     let s:{type}_greys_colors = sort(greys + colors, "s:IntCompare")
147   endif
149   let greys_colors = s:{type}_greys_colors
151   let r = s:NearestElemInList(a:r, greys_colors)
152   let g = s:NearestElemInList(a:g, greys_colors)
153   let b = s:NearestElemInList(a:b, greys_colors)
155   let len = len(colors)
156   if (r == g && g == b && index(greys, r) != -1)
157     let rv = 16 + len * len * len + index(greys, r)
158   else
159     let r = s:NearestElemInList(a:r, colors)
160     let g = s:NearestElemInList(a:g, colors)
161     let b = s:NearestElemInList(a:b, colors)
162     let rv = index(colors, r) * len * len
163          \ + index(colors, g) * len
164          \ + index(colors, b)
165          \ + 16
166   endif
168   let s:approximator_cache_{type}[hex] = rv
169   return rv
170 endfunction
172 " {>2} Color comparator
173 " Finds the nearest element to the given element in the given list
174 function! s:NearestElemInList(elem, list)
175   let len = len(a:list)
176   for i in range(len-1)
177     if (a:elem <= (a:list[i] + a:list[i+1]) / 2)
178       return a:list[i]
179     endif
180   endfor
181   return a:list[len-1]
182 endfunction
184 " {>1} Collect info for the set highlights
186 " {>2} Determine if synIDattr is usable
187 " synIDattr() couldn't support 'guisp' until 7.2.052.  This function returns
188 " true if :redir is needed to find the 'guisp' attribute, false if synIDattr()
189 " is functional.  This test can be overridden by setting the global variable
190 " g:CSApprox_redirfallback to 1 (to force use of :redir) or to 0 (to force use
191 " of synIDattr()).
192 function! s:NeedRedirFallback()
193   if !exists("g:CSApprox_redirfallback")
194     let g:CSApprox_redirfallback = (v:version == 702 && !has('patch52'))
195                                  \  || v:version < 702
196   endif
197   return g:CSApprox_redirfallback
198 endfunction
200 " {>2} Collect and store the highlights
201 " Get a dictionary containing information for every highlight group not merely
202 " linked to another group.  Return value is a dictionary, with highlight group
203 " numbers for keys and values that are dictionaries with four keys each,
204 " 'name', 'term', 'cterm', and 'gui'.  'name' holds the group name, and each
205 " of the others holds highlight information for that particular mode.
206 function! s:Highlights(modes)
207   let rv = {}
209   let i = 0
210   while 1
211     let i += 1
213     " Only interested in groups that exist and aren't linked
214     if synIDtrans(i) == 0
215       break
216     endif
218     " Handle vim bug allowing groups with name == "" to be created
219     if synIDtrans(i) != i || len(synIDattr(i, "name")) == 0
220       continue
221     endif
223     let rv[i] = {}
224     let rv[i].name = synIDattr(i, "name")
226     for where in a:modes
227       let rv[i][where]  = {}
228       for attr in [ "bold", "italic", "reverse", "underline", "undercurl" ]
229         let rv[i][where][attr] = synIDattr(i, attr, where)
230       endfor
232       for attr in [ "fg", "bg" ]
233         let rv[i][where][attr] = synIDattr(i, attr.'#', where)
234       endfor
236       if where == "gui"
237         let rv[i][where]["sp"] = s:SynGuiSp(i, rv[i].name)
238       else
239         let rv[i][where]["sp"] = -1
240       endif
242       for attr in [ "fg", "bg", "sp" ]
243         if rv[i][where][attr] == -1
244           let rv[i][where][attr] = ''
245         endif
246       endfor
247     endfor
248   endwhile
250   return rv
251 endfunction
253 " {>2} Retrieve guisp
255 " Get guisp using whichever method is specified by _redir_fallback
256 function! s:SynGuiSp(idx, name)
257   if !s:NeedRedirFallback()
258     return s:SynGuiSpAttr(a:idx)
259   else
260     return s:SynGuiSpRedir(a:name)
261   endif
262 endfunction
264 " {>3} Implementation for retrieving guisp with redir hack
265 function! s:SynGuiSpRedir(name)
266   redir => temp
267   exe 'sil hi ' . a:name
268   redir END
269   let temp = matchstr(temp, 'guisp=\zs.*')
270   if len(temp) == 0 || temp[0] =~ '\s'
271     let temp = ""
272   else
273     " Make sure we can handle guisp='dark red'
274     let temp = substitute(temp, '[\x00].*', '', '')
275     let temp = substitute(temp, '\s*\(c\=term\|gui\).*', '', '')
276     let temp = substitute(temp, '\s*$', '', '')
277   endif
278   return temp
279 endfunction
281 " {>3} Implementation for retrieving guisp with synIDattr()
282 function! s:SynGuiSpAttr(idx)
283   return synIDattr(a:idx, 'sp#', 'gui')
284 endfunction
286 " {>1} Handle color names
288 " Place to store rgb.txt name to color mappings - lazy loaded if needed
289 let s:rgb = {}
291 " {>2} Builtin gui color names
292 " gui_x11.c and gui_gtk_x11.c have some default colors names that are searched
293 " if the x server doesn't know about a color.  If 'showrgb' is available,
294 " we'll default to using these color names and values, and overwrite them with
295 " other values if 'showrgb' tells us about those colors.
296 let s:rgb_defaults = { "lightred"     : "#FFBBBB",
297                      \ "lightgreen"   : "#88FF88",
298                      \ "lightmagenta" : "#FFBBFF",
299                      \ "darkcyan"     : "#008888",
300                      \ "darkblue"     : "#0000BB",
301                      \ "darkred"      : "#BB0000",
302                      \ "darkmagenta"  : "#BB00BB",
303                      \ "darkgrey"     : "#BBBBBB",
304                      \ "darkyellow"   : "#BBBB00",
305                      \ "gray10"       : "#1A1A1A",
306                      \ "grey10"       : "#1A1A1A",
307                      \ "gray20"       : "#333333",
308                      \ "grey20"       : "#333333",
309                      \ "gray30"       : "#4D4D4D",
310                      \ "grey30"       : "#4D4D4D",
311                      \ "gray40"       : "#666666",
312                      \ "grey40"       : "#666666",
313                      \ "gray50"       : "#7F7F7F",
314                      \ "grey50"       : "#7F7F7F",
315                      \ "gray60"       : "#999999",
316                      \ "grey60"       : "#999999",
317                      \ "gray70"       : "#B3B3B3",
318                      \ "grey70"       : "#B3B3B3",
319                      \ "gray80"       : "#CCCCCC",
320                      \ "grey80"       : "#CCCCCC",
321                      \ "gray90"       : "#E5E5E5",
322                      \ "grey90"       : "#E5E5E5" }
324 " {>2} Colors that vim will use by name in one of the default schemes, either
325 " for bg=light or for bg=dark.  This lets us avoid loading the entire rgb.txt
326 " database when the scheme itself doesn't ask for colors by name.
327 let s:rgb_presets = { "black"         : "#000000",
328                      \ "blue"         : "#0000ff",
329                      \ "brown"        : "#a52a2a",
330                      \ "cyan"         : "#00ffff",
331                      \ "darkblue"     : "#00008b",
332                      \ "darkcyan"     : "#008b8b",
333                      \ "darkgrey"     : "#a9a9a9",
334                      \ "darkmagenta"  : "#8b008b",
335                      \ "green"        : "#00ff00",
336                      \ "grey"         : "#bebebe",
337                      \ "grey40"       : "#666666",
338                      \ "grey90"       : "#e5e5e5",
339                      \ "lightblue"    : "#add8e6",
340                      \ "lightcyan"    : "#e0ffff",
341                      \ "lightgrey"    : "#d3d3d3",
342                      \ "lightmagenta" : "#ffbbff",
343                      \ "magenta"      : "#ff00ff",
344                      \ "red"          : "#ff0000",
345                      \ "seagreen"     : "#2e8b57",
346                      \ "white"        : "#ffffff",
347                      \ "yellow"       : "#ffff00" }
349 " {>2} Find available color names
350 " Find the valid named colors.  By default, use our own rgb list, but try to
351 " retrieve the system's list if g:CSApprox_use_showrgb is set to true.  Store
352 " the color names and color values to the dictionary s:rgb - the keys are
353 " color names (in lowercase), the values are strings representing color values
354 " (as '#rrggbb').
355 function! s:UpdateRgbHash()
356   try
357     if !exists("g:CSApprox_use_showrgb") || !g:CSApprox_use_showrgb
358       throw "Not using showrgb"
359     endif
361     " We want to use the 'showrgb' program, if it's around
362     let lines = split(system('showrgb'), '\n')
364     if v:shell_error || !exists('lines') || empty(lines)
365       throw "'showrgb' didn't give us an rgb.txt"
366     endif
368     let s:rgb = copy(s:rgb_defaults)
370     " fmt is (blanks?)(red)(blanks)(green)(blanks)(blue)(blanks)(name)
371     let parsepat  = '^\s*\(\d\+\)\s\+\(\d\+\)\s\+\(\d\+\)\s\+\(.*\)$'
373     for line in lines
374       let v = matchlist(line, parsepat)
375       if len(v) < 0
376         throw "CSApprox: Bad RGB line: " . string(line)
377       endif
378       let s:rgb[tolower(v[4])] = printf("#%02x%02x%02x", v[1], v[2], v[3])
379     endfor
380   catch
381     try
382       let s:rgb = csapprox#rgb()
383     catch
384       echohl ErrorMsg
385       echomsg "Can't call rgb() from autoload/csapprox.vim"
386       echomsg "Named colors will not be available!"
387       echohl None
388     endtry
389   endtry
391   return 0
392 endfunction
394 " {>1} Derive and set cterm attributes
396 " {>2} Attribute overrides
397 " Allow the user to override a specified attribute with another attribute.
398 " For example, the default is to map 'italic' to 'underline' (since many
399 " terminals cannot display italic text, and gvim itself will replace italics
400 " with underlines where italicizing is impossible), and to replace 'sp' with
401 " 'fg' (since terminals can't use one color for the underline and another for
402 " the foreground, we color the entire word).  This default can of course be
403 " overridden by the user, by setting g:CSApprox_attr_map.  This map must be
404 " a dictionary of string keys, representing the same attributes that synIDattr
405 " can look up, to string values, representing the attribute mapped to or an
406 " empty string to disable the given attribute entirely.
407 function! s:attr_map(attr)
408   let rv = get(g:CSApprox_attr_map, a:attr, a:attr)
410   return rv
411 endfunction
413 function! s:NormalizeAttrMap(map)
414   let old = copy(a:map)
415   let new = filter(a:map, '0')
417   let valid_attrs = [ 'bg', 'fg', 'sp', 'bold', 'italic',
418                     \ 'reverse', 'underline', 'undercurl' ]
420   let colorattrs = [ 'fg', 'bg', 'sp' ]
422   for olhs in keys(old)
423     if olhs ==? 'inverse'
424       let nlhs = 'reverse'
425     endif
427     let orhs = old[olhs]
429     if orhs ==? 'inverse'
430       let nrhs = 'reverse'
431     endif
433     let nlhs = tolower(olhs)
434     let nrhs = tolower(orhs)
436     try
437       if index(valid_attrs, nlhs) == -1
438         echomsg "CSApprox: Bad attr map (removing unrecognized attribute " . olhs . ")"
439       elseif nrhs != '' && index(valid_attrs, nrhs) == -1
440         echomsg "CSApprox: Bad attr map (removing unrecognized attribute " . orhs . ")"
441       elseif nrhs != '' && !!(index(colorattrs, nlhs)+1) != !!(index(colorattrs, nrhs)+1)
442         echomsg "CSApprox: Bad attr map (removing " . olhs . "; type mismatch with " . orhs . ")"
443       elseif nrhs == 'sp'
444         echomsg "CSApprox: Bad attr map (removing " . olhs . "; can't map to 'sp')"
445       else
446         let new[nlhs] = nrhs
447       endif
448     catch
449       echo v:exception
450     endtry
451   endfor
452 endfunction
454 " {>2} Normalize the GUI settings of a highlight group
455 " If the Normal group is cleared, set it to gvim's default, black on white
456 " Though this would be a really weird thing for a scheme to do... *shrug*
457 function! s:FixupGuiInfo(highlights)
458   if a:highlights[s:hlid_normal].gui.bg == ''
459     let a:highlights[s:hlid_normal].gui.bg = 'white'
460   endif
462   if a:highlights[s:hlid_normal].gui.fg == ''
463     let a:highlights[s:hlid_normal].gui.fg = 'black'
464   endif
465 endfunction
467 " {>2} Map gui settings to cterm settings
468 " Given information about a highlight group, replace the cterm settings with
469 " the mapped gui settings, applying any attribute overrides along the way.  In
470 " particular, this gives special treatment to the 'reverse' attribute and the
471 " 'guisp' attribute.  In particular, if the 'reverse' attribute is set for
472 " gvim, we unset it for the terminal and instead set ctermfg to match guibg
473 " and vice versa, since terminals can consider a 'reverse' flag to mean using
474 " default-bg-on-default-fg instead of current-bg-on-current-fg.  We also
475 " ensure that the 'sp' attribute is never set for cterm, since no terminal can
476 " handle that particular highlight.  If the user wants to display the guisp
477 " color, he should map it to either 'fg' or 'bg' using g:CSApprox_attr_map.
478 function! s:FixupCtermInfo(highlights)
479   for hl in values(a:highlights)
481     if !has_key(hl, 'cterm')
482       let hl["cterm"] = {}
483     endif
485     " Find attributes to be set in the terminal
486     for attr in [ "bold", "italic", "reverse", "underline", "undercurl" ]
487       let hl.cterm[attr] = ''
488       if hl.gui[attr] == 1
489         if s:attr_map(attr) != ''
490           let hl.cterm[ s:attr_map(attr) ] = 1
491         endif
492       endif
493     endfor
495     for color in [ "bg", "fg" ]
496       let eff_color = color
497       if hl.cterm['reverse']
498         let eff_color = (color == 'bg' ? 'fg' : 'bg')
499       endif
501       let hl.cterm[color] = get(hl.gui, s:attr_map(eff_color), '')
502     endfor
504     if hl.gui['sp'] != '' && s:attr_map('sp') != ''
505       let hl.cterm[s:attr_map('sp')] = hl.gui['sp']
506     endif
508     if hl.cterm['reverse'] && hl.cterm.bg == ''
509       let hl.cterm.bg = 'fg'
510     endif
512     if hl.cterm['reverse'] && hl.cterm.fg == ''
513       let hl.cterm.fg = 'bg'
514     endif
516     if hl.cterm['reverse']
517       let hl.cterm.reverse = ''
518     endif
519   endfor
520 endfunction
522 " {>2} Set cterm colors for a highlight group
523 " Given the information for a single highlight group (ie, the value of
524 " one of the items in s:Highlights() already normalized with s:FixupCtermInfo
525 " and s:FixupGuiInfo), handle matching the gvim colors to the closest cterm
526 " colors by calling the appropriate approximator as specified with the
527 " g:CSApprox_approximator_function variable and set the colors and attributes
528 " appropriately to match the gui.
529 function! s:SetCtermFromGui(hl)
530   let hl = a:hl
532   " Set up the default approximator function, if needed
533   if !exists("g:CSApprox_approximator_function")
534     let g:CSApprox_approximator_function=function("s:ApproximatePerComponent")
535   endif
537   " Clear existing highlights
538   exe 'hi ' . hl.name . ' cterm=NONE ctermbg=NONE ctermfg=NONE'
540   for which in [ 'bg', 'fg' ]
541     let val = hl.cterm[which]
543     " Skip unset colors
544     if val == -1 || val == ""
545       continue
546     endif
548     " Try translating anything but 'fg', 'bg', #rrggbb, and rrggbb from an
549     " rgb.txt color to a #rrggbb color
550     if val !~? '^[fb]g$' && val !~ '^#\=\x\{6}$'
551       try
552         " First see if it is in our preset-by-vim rgb list
553         let val = s:rgb_presets[tolower(val)]
554       catch
555         " Then try loading and checking our real rgb list
556         if empty(s:rgb)
557           call s:UpdateRgbHash()
558         endif
559         try
560           let val = s:rgb[tolower(val)]
561         catch
562           " And then barf if we still haven't found it
563           if &verbose
564             echomsg "CSApprox: Colorscheme uses unknown color \"" . val . "\""
565           endif
566           continue
567         endtry
568       endtry
569     endif
571     if val =~? '^[fb]g$'
572       exe 'hi ' . hl.name . ' cterm' . which . '=' . val
573       let hl.cterm[which] = val
574     elseif val =~ '^#\=\x\{6}$'
575       let val = substitute(val, '^#', '', '')
576       let r = str2nr(val[0:1], 16)
577       let g = str2nr(val[2:3], 16)
578       let b = str2nr(val[4:5], 16)
579       let hl.cterm[which] = g:CSApprox_approximator_function(r, g, b)
580       exe 'hi ' . hl.name . ' cterm' . which . '=' . hl.cterm[which]
581     else
582       throw "Internal error handling color: " . val
583     endif
584   endfor
586   " Finally, set the attributes
587   let attrs = [ 'bold', 'italic', 'underline', 'undercurl' ]
588   call filter(attrs, 'hl.cterm[v:val] == 1')
590   if !empty(attrs)
591     exe 'hi ' . hl.name . ' cterm=' . join(attrs, ',')
592   endif
593 endfunction
596 " {>1} Top-level control
598 " Cache the highlight ID of the normal group; it's used often and won't change
599 let s:hlid_normal = hlID('Normal')
601 " {>2} Builtin cterm color names above 15
602 " Vim defines some color name to high color mappings internally (see
603 " syntax.c:do_highlight).  Since we don't want to overwrite a colorscheme that
604 " was actually written for a high color terminal with our choices, but have no
605 " way to tell if a colorscheme was written for a high color terminal, we fall
606 " back on guessing.  If any highlight group has a cterm color set to 16 or
607 " higher, we assume that the user has used a high color colorscheme - unless
608 " that color is one of the below, which vim can set internally when a color is
609 " requested by name.
610 let s:presets_88  = []
611 let s:presets_88 += [32] " Brown
612 let s:presets_88 += [72] " DarkYellow
613 let s:presets_88 += [84] " Gray
614 let s:presets_88 += [84] " Grey
615 let s:presets_88 += [82] " DarkGray
616 let s:presets_88 += [82] " DarkGrey
617 let s:presets_88 += [43] " LightBlue
618 let s:presets_88 += [61] " LightGreen
619 let s:presets_88 += [63] " LightCyan
620 let s:presets_88 += [74] " LightRed
621 let s:presets_88 += [75] " LightMagenta
622 let s:presets_88 += [78] " LightYellow
624 let s:presets_256  = []
625 let s:presets_256 += [130] " Brown
626 let s:presets_256 += [130] " DarkYellow
627 let s:presets_256 += [248] " Gray
628 let s:presets_256 += [248] " Grey
629 let s:presets_256 += [242] " DarkGray
630 let s:presets_256 += [242] " DarkGrey
631 let s:presets_256 += [ 81] " LightBlue
632 let s:presets_256 += [121] " LightGreen
633 let s:presets_256 += [159] " LightCyan
634 let s:presets_256 += [224] " LightRed
635 let s:presets_256 += [225] " LightMagenta
636 let s:presets_256 += [229] " LightYellow
638 " {>2} Wrapper around :exe to allow :executing multiple commands.
639 " "cmd" is the command to be :executed.
640 " If the variable is a String, it is :executed.
641 " If the variable is a List, each element is :executed.
642 function! s:exe(cmd)
643   if type(a:cmd) == type('')
644     exe a:cmd
645   else
646     for cmd in a:cmd
647       call s:exe(cmd)
648     endfor
649   endif
650 endfunction
652 " {>2} Function to handle hooks
653 " Prototype: HandleHooks(type [, scheme])
654 " "type" is the type of hook to be executed, ie. "pre" or "post"
655 " "scheme" is the name of the colorscheme that is currently active, if known
657 " If the variables g:CSApprox_hook_{type} and g:CSApprox_hook_{scheme}_{type}
658 " exist, this will :execute them in that order.  If one does not exist, it
659 " will silently be ignored.
661 " If the scheme name contains characters that are invalid in a variable name,
662 " they will simply be removed.  Ie, g:colors_name = "123 foo_bar-baz456"
663 " becomes "foo_barbaz456"
665 " NOTE: Exceptions will be printed out, rather than end processing early.  The
666 " rationale is that it is worse for the user to fix the hook in an editor with
667 " broken colors.  :)
668 function! s:HandleHooks(type, ...)
669   let type = a:type
670   let scheme = (a:0 == 1 ? a:1 : "")
671   let scheme = substitute(scheme, '[^[:alnum:]_]', '', 'g')
672   let scheme = substitute(scheme, '^\d\+', '', '')
674   for cmd in [ 'g:CSApprox_hook_' . type,
675              \ 'g:CSApprox_' . scheme . '_hook_' . type,
676              \ 'g:CSApprox_hook_' . scheme . '_' . type ]
677     if exists(cmd)
678       try
679         call s:exe(eval(cmd))
680       catch
681         echomsg "Error processing " . cmd . ":"
682         echomsg v:exception
683       endtry
684     endif
685   endfor
686 endfunction
688 " {>2} Main function
689 " Wrapper around the actual implementation to make it easier to ensure that
690 " all temporary settings are restored by the time we return, whether or not
691 " something was thrown.  Additionally, sets the 'verbose' option to the max of
692 " g:CSApprox_verbose_level (default 1) and &verbose for the duration of the
693 " main function.  This allows us to default to a message whenever any error,
694 " even a recoverable one, occurs, meaning the user quickly finds out when
695 " something's wrong, but makes it very easy for the user to make us silent.
696 function! s:CSApprox()
697   try
698     let savelz  = &lz
700     set lz
702     if exists("g:CSApprox_attr_map") && type(g:CSApprox_attr_map) == type({})
703       call s:NormalizeAttrMap(g:CSApprox_attr_map)
704     else
705       let g:CSApprox_attr_map = { 'italic' : 'underline', 'sp' : 'fg' }
706     endif
708     " colors_name must be unset and reset, or vim will helpfully reload the
709     " colorscheme when we set the background for the Normal group.
710     " See the help entries ':hi-normal-cterm' and 'g:colors_name'
711     if exists("g:colors_name")
712       let colors_name = g:colors_name
713       unlet g:colors_name
714     endif
716     " Similarly, the global variable "syntax_cmd" must be set to something vim
717     " doesn't recognize, lest vim helpfully switch all colors back to the
718     " default whenever the Normal group is changed (in syncolor.vim)...
719     if exists("g:syntax_cmd")
720       let syntax_cmd = g:syntax_cmd
721     endif
722     let g:syntax_cmd = "PLEASE DON'T CHANGE ANY COLORS!!!"
724     " Set up our verbosity level, if needed.
725     " Default to 1, so the user can know if something's wrong.
726     if !exists("g:CSApprox_verbose_level")
727       let g:CSApprox_verbose_level = 1
728     endif
730     call s:HandleHooks("pre", (exists("colors_name") ? colors_name : ""))
732     " Set 'verbose' set to the maximum of &verbose and CSApprox_verbose_level
733     exe max([&vbs, g:CSApprox_verbose_level]) 'verbose call s:CSApproxImpl()'
735     call s:HandleHooks("post", (exists("colors_name") ? colors_name : ""))
736   finally
737     if exists("colors_name")
738       let g:colors_name = colors_name
739     endif
741     unlet g:syntax_cmd
742     if exists("syntax_cmd")
743       let g:syntax_cmd = syntax_cmd
744     endif
746     let &lz   = savelz
747   endtry
748 endfunction
750 " {>2} CSApprox implementation
751 " Verifies that the user has not started the gui, and that vim recognizes his
752 " terminal as having enough colors for us to go on, then gathers the existing
753 " highlights and sets the cterm colors to match the gui colors for all those
754 " highlights (unless the colorscheme was already high-color).
755 function! s:CSApproxImpl()
756   " Return if not running in an 88/256 color terminal
757   if &t_Co != 256 && &t_Co != 88
758     if &verbose && !has('gui_running')
759       echomsg "CSApprox skipped; terminal only has" &t_Co "colors, not 88/256"
760       echomsg "Try checking :help csapprox-terminal for workarounds"
761     endif
763     return
764   endif
766   " Get the current highlight colors
767   let highlights = s:Highlights(["gui"])
769   let hinums = keys(highlights)
771   " Make sure that the script is not already 256 color by checking to make
772   " sure that no groups are set to a value above 256, unless the color they're
773   " set to can be set internally by vim (gotten by scraping
774   " color_numbers_{88,256} in syntax.c:do_highlight)
775   "
776   " XXX: s:inhibit_hicolor_test allows this test to be skipped for snapshots
777   if !exists("s:inhibit_hicolor_test") || !s:inhibit_hicolor_test
778     for hlid in hinums
779       for type in [ 'bg', 'fg' ]
780         let color = synIDattr(hlid, type, 'cterm')
782         if color > 15 && index(s:presets_{&t_Co}, str2nr(color)) < 0
783           " The value is set above 15, and wasn't set by vim.
784           if &verbose >= 2
785             echomsg 'CSApprox: Exiting - high' type 'color found for' highlights[hlid].name
786           endif
787           return
788         endif
789       endfor
790     endfor
791   endif
793   call s:FixupGuiInfo(highlights)
794   call s:FixupCtermInfo(highlights)
796   " We need to set the Normal group first so 'bg' and 'fg' work as colors
797   call insert(hinums, remove(hinums, index(hinums, string(s:hlid_normal))))
799   " then set each color's cterm attributes to match gui
800   for hlid in hinums
801     call s:SetCtermFromGui(highlights[hlid])
802   endfor
803 endfunction
805 " {>2} Write out the current colors to an 88/256 color colorscheme file.
806 " "file" - destination filename
807 " "overwrite" - overwrite an existing file
808 function! s:CSApproxSnapshot(file, overwrite)
809   let force = a:overwrite
810   let file = fnamemodify(a:file, ":p")
812   if empty(file)
813     throw "Bad file name: \"" . file . "\""
814   elseif (filewritable(fnamemodify(file, ':h')) != 2)
815     throw "Cannot write to directory \"" . fnamemodify(file, ':h') . "\""
816   elseif (glob(file) || filereadable(file)) && !force
817     " TODO - respect 'confirm' here and prompt if it's set.
818     echohl ErrorMsg
819     echomsg "E13: File exists (add ! to override)"
820     echohl None
821     return
822   endif
824   " Sigh... This is basically a bug, but one that I have no chance of fixing.
825   " Vim decides that Pmenu should be highlighted in 'LightMagenta' in terminal
826   " vim and as 'Magenta' in gvim...  And I can't ask it what color it actually
827   " *wants*.  As far as I can see, there's no way for me to learn that
828   " I should output 'Magenta' when 'LightMagenta' is provided by vim for the
829   " terminal.
830   if !has('gui_running')
831     echohl WarningMsg
832     echomsg "Warning: The written colorscheme may have incorrect colors"
833     echomsg "         when CSApproxSnapshot is used in terminal vim!"
834     echohl None
835   endif
837   let save_t_Co = &t_Co
838   let s:inhibit_hicolor_test = 1
839   if exists("g:CSApprox_konsole")
840     let save_CSApprox_konsole = g:CSApprox_konsole
841   endif
842   if exists("g:CSApprox_eterm")
843     let save_CSApprox_eterm = g:CSApprox_eterm
844   endif
846   " Needed just like in CSApprox()
847   if exists("g:colors_name")
848     let colors_name = g:colors_name
849     unlet g:colors_name
850   endif
852   " Needed just like in CSApprox()
853   if exists("g:syntax_cmd")
854     let syntax_cmd = g:syntax_cmd
855   endif
856   let g:syntax_cmd = "PLEASE DON'T CHANGE ANY COLORS!!!"
858   try
859     let lines = []
860     let lines += [ '" This scheme was created by CSApproxSnapshot' ]
861     let lines += [ '" on ' . strftime("%a, %d %b %Y") ]
862     let lines += [ '' ]
863     let lines += [ 'hi clear' ]
864     let lines += [ 'if exists("syntax_on")' ]
865     let lines += [ '    syntax reset' ]
866     let lines += [ 'endif' ]
867     let lines += [ '' ]
868     let lines += [ 'if v:version < 700' ]
869     let lines += [ '    let g:colors_name = expand("<sfile>:t:r")' ]
870     let lines += [ '    command! -nargs=+ CSAHi exe "hi" substitute(substitute(<q-args>, "undercurl", "underline", "g"), "guisp\\S\\+", "", "g")' ]
871     let lines += [ 'else' ]
872     let lines += [ '    let g:colors_name = expand("<sfile>:t:r")' ]
873     let lines += [ '    command! -nargs=+ CSAHi exe "hi" <q-args>' ]
874     let lines += [ 'endif' ]
875     let lines += [ '' ]
877     let lines += [ 'if 0' ]
878     for round in [ 'konsole', 'eterm', 'xterm', 'urxvt' ]
879       sil! unlet g:CSApprox_eterm
880       sil! unlet g:CSApprox_konsole
882       if round == 'konsole'
883         let g:CSApprox_konsole = 1
884       elseif round == 'eterm'
885         let g:CSApprox_eterm = 1
886       endif
888       if round == 'urxvt'
889         set t_Co=88
890       else
891         set t_Co=256
892       endif
894       call s:CSApprox()
896       let highlights = s:Highlights(["term", "cterm", "gui"])
897       call s:FixupGuiInfo(highlights)
899       if round == 'konsole' || round == 'eterm'
900         let lines += [ 'elseif has("gui_running") || (&t_Co == ' . &t_Co
901                    \ . ' && (&term ==# "xterm" || &term =~# "^screen")'
902                    \ . ' && exists("g:CSApprox_' . round . '")'
903                    \ . ' && g:CSApprox_' . round . ')'
904                    \ . ' || &term =~? "^' . round . '"' ]
905       else
906         let lines += [ 'elseif has("gui_running") || &t_Co == ' . &t_Co ]
907       endif
909       let hinums = keys(highlights)
911       call insert(hinums, remove(hinums, index(hinums, string(s:hlid_normal))))
913       for hlnum in hinums
914         let hl = highlights[hlnum]
915         let line = '    CSAHi ' . hl.name
916         for type in [ 'term', 'cterm', 'gui' ]
917           let attrs = [ 'reverse', 'bold', 'italic', 'underline', 'undercurl' ]
918           call filter(attrs, 'hl[type][v:val] == 1')
919           let line .= ' ' . type . '=' . (empty(attrs) ? 'NONE' : join(attrs, ','))
920           if type != 'term'
921             let line .= ' ' . type . 'bg=' . (len(hl[type].bg) ? hl[type].bg : 'bg')
922             let line .= ' ' . type . 'fg=' . (len(hl[type].fg) ? hl[type].fg : 'fg')
923             if type == 'gui' && hl.gui.sp !~ '^\s*$'
924               let line .= ' ' . type . 'sp=' . hl[type].sp
925             endif
926           endif
927         endfor
928         let lines += [ line ]
929       endfor
930     endfor
931     let lines += [ 'endif' ]
932     let lines += [ '' ]
933     let lines += [ 'if 1' ]
934     let lines += [ '    delcommand CSAHi' ]
935     let lines += [ 'endif' ]
936     call writefile(lines, file)
937   finally
938     let &t_Co = save_t_Co
940     if exists("save_CSApprox_konsole")
941       let g:CSApprox_konsole = save_CSApprox_konsole
942     endif
943     if exists("save_CSApprox_eterm")
944       let g:CSApprox_eterm = save_CSApprox_eterm
945     endif
947     if exists("colors_name")
948       let g:colors_name = colors_name
949     endif
951     unlet g:syntax_cmd
952     if exists("syntax_cmd")
953       let g:syntax_cmd = syntax_cmd
954     endif
956     call s:CSApprox()
958     unlet s:inhibit_hicolor_test
959   endtry
960 endfunction
962 " {>2} Snapshot user command
963 command! -bang -nargs=1 -complete=file -bar CSApproxSnapshot
964         \ call s:CSApproxSnapshot(<f-args>, strlen("<bang>"))
966 " {>1} Hooks
968 " {>2} Autocmds
969 " Set up an autogroup to hook us on the completion of any :colorscheme command
970 augroup CSApprox
971   au!
972   au ColorScheme * call s:CSApprox()
973   "au User CSApproxPost highlight Normal ctermbg=none | highlight NonText ctermbg=None
974 augroup END
976 " {>2} Execute
977 " The last thing to do when sourced is to run and actually fix up the colors.
978 if !has('gui_running')
979   call s:CSApprox()
980 endif
982 " {>1} Restore compatibility options
983 let &cpo = s:savecpo
984 unlet s:savecpo
987 " {0} vim:sw=2:sts=2:et:fdm=expr:fde=substitute(matchstr(getline(v\:lnum),'^\\s*"\\s*{\\zs.\\{-}\\ze}'),'^$','=','')