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