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