##// END OF EJS Templates
added --colors flag to ipythonqt
MinRK -
Show More
@@ -1,456 +1,460 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3
4 4 TODO: Add support for retrieving the system default editor. Requires code
5 5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
6 6 Linux (use the xdg system).
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Imports
11 11 #-----------------------------------------------------------------------------
12 12
13 13 # Standard library imports
14 14 from collections import namedtuple
15 15 import re
16 16 from subprocess import Popen
17 17 from textwrap import dedent
18 18
19 19 # System library imports
20 20 from PyQt4 import QtCore, QtGui
21 21
22 22 # Local imports
23 23 from IPython.core.inputsplitter import IPythonInputSplitter, \
24 24 transform_ipy_prompt
25 25 from IPython.core.usage import default_gui_banner
26 26 from IPython.utils.traitlets import Bool, Str
27 27 from frontend_widget import FrontendWidget
28 from styles import (default_light_style_sheet, default_dark_style_sheet,
29 default_light_syntax_style, default_dark_syntax_style)
28 from styles import (default_light_style_sheet, default_light_syntax_style,
29 default_dark_style_sheet, default_dark_syntax_style,
30 default_bw_style_sheet, default_bw_syntax_style)
30 31
31 32 #-----------------------------------------------------------------------------
32 33 # Constants
33 34 #-----------------------------------------------------------------------------
34 35
35 36 # Default strings to build and display input and output prompts (and separators
36 37 # in between)
37 38 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
38 39 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
39 40 default_input_sep = '\n'
40 41 default_output_sep = ''
41 42 default_output_sep2 = ''
42 43
43 44 # Base path for most payload sources.
44 45 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
45 46
46 47 #-----------------------------------------------------------------------------
47 48 # IPythonWidget class
48 49 #-----------------------------------------------------------------------------
49 50
50 51 class IPythonWidget(FrontendWidget):
51 52 """ A FrontendWidget for an IPython kernel.
52 53 """
53 54
54 55 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
55 56 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
56 57 # settings.
57 58 custom_edit = Bool(False)
58 59 custom_edit_requested = QtCore.pyqtSignal(object, object)
59 60
60 61 # A command for invoking a system text editor. If the string contains a
61 62 # {filename} format specifier, it will be used. Otherwise, the filename will
62 63 # be appended to the end the command.
63 64 editor = Str('default', config=True)
64 65
65 66 # The editor command to use when a specific line number is requested. The
66 67 # string should contain two format specifiers: {line} and {filename}. If
67 68 # this parameter is not specified, the line number option to the %edit magic
68 69 # will be ignored.
69 70 editor_line = Str(config=True)
70 71
71 72 # A CSS stylesheet. The stylesheet can contain classes for:
72 73 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
73 74 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
74 75 # 3. IPython: .error, .in-prompt, .out-prompt, etc
75 76 style_sheet = Str(config=True)
76 77
77 78 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
78 79 # the style sheet is queried for Pygments style information.
79 80 syntax_style = Str(config=True)
80 81
81 82 # Prompts.
82 83 in_prompt = Str(default_in_prompt, config=True)
83 84 out_prompt = Str(default_out_prompt, config=True)
84 85 input_sep = Str(default_input_sep, config=True)
85 86 output_sep = Str(default_output_sep, config=True)
86 87 output_sep2 = Str(default_output_sep2, config=True)
87 88
88 89 # FrontendWidget protected class variables.
89 90 _input_splitter_class = IPythonInputSplitter
90 91
91 92 # IPythonWidget protected class variables.
92 93 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
93 94 _payload_source_edit = zmq_shell_source + '.edit_magic'
94 95 _payload_source_exit = zmq_shell_source + '.ask_exit'
95 96 _payload_source_loadpy = zmq_shell_source + '.magic_loadpy'
96 97 _payload_source_page = 'IPython.zmq.page.page'
97 98
98 99 #---------------------------------------------------------------------------
99 100 # 'object' interface
100 101 #---------------------------------------------------------------------------
101 102
102 103 def __init__(self, *args, **kw):
103 104 super(IPythonWidget, self).__init__(*args, **kw)
104 105
105 106 # IPythonWidget protected variables.
106 107 self._code_to_load = None
107 108 self._payload_handlers = {
108 109 self._payload_source_edit : self._handle_payload_edit,
109 110 self._payload_source_exit : self._handle_payload_exit,
110 111 self._payload_source_page : self._handle_payload_page,
111 112 self._payload_source_loadpy : self._handle_payload_loadpy }
112 113 self._previous_prompt_obj = None
113 114
114 115 # Initialize widget styling.
115 116 if self.style_sheet:
116 117 self._style_sheet_changed()
117 118 self._syntax_style_changed()
118 119 else:
119 120 self.set_default_style()
120 121
121 122 #---------------------------------------------------------------------------
122 123 # 'BaseFrontendMixin' abstract interface
123 124 #---------------------------------------------------------------------------
124 125
125 126 def _handle_complete_reply(self, rep):
126 127 """ Reimplemented to support IPython's improved completion machinery.
127 128 """
128 129 cursor = self._get_cursor()
129 130 info = self._request_info.get('complete')
130 131 if info and info.id == rep['parent_header']['msg_id'] and \
131 132 info.pos == cursor.position():
132 133 matches = rep['content']['matches']
133 134 text = rep['content']['matched_text']
134 135 offset = len(text)
135 136
136 137 # Clean up matches with period and path separators if the matched
137 138 # text has not been transformed. This is done by truncating all
138 139 # but the last component and then suitably decreasing the offset
139 140 # between the current cursor position and the start of completion.
140 141 if len(matches) > 1 and matches[0][:offset] == text:
141 142 parts = re.split(r'[./\\]', text)
142 143 sep_count = len(parts) - 1
143 144 if sep_count:
144 145 chop_length = sum(map(len, parts[:sep_count])) + sep_count
145 146 matches = [ match[chop_length:] for match in matches ]
146 147 offset -= chop_length
147 148
148 149 # Move the cursor to the start of the match and complete.
149 150 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
150 151 self._complete_with_items(cursor, matches)
151 152
152 153 def _handle_execute_reply(self, msg):
153 154 """ Reimplemented to support prompt requests.
154 155 """
155 156 info = self._request_info.get('execute')
156 157 if info and info.id == msg['parent_header']['msg_id']:
157 158 if info.kind == 'prompt':
158 159 number = msg['content']['execution_count'] + 1
159 160 self._show_interpreter_prompt(number)
160 161 else:
161 162 super(IPythonWidget, self)._handle_execute_reply(msg)
162 163
163 164 def _handle_history_reply(self, msg):
164 165 """ Implemented to handle history replies, which are only supported by
165 166 the IPython kernel.
166 167 """
167 168 history_dict = msg['content']['history']
168 169 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
169 170 self._set_history(items)
170 171
171 172 def _handle_pyout(self, msg):
172 173 """ Reimplemented for IPython-style "display hook".
173 174 """
174 175 if not self._hidden and self._is_from_this_session(msg):
175 176 content = msg['content']
176 177 prompt_number = content['execution_count']
177 178 self._append_plain_text(self.output_sep)
178 179 self._append_html(self._make_out_prompt(prompt_number))
179 180 self._append_plain_text(content['data']+self.output_sep2)
180 181
181 182 def _started_channels(self):
182 183 """ Reimplemented to make a history request.
183 184 """
184 185 super(IPythonWidget, self)._started_channels()
185 186 # FIXME: Disabled until history requests are properly implemented.
186 187 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
187 188
188 189 #---------------------------------------------------------------------------
189 190 # 'ConsoleWidget' public interface
190 191 #---------------------------------------------------------------------------
191 192
192 193 def copy(self):
193 194 """ Copy the currently selected text to the clipboard, removing prompts
194 195 if possible.
195 196 """
196 197 text = unicode(self._control.textCursor().selection().toPlainText())
197 198 if text:
198 199 lines = map(transform_ipy_prompt, text.splitlines())
199 200 text = '\n'.join(lines)
200 201 QtGui.QApplication.clipboard().setText(text)
201 202
202 203 #---------------------------------------------------------------------------
203 204 # 'FrontendWidget' public interface
204 205 #---------------------------------------------------------------------------
205 206
206 207 def execute_file(self, path, hidden=False):
207 208 """ Reimplemented to use the 'run' magic.
208 209 """
209 210 self.execute('%%run %s' % path, hidden=hidden)
210 211
211 212 #---------------------------------------------------------------------------
212 213 # 'FrontendWidget' protected interface
213 214 #---------------------------------------------------------------------------
214 215
215 216 def _complete(self):
216 217 """ Reimplemented to support IPython's improved completion machinery.
217 218 """
218 219 # We let the kernel split the input line, so we *always* send an empty
219 220 # text field. Readline-based frontends do get a real text field which
220 221 # they can use.
221 222 text = ''
222 223
223 224 # Send the completion request to the kernel
224 225 msg_id = self.kernel_manager.xreq_channel.complete(
225 226 text, # text
226 227 self._get_input_buffer_cursor_line(), # line
227 228 self._get_input_buffer_cursor_column(), # cursor_pos
228 229 self.input_buffer) # block
229 230 pos = self._get_cursor().position()
230 231 info = self._CompletionRequest(msg_id, pos)
231 232 self._request_info['complete'] = info
232 233
233 234 def _get_banner(self):
234 235 """ Reimplemented to return IPython's default banner.
235 236 """
236 237 return default_gui_banner
237 238
238 239 def _process_execute_error(self, msg):
239 240 """ Reimplemented for IPython-style traceback formatting.
240 241 """
241 242 content = msg['content']
242 243 traceback = '\n'.join(content['traceback']) + '\n'
243 244 if False:
244 245 # FIXME: For now, tracebacks come as plain text, so we can't use
245 246 # the html renderer yet. Once we refactor ultratb to produce
246 247 # properly styled tracebacks, this branch should be the default
247 248 traceback = traceback.replace(' ', '&nbsp;')
248 249 traceback = traceback.replace('\n', '<br/>')
249 250
250 251 ename = content['ename']
251 252 ename_styled = '<span class="error">%s</span>' % ename
252 253 traceback = traceback.replace(ename, ename_styled)
253 254
254 255 self._append_html(traceback)
255 256 else:
256 257 # This is the fallback for now, using plain text with ansi escapes
257 258 self._append_plain_text(traceback)
258 259
259 260 def _process_execute_payload(self, item):
260 261 """ Reimplemented to dispatch payloads to handler methods.
261 262 """
262 263 handler = self._payload_handlers.get(item['source'])
263 264 if handler is None:
264 265 # We have no handler for this type of payload, simply ignore it
265 266 return False
266 267 else:
267 268 handler(item)
268 269 return True
269 270
270 271 def _show_interpreter_prompt(self, number=None):
271 272 """ Reimplemented for IPython-style prompts.
272 273 """
273 274 # If a number was not specified, make a prompt number request.
274 275 if number is None:
275 276 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
276 277 info = self._ExecutionRequest(msg_id, 'prompt')
277 278 self._request_info['execute'] = info
278 279 return
279 280
280 281 # Show a new prompt and save information about it so that it can be
281 282 # updated later if the prompt number turns out to be wrong.
282 283 self._prompt_sep = self.input_sep
283 284 self._show_prompt(self._make_in_prompt(number), html=True)
284 285 block = self._control.document().lastBlock()
285 286 length = len(self._prompt)
286 287 self._previous_prompt_obj = self._PromptBlock(block, length, number)
287 288
288 289 # Update continuation prompt to reflect (possibly) new prompt length.
289 290 self._set_continuation_prompt(
290 291 self._make_continuation_prompt(self._prompt), html=True)
291 292
292 293 # Load code from the %loadpy magic, if necessary.
293 294 if self._code_to_load is not None:
294 295 self.input_buffer = dedent(unicode(self._code_to_load).rstrip())
295 296 self._code_to_load = None
296 297
297 298 def _show_interpreter_prompt_for_reply(self, msg):
298 299 """ Reimplemented for IPython-style prompts.
299 300 """
300 301 # Update the old prompt number if necessary.
301 302 content = msg['content']
302 303 previous_prompt_number = content['execution_count']
303 304 if self._previous_prompt_obj and \
304 305 self._previous_prompt_obj.number != previous_prompt_number:
305 306 block = self._previous_prompt_obj.block
306 307
307 308 # Make sure the prompt block has not been erased.
308 309 if block.isValid() and not block.text().isEmpty():
309 310
310 311 # Remove the old prompt and insert a new prompt.
311 312 cursor = QtGui.QTextCursor(block)
312 313 cursor.movePosition(QtGui.QTextCursor.Right,
313 314 QtGui.QTextCursor.KeepAnchor,
314 315 self._previous_prompt_obj.length)
315 316 prompt = self._make_in_prompt(previous_prompt_number)
316 317 self._prompt = self._insert_html_fetching_plain_text(
317 318 cursor, prompt)
318 319
319 320 # When the HTML is inserted, Qt blows away the syntax
320 321 # highlighting for the line, so we need to rehighlight it.
321 322 self._highlighter.rehighlightBlock(cursor.block())
322 323
323 324 self._previous_prompt_obj = None
324 325
325 326 # Show a new prompt with the kernel's estimated prompt number.
326 327 self._show_interpreter_prompt(previous_prompt_number + 1)
327 328
328 329 #---------------------------------------------------------------------------
329 330 # 'IPythonWidget' interface
330 331 #---------------------------------------------------------------------------
331 332
332 def set_default_style(self, lightbg=True):
333 def set_default_style(self, colors='light'):
333 334 """ Sets the widget style to the class defaults.
334 335
335 336 Parameters:
336 337 -----------
337 lightbg : bool, optional (default True)
338 colors : str, optional (default light)
338 339 Whether to use the default IPython light background or dark
339 background style.
340 background or B&W style.
340 341 """
341 if lightbg:
342 if colors=='light':
342 343 self.style_sheet = default_light_style_sheet
343 344 self.syntax_style = default_light_syntax_style
344 else:
345 elif colors=='dark':
345 346 self.style_sheet = default_dark_style_sheet
346 347 self.syntax_style = default_dark_syntax_style
348 elif colors=='bw':
349 self.style_sheet = default_bw_style_sheet
350 self.syntax_style = default_bw_syntax_style
347 351
348 352 #---------------------------------------------------------------------------
349 353 # 'IPythonWidget' protected interface
350 354 #---------------------------------------------------------------------------
351 355
352 356 def _edit(self, filename, line=None):
353 357 """ Opens a Python script for editing.
354 358
355 359 Parameters:
356 360 -----------
357 361 filename : str
358 362 A path to a local system file.
359 363
360 364 line : int, optional
361 365 A line of interest in the file.
362 366 """
363 367 if self.custom_edit:
364 368 self.custom_edit_requested.emit(filename, line)
365 369 elif self.editor == 'default':
366 370 self._append_plain_text('No default editor available.\n')
367 371 else:
368 372 try:
369 373 filename = '"%s"' % filename
370 374 if line and self.editor_line:
371 375 command = self.editor_line.format(filename=filename,
372 376 line=line)
373 377 else:
374 378 try:
375 379 command = self.editor.format()
376 380 except KeyError:
377 381 command = self.editor.format(filename=filename)
378 382 else:
379 383 command += ' ' + filename
380 384 except KeyError:
381 385 self._append_plain_text('Invalid editor command.\n')
382 386 else:
383 387 try:
384 388 Popen(command, shell=True)
385 389 except OSError:
386 390 msg = 'Opening editor with command "%s" failed.\n'
387 391 self._append_plain_text(msg % command)
388 392
389 393 def _make_in_prompt(self, number):
390 394 """ Given a prompt number, returns an HTML In prompt.
391 395 """
392 396 body = self.in_prompt % number
393 397 return '<span class="in-prompt">%s</span>' % body
394 398
395 399 def _make_continuation_prompt(self, prompt):
396 400 """ Given a plain text version of an In prompt, returns an HTML
397 401 continuation prompt.
398 402 """
399 403 end_chars = '...: '
400 404 space_count = len(prompt.lstrip('\n')) - len(end_chars)
401 405 body = '&nbsp;' * space_count + end_chars
402 406 return '<span class="in-prompt">%s</span>' % body
403 407
404 408 def _make_out_prompt(self, number):
405 409 """ Given a prompt number, returns an HTML Out prompt.
406 410 """
407 411 body = self.out_prompt % number
408 412 return '<span class="out-prompt">%s</span>' % body
409 413
410 414 #------ Payload handlers --------------------------------------------------
411 415
412 416 # Payload handlers with a generic interface: each takes the opaque payload
413 417 # dict, unpacks it and calls the underlying functions with the necessary
414 418 # arguments.
415 419
416 420 def _handle_payload_edit(self, item):
417 421 self._edit(item['filename'], item['line_number'])
418 422
419 423 def _handle_payload_exit(self, item):
420 424 self.exit_requested.emit()
421 425
422 426 def _handle_payload_loadpy(self, item):
423 427 # Simple save the text of the .py file for later. The text is written
424 428 # to the buffer when _prompt_started_hook is called.
425 429 self._code_to_load = item['text']
426 430
427 431 def _handle_payload_page(self, item):
428 432 # Since the plain text widget supports only a very small subset of HTML
429 433 # and we have no control over the HTML source, we only page HTML
430 434 # payloads in the rich text widget.
431 435 if item['html'] and self.kind == 'rich':
432 436 self._page(item['html'], html=True)
433 437 else:
434 438 self._page(item['text'], html=False)
435 439
436 440 #------ Trait change handlers ---------------------------------------------
437 441
438 442 def _style_sheet_changed(self):
439 443 """ Set the style sheets of the underlying widgets.
440 444 """
441 445 self.setStyleSheet(self.style_sheet)
442 446 self._control.document().setDefaultStyleSheet(self.style_sheet)
443 447 if self._page_control:
444 448 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
445 449
446 450 bg_color = self._control.palette().background().color()
447 451 self._ansi_processor.set_background_color(bg_color)
448 452
449 453 def _syntax_style_changed(self):
450 454 """ Set the style for the syntax highlighter.
451 455 """
452 456 if self.syntax_style:
453 457 self._highlighter.set_style(self.syntax_style)
454 458 else:
455 459 self._highlighter.set_style_sheet(self.style_sheet)
456 460
@@ -1,235 +1,247 b''
1 1 """ A minimal application using the Qt console-style IPython frontend.
2 2 """
3 3
4 4 #-----------------------------------------------------------------------------
5 5 # Imports
6 6 #-----------------------------------------------------------------------------
7 7
8 8 # Systemm library imports
9 9 from PyQt4 import QtGui
10 10 from pygments.styles import get_all_styles
11 11 # Local imports
12 12 from IPython.external.argparse import ArgumentParser
13 13 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
14 14 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
15 15 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
16 16 from IPython.frontend.qt.console import styles
17 17 from IPython.frontend.qt.kernelmanager import QtKernelManager
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Network Constants
21 21 #-----------------------------------------------------------------------------
22 22
23 23 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
24 24
25 25 #-----------------------------------------------------------------------------
26 26 # Classes
27 27 #-----------------------------------------------------------------------------
28 28
29 29 class MainWindow(QtGui.QMainWindow):
30 30
31 31 #---------------------------------------------------------------------------
32 32 # 'object' interface
33 33 #---------------------------------------------------------------------------
34 34
35 35 def __init__(self, app, frontend, existing=False, may_close=True):
36 36 """ Create a MainWindow for the specified FrontendWidget.
37 37
38 38 The app is passed as an argument to allow for different
39 39 closing behavior depending on whether we are the Kernel's parent.
40 40
41 41 If existing is True, then this Console does not own the Kernel.
42 42
43 43 If may_close is True, then this Console is permitted to close the kernel
44 44 """
45 45 super(MainWindow, self).__init__()
46 46 self._app = app
47 47 self._frontend = frontend
48 48 self._existing = existing
49 49 if existing:
50 50 self._may_close = may_close
51 51 else:
52 52 self._may_close = True
53 53 self._frontend.exit_requested.connect(self.close)
54 54 self.setCentralWidget(frontend)
55 55
56 56 #---------------------------------------------------------------------------
57 57 # QWidget interface
58 58 #---------------------------------------------------------------------------
59 59
60 60 def closeEvent(self, event):
61 61 """ Reimplemented to prompt the user and close the kernel cleanly.
62 62 """
63 63 kernel_manager = self._frontend.kernel_manager
64 64 if kernel_manager and kernel_manager.channels_running:
65 65 title = self.window().windowTitle()
66 66 cancel = QtGui.QMessageBox.Cancel
67 67 okay = QtGui.QMessageBox.Ok
68 68 if self._may_close:
69 69 msg = "You are closing this Console window."
70 70 info = "Would you like to quit the Kernel and all attached Consoles as well?"
71 71 justthis = QtGui.QPushButton("&No, just this Console", self)
72 72 justthis.setShortcut('N')
73 73 closeall = QtGui.QPushButton("&Yes, quit everything", self)
74 74 closeall.setShortcut('Y')
75 75 box = QtGui.QMessageBox(QtGui.QMessageBox.Question, title, msg)
76 76 box.setInformativeText(info)
77 77 box.addButton(cancel)
78 78 box.addButton(justthis, QtGui.QMessageBox.NoRole)
79 79 box.addButton(closeall, QtGui.QMessageBox.YesRole)
80 80 box.setDefaultButton(closeall)
81 81 box.setEscapeButton(cancel)
82 82 reply = box.exec_()
83 83 if reply == 1: # close All
84 84 kernel_manager.shutdown_kernel()
85 85 #kernel_manager.stop_channels()
86 86 event.accept()
87 87 elif reply == 0: # close Console
88 88 if not self._existing:
89 89 # I have the kernel: don't quit, just close the window
90 90 self._app.setQuitOnLastWindowClosed(False)
91 91 self.deleteLater()
92 92 event.accept()
93 93 else:
94 94 event.ignore()
95 95 else:
96 96 reply = QtGui.QMessageBox.question(self, title,
97 97 "Are you sure you want to close this Console?"+
98 98 "\nThe Kernel and other Consoles will remain active.",
99 99 okay|cancel,
100 100 defaultButton=okay
101 101 )
102 102 if reply == okay:
103 103 event.accept()
104 104 else:
105 105 event.ignore()
106 106
107 107
108 108 #-----------------------------------------------------------------------------
109 109 # Main entry point
110 110 #-----------------------------------------------------------------------------
111 111
112 112 def main():
113 113 """ Entry point for application.
114 114 """
115 115 # Parse command line arguments.
116 116 parser = ArgumentParser()
117 117 kgroup = parser.add_argument_group('kernel options')
118 118 kgroup.add_argument('-e', '--existing', action='store_true',
119 119 help='connect to an existing kernel')
120 120 kgroup.add_argument('--ip', type=str, default=LOCALHOST,
121 121 help=\
122 122 "set the kernel\'s IP address [default localhost].\
123 123 If the IP address is something other than localhost, then \
124 124 Consoles on other machines will be able to connect\
125 125 to the Kernel, so be careful!")
126 126 kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
127 127 help='set the XREQ channel port [default random]')
128 128 kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
129 129 help='set the SUB channel port [default random]')
130 130 kgroup.add_argument('--rep', type=int, metavar='PORT', default=0,
131 131 help='set the REP channel port [default random]')
132 132 kgroup.add_argument('--hb', type=int, metavar='PORT', default=0,
133 133 help='set the heartbeat port [default random]')
134 134
135 135 egroup = kgroup.add_mutually_exclusive_group()
136 136 egroup.add_argument('--pure', action='store_true', help = \
137 137 'use a pure Python kernel instead of an IPython kernel')
138 138 egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
139 139 const='auto', help = \
140 140 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
141 141 given, the GUI backend is matplotlib's, otherwise use one of: \
142 142 ['tk', 'gtk', 'qt', 'wx', 'inline'].")
143 143
144 144 wgroup = parser.add_argument_group('widget options')
145 145 wgroup.add_argument('--paging', type=str, default='inside',
146 146 choices = ['inside', 'hsplit', 'vsplit', 'none'],
147 147 help='set the paging style [default inside]')
148 148 wgroup.add_argument('--rich', action='store_true',
149 149 help='enable rich text support')
150 150 wgroup.add_argument('--gui-completion', action='store_true',
151 151 help='use a GUI widget for tab completion')
152 152 wgroup.add_argument('--style', type=str,
153 help='specify a pygments style by name. \
154 Valid are: %s'%(list(get_all_styles())))
153 choices = list(get_all_styles()),
154 help='specify a pygments style for by name.')
155 155 wgroup.add_argument('--stylesheet', type=str,
156 156 help="path to a custom CSS stylesheet.")
157 wgroup.add_argument('--dark', action='store_true',
158 help="use the dark style template instead of lightbg.\
159 If --style is not specified, the default dark style is used.")
157 wgroup.add_argument('--colors', type=str,
158 help="Set the color scheme (light, dark, or bw). This is guessed\
159 based on the pygments style if not set.")
160 160
161 161 args = parser.parse_args()
162 162
163 163 # Don't let Qt or ZMQ swallow KeyboardInterupts.
164 164 import signal
165 165 signal.signal(signal.SIGINT, signal.SIG_DFL)
166 166
167 167 # Create a KernelManager and start a kernel.
168 168 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
169 169 sub_address=(args.ip, args.sub),
170 170 rep_address=(args.ip, args.rep),
171 171 hb_address=(args.ip, args.hb))
172 172 if not args.existing:
173 173 # if not args.ip in LOCAL_IPS+ALL_ALIAS:
174 174 # raise ValueError("Must bind a local ip, such as: %s"%LOCAL_IPS)
175 175
176 176 kwargs = dict(ip=args.ip)
177 177 if args.pure:
178 178 kwargs['ipython']=False
179 179 elif args.pylab:
180 180 kwargs['pylab']=args.pylab
181 181
182 182 kernel_manager.start_kernel(**kwargs)
183 183 kernel_manager.start_channels()
184 184
185 185 local_kernel = (not args.existing) or args.ip in LOCAL_IPS
186 186 # Create the widget.
187 187 app = QtGui.QApplication([])
188 188 if args.pure:
189 189 kind = 'rich' if args.rich else 'plain'
190 190 widget = FrontendWidget(kind=kind, paging=args.paging, local_kernel=local_kernel)
191 191 elif args.rich or args.pylab:
192 192 widget = RichIPythonWidget(paging=args.paging, local_kernel=local_kernel)
193 193 else:
194 194 widget = IPythonWidget(paging=args.paging, local_kernel=local_kernel)
195 195 widget.gui_completion = args.gui_completion
196 196 widget.kernel_manager = kernel_manager
197 197
198 198 # configure the style:
199 199 if not args.pure: # only IPythonWidget supports styles
200 # parse the colors arg down to current known labels
201 if args.colors:
202 colors=args.colors.lower()
203 if colors in ('lightbg', 'light'):
204 colors='light'
205 elif colors in ('dark', 'linux'):
206 colors='dark'
207 else:
208 colors='nocolor'
209 else:
210 colors=None
211 lightbg = colors != 'linux'
212
200 213 if args.style:
201 214 # guess whether it's a dark style:
202 dark = args.dark or styles.dark_style(args.style)
203 215 widget.syntax_style = args.style
204 widget.style_sheet = styles.sheet_from_template(args.style, not dark)
216 widget.style_sheet = styles.sheet_from_template(args.style, lightbg)
205 217 widget._syntax_style_changed()
206 218 widget._style_sheet_changed()
207 elif args.dark:
208 # use default dark style
209 widget.set_default_style(lightbg=False)
219 elif colors:
220 # use a default style
221 widget.set_default_style(colors=colors)
210 222 else:
211 223 # this is redundant for now, but allows the widget's
212 224 # defaults to change
213 widget.set_default_style(lightbg=True)
225 widget.set_default_style()
214 226
215 227 if args.stylesheet:
216 228 # we got an expicit stylesheet
217 229 if os.path.isfile(args.stylesheet):
218 230 with open(args.stylesheet) as f:
219 231 sheet = f.read()
220 232 widget.style_sheet = sheet
221 233 widget._style_sheet_changed()
222 234 else:
223 235 raise IOError("Stylesheet %r not found."%args.stylesheet)
224 236
225 237 # Create the main window.
226 238 window = MainWindow(app, widget, args.existing, may_close=local_kernel)
227 239 window.setWindowTitle('Python' if args.pure else 'IPython')
228 240 window.show()
229 241
230 242 # Start the application main loop.
231 243 app.exec_()
232 244
233 245
234 246 if __name__ == '__main__':
235 247 main()
@@ -1,102 +1,119 b''
1 1 """ Style utilities, templates, and defaults for syntax highlighting widgets.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 from colorsys import rgb_to_hls
8 8 from pygments.styles import get_style_by_name
9 9 from pygments.token import Token
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Constants
13 13 #-----------------------------------------------------------------------------
14 14
15 15 # The default light style sheet: black text on a white background.
16 16 default_light_style_template = '''
17 17 QPlainTextEdit, QTextEdit { background-color: %(bgcolor)s;
18 18 color: %(fgcolor)s ;
19 19 selection-background-color: %(select)s}
20 20 .error { color: red; }
21 21 .in-prompt { color: navy; }
22 22 .in-prompt-number { font-weight: bold; }
23 23 .out-prompt { color: darkred; }
24 24 .out-prompt-number { font-weight: bold; }
25 25 '''
26 26 default_light_style_sheet = default_light_style_template%dict(
27 27 bgcolor='white', fgcolor='black', select="#ccc")
28 28 default_light_syntax_style = 'default'
29 29
30 30 # The default dark style sheet: white text on a black background.
31 31 default_dark_style_template = '''
32 32 QPlainTextEdit, QTextEdit { background-color: %(bgcolor)s;
33 33 color: %(fgcolor)s ;
34 34 selection-background-color: %(select)s}
35 35 QFrame { border: 1px solid grey; }
36 36 .error { color: red; }
37 37 .in-prompt { color: lime; }
38 38 .in-prompt-number { color: lime; font-weight: bold; }
39 39 .out-prompt { color: red; }
40 40 .out-prompt-number { color: red; font-weight: bold; }
41 41 '''
42 42 default_dark_style_sheet = default_dark_style_template%dict(
43 43 bgcolor='black', fgcolor='white', select="#555")
44 44 default_dark_syntax_style = 'monokai'
45 45
46 # The default monochrome
47 default_bw_style_sheet = '''
48 QPlainTextEdit, QTextEdit { background-color: white;
49 color: black ;
50 selection-background-color: #cccccc}
51 .in-prompt-number { font-weight: bold; }
52 .out-prompt-number { font-weight: bold; }
53 '''
54 default_bw_syntax_style = 'bw'
55
56
46 57 def hex_to_rgb(color):
47 58 """Convert a hex color to rgb integer tuple."""
48 59 if color.startswith('#'):
49 60 color = color[1:]
50 61 if len(color) == 3:
51 62 color = ''.join([c*2 for c in color])
52 63 if len(color) != 6:
53 64 return False
54 65 try:
55 66 r = int(color[:2],16)
56 67 g = int(color[:2],16)
57 68 b = int(color[:2],16)
58 69 except ValueError:
59 70 return False
60 71 else:
61 72 return r,g,b
62 73
63 74 def dark_color(color):
64 75 """Check whether a color is 'dark'.
65 76
66 77 Currently, this is simply whether the luminance is <50%"""
67 78 rgb = hex_to_rgb(color)
68 79 if rgb:
69 80 return rgb_to_hls(*rgb)[1] < 128
70 81 else: # default to False
71 82 return False
72 83
73 84 def dark_style(stylename):
74 85 """Guess whether the background of the style with name 'stylename'
75 86 counts as 'dark'."""
76 87 return dark_color(get_style_by_name(stylename).background_color)
77 88
78 89 def get_colors(stylename):
79 """Construct the keys to be used building the base stylesheet."""
90 """Construct the keys to be used building the base stylesheet
91 from a templatee."""
80 92 style = get_style_by_name(stylename)
81 93 fgcolor = style.style_for_token(Token.Text)['color'] or ''
82 94 if len(fgcolor) in (3,6):
83 95 # could be 'abcdef' or 'ace' hex, which needs '#' prefix
84 96 try:
85 97 int(fgcolor, 16)
86 98 except TypeError:
87 99 pass
88 100 else:
89 101 fgcolor = "#"+fgcolor
90 102
91 103 return dict(
92 104 bgcolor = style.background_color,
93 105 select = style.highlight_color,
94 106 fgcolor = fgcolor
95 107 )
96 108
97 def sheet_from_template(name, lightbg=True):
109 def sheet_from_template(name, colors='light'):
98 110 """Use one of the base templates, and set bg/fg/select colors."""
99 if lightbg:
111 colors = colors.lower()
112 if colors=='light':
100 113 return default_light_style_template%get_colors(name)
114 elif colors=='dark':
115 return default_dark_style_template%get_colors(name)
116 elif colors=='nocolor':
117 return default_bw_style_sheet
101 118 else:
102 return default_dark_style_template%get_colors(name) No newline at end of file
119 raise KeyError("No such color scheme: %s"%colors)
General Comments 0
You need to be logged in to leave comments. Login now