##// END OF EJS Templates
Add \S prompt char for whitespace as wide as the count (similar to \D)
Wouter Bolsterlee -
Show More
@@ -1,449 +1,450 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Classes for handling input/output prompts.
3 3
4 4 Authors:
5 5
6 6 * Fernando Perez
7 7 * Brian Granger
8 8 * Thomas Kluyver
9 9 """
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Copyright (C) 2008-2011 The IPython Development Team
13 13 # Copyright (C) 2001-2007 Fernando Perez <fperez@colorado.edu>
14 14 #
15 15 # Distributed under the terms of the BSD License. The full license is in
16 16 # the file COPYING, distributed as part of this software.
17 17 #-----------------------------------------------------------------------------
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Imports
21 21 #-----------------------------------------------------------------------------
22 22
23 23 import os
24 24 import re
25 25 import socket
26 26 import sys
27 27 import time
28 28
29 29 from string import Formatter
30 30
31 31 from traitlets.config.configurable import Configurable
32 32 from IPython.core import release
33 33 from IPython.utils import coloransi, py3compat
34 34 from traitlets import (Unicode, Instance, Dict, Bool, Int)
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Color schemes for prompts
38 38 #-----------------------------------------------------------------------------
39 39
40 40 InputColors = coloransi.InputTermColors # just a shorthand
41 41 Colors = coloransi.TermColors # just a shorthand
42 42
43 43 color_lists = dict(normal=Colors(), inp=InputColors(), nocolor=coloransi.NoColors())
44 44
45 45 PColNoColors = coloransi.ColorScheme(
46 46 'NoColor',
47 47 in_prompt = InputColors.NoColor, # Input prompt
48 48 in_number = InputColors.NoColor, # Input prompt number
49 49 in_prompt2 = InputColors.NoColor, # Continuation prompt
50 50 in_normal = InputColors.NoColor, # color off (usu. Colors.Normal)
51 51
52 52 out_prompt = Colors.NoColor, # Output prompt
53 53 out_number = Colors.NoColor, # Output prompt number
54 54
55 55 normal = Colors.NoColor # color off (usu. Colors.Normal)
56 56 )
57 57
58 58 # make some schemes as instances so we can copy them for modification easily:
59 59 PColLinux = coloransi.ColorScheme(
60 60 'Linux',
61 61 in_prompt = InputColors.Green,
62 62 in_number = InputColors.LightGreen,
63 63 in_prompt2 = InputColors.Green,
64 64 in_normal = InputColors.Normal, # color off (usu. Colors.Normal)
65 65
66 66 out_prompt = Colors.Red,
67 67 out_number = Colors.LightRed,
68 68
69 69 normal = Colors.Normal
70 70 )
71 71
72 72 # Slightly modified Linux for light backgrounds
73 73 PColLightBG = PColLinux.copy('LightBG')
74 74
75 75 PColLightBG.colors.update(
76 76 in_prompt = InputColors.Blue,
77 77 in_number = InputColors.LightBlue,
78 78 in_prompt2 = InputColors.Blue
79 79 )
80 80
81 81 #-----------------------------------------------------------------------------
82 82 # Utilities
83 83 #-----------------------------------------------------------------------------
84 84
85 85 class LazyEvaluate(object):
86 86 """This is used for formatting strings with values that need to be updated
87 87 at that time, such as the current time or working directory."""
88 88 def __init__(self, func, *args, **kwargs):
89 89 self.func = func
90 90 self.args = args
91 91 self.kwargs = kwargs
92 92
93 93 def __call__(self, **kwargs):
94 94 self.kwargs.update(kwargs)
95 95 return self.func(*self.args, **self.kwargs)
96 96
97 97 def __str__(self):
98 98 return str(self())
99 99
100 100 def __unicode__(self):
101 101 return py3compat.unicode_type(self())
102 102
103 103 def __format__(self, format_spec):
104 104 return format(self(), format_spec)
105 105
106 106 def multiple_replace(dict, text):
107 107 """ Replace in 'text' all occurences of any key in the given
108 108 dictionary by its corresponding value. Returns the new string."""
109 109
110 110 # Function by Xavier Defrang, originally found at:
111 111 # http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81330
112 112
113 113 # Create a regular expression from the dictionary keys
114 114 regex = re.compile("(%s)" % "|".join(map(re.escape, dict.keys())))
115 115 # For each match, look-up corresponding value in dictionary
116 116 return regex.sub(lambda mo: dict[mo.string[mo.start():mo.end()]], text)
117 117
118 118 #-----------------------------------------------------------------------------
119 119 # Special characters that can be used in prompt templates, mainly bash-like
120 120 #-----------------------------------------------------------------------------
121 121
122 122 # If $HOME isn't defined (Windows), make it an absurd string so that it can
123 123 # never be expanded out into '~'. Basically anything which can never be a
124 124 # reasonable directory name will do, we just want the $HOME -> '~' operation
125 125 # to become a no-op. We pre-compute $HOME here so it's not done on every
126 126 # prompt call.
127 127
128 128 # FIXME:
129 129
130 130 # - This should be turned into a class which does proper namespace management,
131 131 # since the prompt specials need to be evaluated in a certain namespace.
132 132 # Currently it's just globals, which need to be managed manually by code
133 133 # below.
134 134
135 135 # - I also need to split up the color schemes from the prompt specials
136 136 # somehow. I don't have a clean design for that quite yet.
137 137
138 138 HOME = py3compat.str_to_unicode(os.environ.get("HOME","//////:::::ZZZZZ,,,~~~"))
139 139
140 140 # This is needed on FreeBSD, and maybe other systems which symlink /home to
141 141 # /usr/home, but retain the $HOME variable as pointing to /home
142 142 HOME = os.path.realpath(HOME)
143 143
144 144 # We precompute a few more strings here for the prompt_specials, which are
145 145 # fixed once ipython starts. This reduces the runtime overhead of computing
146 146 # prompt strings.
147 147 USER = py3compat.str_to_unicode(os.environ.get("USER",''))
148 148 HOSTNAME = py3compat.str_to_unicode(socket.gethostname())
149 149 HOSTNAME_SHORT = HOSTNAME.split(".")[0]
150 150
151 151 # IronPython doesn't currently have os.getuid() even if
152 152 # os.name == 'posix'; 2/8/2014
153 153 ROOT_SYMBOL = "#" if (os.name=='nt' or sys.platform=='cli' or os.getuid()==0) else "$"
154 154
155 155 prompt_abbreviations = {
156 156 # Prompt/history count
157 157 '%n' : '{color.number}' '{count}' '{color.prompt}',
158 158 r'\#': '{color.number}' '{count}' '{color.prompt}',
159 159 # Just the prompt counter number, WITHOUT any coloring wrappers, so users
160 160 # can get numbers displayed in whatever color they want.
161 161 r'\N': '{count}',
162 162
163 # Prompt/history count, with the actual digits replaced by dots. Used
164 # mainly in continuation prompts (prompt_in2)
163 # Prompt/history count, with the actual digits replaced by dots or
164 # spaces. Used mainly in continuation prompts (prompt_in2).
165 165 r'\D': '{dots}',
166 r'\S': '{spaces}',
166 167
167 168 # Current time
168 169 r'\T' : '{time}',
169 170 # Current working directory
170 171 r'\w': '{cwd}',
171 172 # Basename of current working directory.
172 173 # (use os.sep to make this portable across OSes)
173 174 r'\W' : '{cwd_last}',
174 175 # These X<N> are an extension to the normal bash prompts. They return
175 176 # N terms of the path, after replacing $HOME with '~'
176 177 r'\X0': '{cwd_x[0]}',
177 178 r'\X1': '{cwd_x[1]}',
178 179 r'\X2': '{cwd_x[2]}',
179 180 r'\X3': '{cwd_x[3]}',
180 181 r'\X4': '{cwd_x[4]}',
181 182 r'\X5': '{cwd_x[5]}',
182 183 # Y<N> are similar to X<N>, but they show '~' if it's the directory
183 184 # N+1 in the list. Somewhat like %cN in tcsh.
184 185 r'\Y0': '{cwd_y[0]}',
185 186 r'\Y1': '{cwd_y[1]}',
186 187 r'\Y2': '{cwd_y[2]}',
187 188 r'\Y3': '{cwd_y[3]}',
188 189 r'\Y4': '{cwd_y[4]}',
189 190 r'\Y5': '{cwd_y[5]}',
190 191 # Hostname up to first .
191 192 r'\h': HOSTNAME_SHORT,
192 193 # Full hostname
193 194 r'\H': HOSTNAME,
194 195 # Username of current user
195 196 r'\u': USER,
196 197 # Escaped '\'
197 198 '\\\\': '\\',
198 199 # Newline
199 200 r'\n': '\n',
200 201 # Carriage return
201 202 r'\r': '\r',
202 203 # Release version
203 204 r'\v': release.version,
204 205 # Root symbol ($ or #)
205 206 r'\$': ROOT_SYMBOL,
206 207 }
207 208
208 209 #-----------------------------------------------------------------------------
209 210 # More utilities
210 211 #-----------------------------------------------------------------------------
211 212
212 213 def cwd_filt(depth):
213 214 """Return the last depth elements of the current working directory.
214 215
215 216 $HOME is always replaced with '~'.
216 217 If depth==0, the full path is returned."""
217 218
218 219 cwd = py3compat.getcwd().replace(HOME,"~")
219 220 out = os.sep.join(cwd.split(os.sep)[-depth:])
220 221 return out or os.sep
221 222
222 223 def cwd_filt2(depth):
223 224 """Return the last depth elements of the current working directory.
224 225
225 226 $HOME is always replaced with '~'.
226 227 If depth==0, the full path is returned."""
227 228
228 229 full_cwd = py3compat.getcwd()
229 230 cwd = full_cwd.replace(HOME,"~").split(os.sep)
230 231 if '~' in cwd and len(cwd) == depth+1:
231 232 depth += 1
232 233 drivepart = ''
233 234 if sys.platform == 'win32' and len(cwd) > depth:
234 235 drivepart = os.path.splitdrive(full_cwd)[0]
235 236 out = drivepart + '/'.join(cwd[-depth:])
236 237
237 238 return out or os.sep
238 239
239 240 #-----------------------------------------------------------------------------
240 241 # Prompt classes
241 242 #-----------------------------------------------------------------------------
242 243
243 244 lazily_evaluate = {'time': LazyEvaluate(time.strftime, "%H:%M:%S"),
244 245 'cwd': LazyEvaluate(py3compat.getcwd),
245 246 'cwd_last': LazyEvaluate(lambda: py3compat.getcwd().split(os.sep)[-1]),
246 247 'cwd_x': [LazyEvaluate(lambda: py3compat.getcwd().replace(HOME,"~"))] +\
247 248 [LazyEvaluate(cwd_filt, x) for x in range(1,6)],
248 249 'cwd_y': [LazyEvaluate(cwd_filt2, x) for x in range(6)]
249 250 }
250 251
251 252 def _lenlastline(s):
252 253 """Get the length of the last line. More intelligent than
253 254 len(s.splitlines()[-1]).
254 255 """
255 256 if not s or s.endswith(('\n', '\r')):
256 257 return 0
257 258 return len(s.splitlines()[-1])
258 259
259 260
260 261 invisible_chars_re = re.compile('\001[^\001\002]*\002')
261 262 def _invisible_characters(s):
262 263 """
263 264 Get the number of invisible ANSI characters in s. Invisible characters
264 265 must be delimited by \001 and \002.
265 266 """
266 267 return _lenlastline(s) - _lenlastline(invisible_chars_re.sub('', s))
267 268
268 269 class UserNSFormatter(Formatter):
269 270 """A Formatter that falls back on a shell's user_ns and __builtins__ for name resolution"""
270 271 def __init__(self, shell):
271 272 self.shell = shell
272 273
273 274 def get_value(self, key, args, kwargs):
274 275 # try regular formatting first:
275 276 try:
276 277 return Formatter.get_value(self, key, args, kwargs)
277 278 except Exception:
278 279 pass
279 280 # next, look in user_ns and builtins:
280 281 for container in (self.shell.user_ns, __builtins__):
281 282 if key in container:
282 283 return container[key]
283 284 # nothing found, put error message in its place
284 285 return "<ERROR: '%s' not found>" % key
285 286
286 287
287 288 class PromptManager(Configurable):
288 289 """This is the primary interface for producing IPython's prompts."""
289 290 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC', allow_none=True)
290 291
291 292 color_scheme_table = Instance(coloransi.ColorSchemeTable, allow_none=True)
292 293 color_scheme = Unicode('Linux', config=True)
293 294 def _color_scheme_changed(self, name, new_value):
294 295 self.color_scheme_table.set_active_scheme(new_value)
295 296 for pname in ['in', 'in2', 'out', 'rewrite']:
296 297 # We need to recalculate the number of invisible characters
297 298 self.update_prompt(pname)
298 299
299 300 lazy_evaluate_fields = Dict(help="""
300 301 This maps field names used in the prompt templates to functions which
301 302 will be called when the prompt is rendered. This allows us to include
302 303 things like the current time in the prompts. Functions are only called
303 304 if they are used in the prompt.
304 305 """)
305 306 def _lazy_evaluate_fields_default(self): return lazily_evaluate.copy()
306 307
307 308 in_template = Unicode('In [\\#]: ', config=True,
308 309 help="Input prompt. '\\#' will be transformed to the prompt number")
309 310 in2_template = Unicode(' .\\D.: ', config=True,
310 311 help="Continuation prompt.")
311 312 out_template = Unicode('Out[\\#]: ', config=True,
312 313 help="Output prompt. '\\#' will be transformed to the prompt number")
313 314
314 315 justify = Bool(True, config=True, help="""
315 316 If True (default), each prompt will be right-aligned with the
316 317 preceding one.
317 318 """)
318 319
319 320 # We actually store the expanded templates here:
320 321 templates = Dict()
321 322
322 323 # The number of characters in the last prompt rendered, not including
323 324 # colour characters.
324 325 width = Int()
325 326 txtwidth = Int() # Not including right-justification
326 327
327 328 # The number of characters in each prompt which don't contribute to width
328 329 invisible_chars = Dict()
329 330 def _invisible_chars_default(self):
330 331 return {'in': 0, 'in2': 0, 'out': 0, 'rewrite':0}
331 332
332 333 def __init__(self, shell, **kwargs):
333 334 super(PromptManager, self).__init__(shell=shell, **kwargs)
334 335
335 336 # Prepare colour scheme table
336 337 self.color_scheme_table = coloransi.ColorSchemeTable([PColNoColors,
337 338 PColLinux, PColLightBG], self.color_scheme)
338 339
339 340 self._formatter = UserNSFormatter(shell)
340 341 # Prepare templates & numbers of invisible characters
341 342 self.update_prompt('in', self.in_template)
342 343 self.update_prompt('in2', self.in2_template)
343 344 self.update_prompt('out', self.out_template)
344 345 self.update_prompt('rewrite')
345 346 self.on_trait_change(self._update_prompt_trait, ['in_template',
346 347 'in2_template', 'out_template'])
347 348
348 349 def update_prompt(self, name, new_template=None):
349 350 """This is called when a prompt template is updated. It processes
350 351 abbreviations used in the prompt template (like \#) and calculates how
351 352 many invisible characters (ANSI colour escapes) the resulting prompt
352 353 contains.
353 354
354 355 It is also called for each prompt on changing the colour scheme. In both
355 356 cases, traitlets should take care of calling this automatically.
356 357 """
357 358 if new_template is not None:
358 359 self.templates[name] = multiple_replace(prompt_abbreviations, new_template)
359 360 # We count invisible characters (colour escapes) on the last line of the
360 361 # prompt, to calculate the width for lining up subsequent prompts.
361 362 invis_chars = _invisible_characters(self._render(name, color=True))
362 363 self.invisible_chars[name] = invis_chars
363 364
364 365 def _update_prompt_trait(self, traitname, new_template):
365 366 name = traitname[:-9] # Cut off '_template'
366 367 self.update_prompt(name, new_template)
367 368
368 369 def _render(self, name, color=True, **kwargs):
369 370 """Render but don't justify, or update the width or txtwidth attributes.
370 371 """
371 372 if name == 'rewrite':
372 373 return self._render_rewrite(color=color)
373 374
374 375 if color:
375 376 scheme = self.color_scheme_table.active_colors
376 377 if name=='out':
377 378 colors = color_lists['normal']
378 379 colors.number, colors.prompt, colors.normal = \
379 380 scheme.out_number, scheme.out_prompt, scheme.normal
380 381 else:
381 382 colors = color_lists['inp']
382 383 colors.number, colors.prompt, colors.normal = \
383 384 scheme.in_number, scheme.in_prompt, scheme.in_normal
384 385 if name=='in2':
385 386 colors.prompt = scheme.in_prompt2
386 387 else:
387 388 # No color
388 389 colors = color_lists['nocolor']
389 390 colors.number, colors.prompt, colors.normal = '', '', ''
390 391
391 392 count = self.shell.execution_count # Shorthand
392 393 # Build the dictionary to be passed to string formatting
393 394 fmtargs = dict(color=colors, count=count,
394 dots="."*len(str(count)),
395 width=self.width, txtwidth=self.txtwidth )
395 dots="."*len(str(count)), spaces=" "*len(str(count)),
396 width=self.width, txtwidth=self.txtwidth)
396 397 fmtargs.update(self.lazy_evaluate_fields)
397 398 fmtargs.update(kwargs)
398 399
399 400 # Prepare the prompt
400 401 prompt = colors.prompt + self.templates[name] + colors.normal
401 402
402 403 # Fill in required fields
403 404 return self._formatter.format(prompt, **fmtargs)
404 405
405 406 def _render_rewrite(self, color=True):
406 407 """Render the ---> rewrite prompt."""
407 408 if color:
408 409 scheme = self.color_scheme_table.active_colors
409 410 # We need a non-input version of these escapes
410 411 color_prompt = scheme.in_prompt.replace("\001","").replace("\002","")
411 412 color_normal = scheme.normal
412 413 else:
413 414 color_prompt, color_normal = '', ''
414 415
415 416 return color_prompt + "-> ".rjust(self.txtwidth, "-") + color_normal
416 417
417 418 def render(self, name, color=True, just=None, **kwargs):
418 419 """
419 420 Render the selected prompt.
420 421
421 422 Parameters
422 423 ----------
423 424 name : str
424 425 Which prompt to render. One of 'in', 'in2', 'out', 'rewrite'
425 426 color : bool
426 427 If True (default), include ANSI escape sequences for a coloured prompt.
427 428 just : bool
428 429 If True, justify the prompt to the width of the last prompt. The
429 430 default is stored in self.justify.
430 431 **kwargs :
431 432 Additional arguments will be passed to the string formatting operation,
432 433 so they can override the values that would otherwise fill in the
433 434 template.
434 435
435 436 Returns
436 437 -------
437 438 A string containing the rendered prompt.
438 439 """
439 440 res = self._render(name, color=color, **kwargs)
440 441
441 442 # Handle justification of prompt
442 443 invis_chars = self.invisible_chars[name] if color else 0
443 444 self.txtwidth = _lenlastline(res) - invis_chars
444 445 just = self.justify if (just is None) else just
445 446 # If the prompt spans more than one line, don't try to justify it:
446 447 if just and name != 'in' and ('\n' not in res) and ('\r' not in res):
447 448 res = res.rjust(self.width + invis_chars)
448 449 self.width = _lenlastline(res) - invis_chars
449 450 return res
General Comments 0
You need to be logged in to leave comments. Login now