PageRenderTime 47ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/hgext/color.py

https://bitbucket.org/mirror/mercurial/
Python | 584 lines | 544 code | 7 blank | 33 comment | 12 complexity | d8d783ac9d4d98405f2742a43c018928 MD5 | raw file
Possible License(s): GPL-2.0
  1. # color.py color output for the status and qseries commands
  2. #
  3. # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
  4. #
  5. # This software may be used and distributed according to the terms of the
  6. # GNU General Public License version 2 or any later version.
  7. '''colorize output from some commands
  8. This extension modifies the status and resolve commands to add color
  9. to their output to reflect file status, the qseries command to add
  10. color to reflect patch status (applied, unapplied, missing), and to
  11. diff-related commands to highlight additions, removals, diff headers,
  12. and trailing whitespace.
  13. Other effects in addition to color, like bold and underlined text, are
  14. also available. By default, the terminfo database is used to find the
  15. terminal codes used to change color and effect. If terminfo is not
  16. available, then effects are rendered with the ECMA-48 SGR control
  17. function (aka ANSI escape codes).
  18. Default effects may be overridden from your configuration file::
  19. [color]
  20. status.modified = blue bold underline red_background
  21. status.added = green bold
  22. status.removed = red bold blue_background
  23. status.deleted = cyan bold underline
  24. status.unknown = magenta bold underline
  25. status.ignored = black bold
  26. # 'none' turns off all effects
  27. status.clean = none
  28. status.copied = none
  29. qseries.applied = blue bold underline
  30. qseries.unapplied = black bold
  31. qseries.missing = red bold
  32. diff.diffline = bold
  33. diff.extended = cyan bold
  34. diff.file_a = red bold
  35. diff.file_b = green bold
  36. diff.hunk = magenta
  37. diff.deleted = red
  38. diff.inserted = green
  39. diff.changed = white
  40. diff.trailingwhitespace = bold red_background
  41. resolve.unresolved = red bold
  42. resolve.resolved = green bold
  43. bookmarks.current = green
  44. branches.active = none
  45. branches.closed = black bold
  46. branches.current = green
  47. branches.inactive = none
  48. tags.normal = green
  49. tags.local = black bold
  50. rebase.rebased = blue
  51. rebase.remaining = red bold
  52. shelve.age = cyan
  53. shelve.newest = green bold
  54. shelve.name = blue bold
  55. histedit.remaining = red bold
  56. The available effects in terminfo mode are 'blink', 'bold', 'dim',
  57. 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
  58. ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
  59. 'underline'. How each is rendered depends on the terminal emulator.
  60. Some may not be available for a given terminal type, and will be
  61. silently ignored.
  62. Note that on some systems, terminfo mode may cause problems when using
  63. color with the pager extension and less -R. less with the -R option
  64. will only display ECMA-48 color codes, and terminfo mode may sometimes
  65. emit codes that less doesn't understand. You can work around this by
  66. either using ansi mode (or auto mode), or by using less -r (which will
  67. pass through all terminal control codes, not just color control
  68. codes).
  69. Because there are only eight standard colors, this module allows you
  70. to define color names for other color slots which might be available
  71. for your terminal type, assuming terminfo mode. For instance::
  72. color.brightblue = 12
  73. color.pink = 207
  74. color.orange = 202
  75. to set 'brightblue' to color slot 12 (useful for 16 color terminals
  76. that have brighter colors defined in the upper eight) and, 'pink' and
  77. 'orange' to colors in 256-color xterm's default color cube. These
  78. defined colors may then be used as any of the pre-defined eight,
  79. including appending '_background' to set the background to that color.
  80. By default, the color extension will use ANSI mode (or win32 mode on
  81. Windows) if it detects a terminal. To override auto mode (to enable
  82. terminfo mode, for example), set the following configuration option::
  83. [color]
  84. mode = terminfo
  85. Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
  86. disable color.
  87. '''
  88. import os
  89. from mercurial import cmdutil, commands, dispatch, extensions, ui as uimod, util
  90. from mercurial import templater, error
  91. from mercurial.i18n import _
  92. cmdtable = {}
  93. command = cmdutil.command(cmdtable)
  94. testedwith = 'internal'
  95. # start and stop parameters for effects
  96. _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
  97. 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
  98. 'italic': 3, 'underline': 4, 'inverse': 7,
  99. 'black_background': 40, 'red_background': 41,
  100. 'green_background': 42, 'yellow_background': 43,
  101. 'blue_background': 44, 'purple_background': 45,
  102. 'cyan_background': 46, 'white_background': 47}
  103. def _terminfosetup(ui, mode):
  104. '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
  105. global _terminfo_params
  106. # If we failed to load curses, we go ahead and return.
  107. if not _terminfo_params:
  108. return
  109. # Otherwise, see what the config file says.
  110. if mode not in ('auto', 'terminfo'):
  111. return
  112. _terminfo_params.update((key[6:], (False, int(val)))
  113. for key, val in ui.configitems('color')
  114. if key.startswith('color.'))
  115. try:
  116. curses.setupterm()
  117. except curses.error, e:
  118. _terminfo_params = {}
  119. return
  120. for key, (b, e) in _terminfo_params.items():
  121. if not b:
  122. continue
  123. if not curses.tigetstr(e):
  124. # Most terminals don't support dim, invis, etc, so don't be
  125. # noisy and use ui.debug().
  126. ui.debug("no terminfo entry for %s\n" % e)
  127. del _terminfo_params[key]
  128. if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
  129. # Only warn about missing terminfo entries if we explicitly asked for
  130. # terminfo mode.
  131. if mode == "terminfo":
  132. ui.warn(_("no terminfo entry for setab/setaf: reverting to "
  133. "ECMA-48 color\n"))
  134. _terminfo_params = {}
  135. def _modesetup(ui, coloropt):
  136. global _terminfo_params
  137. auto = (coloropt == 'auto')
  138. always = not auto and util.parsebool(coloropt)
  139. if not always and not auto:
  140. return None
  141. formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
  142. mode = ui.config('color', 'mode', 'auto')
  143. realmode = mode
  144. if mode == 'auto':
  145. if os.name == 'nt' and 'TERM' not in os.environ:
  146. # looks line a cmd.exe console, use win32 API or nothing
  147. realmode = 'win32'
  148. else:
  149. realmode = 'ansi'
  150. if realmode == 'win32':
  151. _terminfo_params = {}
  152. if not w32effects:
  153. if mode == 'win32':
  154. # only warn if color.mode is explicitly set to win32
  155. ui.warn(_('warning: failed to set color mode to %s\n') % mode)
  156. return None
  157. _effects.update(w32effects)
  158. elif realmode == 'ansi':
  159. _terminfo_params = {}
  160. elif realmode == 'terminfo':
  161. _terminfosetup(ui, mode)
  162. if not _terminfo_params:
  163. if mode == 'terminfo':
  164. ## FIXME Shouldn't we return None in this case too?
  165. # only warn if color.mode is explicitly set to win32
  166. ui.warn(_('warning: failed to set color mode to %s\n') % mode)
  167. realmode = 'ansi'
  168. else:
  169. return None
  170. if always or (auto and formatted):
  171. return realmode
  172. return None
  173. try:
  174. import curses
  175. # Mapping from effect name to terminfo attribute name or color number.
  176. # This will also force-load the curses module.
  177. _terminfo_params = {'none': (True, 'sgr0'),
  178. 'standout': (True, 'smso'),
  179. 'underline': (True, 'smul'),
  180. 'reverse': (True, 'rev'),
  181. 'inverse': (True, 'rev'),
  182. 'blink': (True, 'blink'),
  183. 'dim': (True, 'dim'),
  184. 'bold': (True, 'bold'),
  185. 'invisible': (True, 'invis'),
  186. 'italic': (True, 'sitm'),
  187. 'black': (False, curses.COLOR_BLACK),
  188. 'red': (False, curses.COLOR_RED),
  189. 'green': (False, curses.COLOR_GREEN),
  190. 'yellow': (False, curses.COLOR_YELLOW),
  191. 'blue': (False, curses.COLOR_BLUE),
  192. 'magenta': (False, curses.COLOR_MAGENTA),
  193. 'cyan': (False, curses.COLOR_CYAN),
  194. 'white': (False, curses.COLOR_WHITE)}
  195. except ImportError:
  196. _terminfo_params = {}
  197. _styles = {'grep.match': 'red bold',
  198. 'grep.linenumber': 'green',
  199. 'grep.rev': 'green',
  200. 'grep.change': 'green',
  201. 'grep.sep': 'cyan',
  202. 'grep.filename': 'magenta',
  203. 'grep.user': 'magenta',
  204. 'grep.date': 'magenta',
  205. 'bookmarks.current': 'green',
  206. 'branches.active': 'none',
  207. 'branches.closed': 'black bold',
  208. 'branches.current': 'green',
  209. 'branches.inactive': 'none',
  210. 'diff.changed': 'white',
  211. 'diff.deleted': 'red',
  212. 'diff.diffline': 'bold',
  213. 'diff.extended': 'cyan bold',
  214. 'diff.file_a': 'red bold',
  215. 'diff.file_b': 'green bold',
  216. 'diff.hunk': 'magenta',
  217. 'diff.inserted': 'green',
  218. 'diff.trailingwhitespace': 'bold red_background',
  219. 'diffstat.deleted': 'red',
  220. 'diffstat.inserted': 'green',
  221. 'histedit.remaining': 'red bold',
  222. 'ui.prompt': 'yellow',
  223. 'log.changeset': 'yellow',
  224. 'rebase.rebased': 'blue',
  225. 'rebase.remaining': 'red bold',
  226. 'resolve.resolved': 'green bold',
  227. 'resolve.unresolved': 'red bold',
  228. 'shelve.age': 'cyan',
  229. 'shelve.newest': 'green bold',
  230. 'shelve.name': 'blue bold',
  231. 'status.added': 'green bold',
  232. 'status.clean': 'none',
  233. 'status.copied': 'none',
  234. 'status.deleted': 'cyan bold underline',
  235. 'status.ignored': 'black bold',
  236. 'status.modified': 'blue bold',
  237. 'status.removed': 'red bold',
  238. 'status.unknown': 'magenta bold underline',
  239. 'tags.normal': 'green',
  240. 'tags.local': 'black bold'}
  241. def _effect_str(effect):
  242. '''Helper function for render_effects().'''
  243. bg = False
  244. if effect.endswith('_background'):
  245. bg = True
  246. effect = effect[:-11]
  247. attr, val = _terminfo_params[effect]
  248. if attr:
  249. return curses.tigetstr(val)
  250. elif bg:
  251. return curses.tparm(curses.tigetstr('setab'), val)
  252. else:
  253. return curses.tparm(curses.tigetstr('setaf'), val)
  254. def render_effects(text, effects):
  255. 'Wrap text in commands to turn on each effect.'
  256. if not text:
  257. return text
  258. if not _terminfo_params:
  259. start = [str(_effects[e]) for e in ['none'] + effects.split()]
  260. start = '\033[' + ';'.join(start) + 'm'
  261. stop = '\033[' + str(_effects['none']) + 'm'
  262. else:
  263. start = ''.join(_effect_str(effect)
  264. for effect in ['none'] + effects.split())
  265. stop = _effect_str('none')
  266. return ''.join([start, text, stop])
  267. def extstyles():
  268. for name, ext in extensions.extensions():
  269. _styles.update(getattr(ext, 'colortable', {}))
  270. def valideffect(effect):
  271. 'Determine if the effect is valid or not.'
  272. good = False
  273. if not _terminfo_params and effect in _effects:
  274. good = True
  275. elif effect in _terminfo_params or effect[:-11] in _terminfo_params:
  276. good = True
  277. return good
  278. def configstyles(ui):
  279. for status, cfgeffects in ui.configitems('color'):
  280. if '.' not in status or status.startswith('color.'):
  281. continue
  282. cfgeffects = ui.configlist('color', status)
  283. if cfgeffects:
  284. good = []
  285. for e in cfgeffects:
  286. if valideffect(e):
  287. good.append(e)
  288. else:
  289. ui.warn(_("ignoring unknown color/effect %r "
  290. "(configured in color.%s)\n")
  291. % (e, status))
  292. _styles[status] = ' '.join(good)
  293. class colorui(uimod.ui):
  294. def popbuffer(self, labeled=False):
  295. if self._colormode is None:
  296. return super(colorui, self).popbuffer(labeled)
  297. self._bufferstates.pop()
  298. if labeled:
  299. return ''.join(self.label(a, label) for a, label
  300. in self._buffers.pop())
  301. return ''.join(a for a, label in self._buffers.pop())
  302. _colormode = 'ansi'
  303. def write(self, *args, **opts):
  304. if self._colormode is None:
  305. return super(colorui, self).write(*args, **opts)
  306. label = opts.get('label', '')
  307. if self._buffers:
  308. self._buffers[-1].extend([(str(a), label) for a in args])
  309. elif self._colormode == 'win32':
  310. for a in args:
  311. win32print(a, super(colorui, self).write, **opts)
  312. else:
  313. return super(colorui, self).write(
  314. *[self.label(str(a), label) for a in args], **opts)
  315. def write_err(self, *args, **opts):
  316. if self._colormode is None:
  317. return super(colorui, self).write_err(*args, **opts)
  318. label = opts.get('label', '')
  319. if self._bufferstates and self._bufferstates[-1]:
  320. return self.write(*args, **opts)
  321. if self._colormode == 'win32':
  322. for a in args:
  323. win32print(a, super(colorui, self).write_err, **opts)
  324. else:
  325. return super(colorui, self).write_err(
  326. *[self.label(str(a), label) for a in args], **opts)
  327. def label(self, msg, label):
  328. if self._colormode is None:
  329. return super(colorui, self).label(msg, label)
  330. effects = []
  331. for l in label.split():
  332. s = _styles.get(l, '')
  333. if s:
  334. effects.append(s)
  335. elif valideffect(l):
  336. effects.append(l)
  337. effects = ' '.join(effects)
  338. if effects:
  339. return '\n'.join([render_effects(s, effects)
  340. for s in msg.split('\n')])
  341. return msg
  342. def templatelabel(context, mapping, args):
  343. if len(args) != 2:
  344. # i18n: "label" is a keyword
  345. raise error.ParseError(_("label expects two arguments"))
  346. # add known effects to the mapping so symbols like 'red', 'bold',
  347. # etc. don't need to be quoted
  348. mapping.update(dict([(k, k) for k in _effects]))
  349. thing = templater._evalifliteral(args[1], context, mapping)
  350. # apparently, repo could be a string that is the favicon?
  351. repo = mapping.get('repo', '')
  352. if isinstance(repo, str):
  353. return thing
  354. label = templater._evalifliteral(args[0], context, mapping)
  355. thing = templater.stringify(thing)
  356. label = templater.stringify(label)
  357. return repo.ui.label(thing, label)
  358. def uisetup(ui):
  359. if ui.plain():
  360. return
  361. if not isinstance(ui, colorui):
  362. colorui.__bases__ = (ui.__class__,)
  363. ui.__class__ = colorui
  364. def colorcmd(orig, ui_, opts, cmd, cmdfunc):
  365. mode = _modesetup(ui_, opts['color'])
  366. colorui._colormode = mode
  367. if mode:
  368. extstyles()
  369. configstyles(ui_)
  370. return orig(ui_, opts, cmd, cmdfunc)
  371. extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
  372. templater.funcs['label'] = templatelabel
  373. def extsetup(ui):
  374. commands.globalopts.append(
  375. ('', 'color', 'auto',
  376. # i18n: 'always', 'auto', and 'never' are keywords and should
  377. # not be translated
  378. _("when to colorize (boolean, always, auto, or never)"),
  379. _('TYPE')))
  380. @command('debugcolor', [], 'hg debugcolor')
  381. def debugcolor(ui, repo, **opts):
  382. global _styles
  383. _styles = {}
  384. for effect in _effects.keys():
  385. _styles[effect] = effect
  386. ui.write(('color mode: %s\n') % ui._colormode)
  387. ui.write(_('available colors:\n'))
  388. for label, colors in _styles.items():
  389. ui.write(('%s\n') % colors, label=label)
  390. if os.name != 'nt':
  391. w32effects = None
  392. else:
  393. import re, ctypes
  394. _kernel32 = ctypes.windll.kernel32
  395. _WORD = ctypes.c_ushort
  396. _INVALID_HANDLE_VALUE = -1
  397. class _COORD(ctypes.Structure):
  398. _fields_ = [('X', ctypes.c_short),
  399. ('Y', ctypes.c_short)]
  400. class _SMALL_RECT(ctypes.Structure):
  401. _fields_ = [('Left', ctypes.c_short),
  402. ('Top', ctypes.c_short),
  403. ('Right', ctypes.c_short),
  404. ('Bottom', ctypes.c_short)]
  405. class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
  406. _fields_ = [('dwSize', _COORD),
  407. ('dwCursorPosition', _COORD),
  408. ('wAttributes', _WORD),
  409. ('srWindow', _SMALL_RECT),
  410. ('dwMaximumWindowSize', _COORD)]
  411. _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
  412. _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
  413. _FOREGROUND_BLUE = 0x0001
  414. _FOREGROUND_GREEN = 0x0002
  415. _FOREGROUND_RED = 0x0004
  416. _FOREGROUND_INTENSITY = 0x0008
  417. _BACKGROUND_BLUE = 0x0010
  418. _BACKGROUND_GREEN = 0x0020
  419. _BACKGROUND_RED = 0x0040
  420. _BACKGROUND_INTENSITY = 0x0080
  421. _COMMON_LVB_REVERSE_VIDEO = 0x4000
  422. _COMMON_LVB_UNDERSCORE = 0x8000
  423. # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
  424. w32effects = {
  425. 'none': -1,
  426. 'black': 0,
  427. 'red': _FOREGROUND_RED,
  428. 'green': _FOREGROUND_GREEN,
  429. 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
  430. 'blue': _FOREGROUND_BLUE,
  431. 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
  432. 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
  433. 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
  434. 'bold': _FOREGROUND_INTENSITY,
  435. 'black_background': 0x100, # unused value > 0x0f
  436. 'red_background': _BACKGROUND_RED,
  437. 'green_background': _BACKGROUND_GREEN,
  438. 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
  439. 'blue_background': _BACKGROUND_BLUE,
  440. 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
  441. 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
  442. 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
  443. _BACKGROUND_BLUE),
  444. 'bold_background': _BACKGROUND_INTENSITY,
  445. 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
  446. 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
  447. }
  448. passthrough = set([_FOREGROUND_INTENSITY,
  449. _BACKGROUND_INTENSITY,
  450. _COMMON_LVB_UNDERSCORE,
  451. _COMMON_LVB_REVERSE_VIDEO])
  452. stdout = _kernel32.GetStdHandle(
  453. _STD_OUTPUT_HANDLE) # don't close the handle returned
  454. if stdout is None or stdout == _INVALID_HANDLE_VALUE:
  455. w32effects = None
  456. else:
  457. csbi = _CONSOLE_SCREEN_BUFFER_INFO()
  458. if not _kernel32.GetConsoleScreenBufferInfo(
  459. stdout, ctypes.byref(csbi)):
  460. # stdout may not support GetConsoleScreenBufferInfo()
  461. # when called from subprocess or redirected
  462. w32effects = None
  463. else:
  464. origattr = csbi.wAttributes
  465. ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
  466. re.MULTILINE | re.DOTALL)
  467. def win32print(text, orig, **opts):
  468. label = opts.get('label', '')
  469. attr = origattr
  470. def mapcolor(val, attr):
  471. if val == -1:
  472. return origattr
  473. elif val in passthrough:
  474. return attr | val
  475. elif val > 0x0f:
  476. return (val & 0x70) | (attr & 0x8f)
  477. else:
  478. return (val & 0x07) | (attr & 0xf8)
  479. # determine console attributes based on labels
  480. for l in label.split():
  481. style = _styles.get(l, '')
  482. for effect in style.split():
  483. attr = mapcolor(w32effects[effect], attr)
  484. # hack to ensure regexp finds data
  485. if not text.startswith('\033['):
  486. text = '\033[m' + text
  487. # Look for ANSI-like codes embedded in text
  488. m = re.match(ansire, text)
  489. try:
  490. while m:
  491. for sattr in m.group(1).split(';'):
  492. if sattr:
  493. attr = mapcolor(int(sattr), attr)
  494. _kernel32.SetConsoleTextAttribute(stdout, attr)
  495. orig(m.group(2), **opts)
  496. m = re.match(ansire, m.group(3))
  497. finally:
  498. # Explicitly reset original attributes
  499. _kernel32.SetConsoleTextAttribute(stdout, origattr)