1# consolewindow.py
2# a python-like qt console
3
4#    Copyright (C) 2003 Jeremy S. Sanders
5#    Email: Jeremy Sanders <jeremy@jeremysanders.net>
6#
7#    This program is free software; you can redistribute it and/or modify
8#    it under the terms of the GNU General Public License as published by
9#    the Free Software Foundation; either version 2 of the License, or
10#    (at your option) any later version.
11#
12#    This program is distributed in the hope that it will be useful,
13#    but WITHOUT ANY WARRANTY; without even the implied warranty of
14#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15#    GNU General Public License for more details.
16#
17#    You should have received a copy of the GNU General Public License along
18#    with this program; if not, write to the Free Software Foundation, Inc.,
19#    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20##############################################################################
21
22from __future__ import division
23import codeop
24import traceback
25import sys
26
27from ..compat import cstr
28from .. import qtall as qt
29
30from .. import document
31from .. import utils
32from .. import setting
33
34# TODO - command line completion
35
36def _(text, disambiguation=None, context='ConsoleWindow'):
37    """Translate text."""
38    return qt.QCoreApplication.translate(context, text, disambiguation)
39
40class _Writer(object):
41    """ Class to behave like an output stream. Pipes input back to
42    the specified function."""
43
44    def __init__(self, function):
45        """Set the function output is sent to."""
46        self.function = function
47
48    def write(self, text):
49        """Send text to the output function."""
50        self.function(text)
51
52    def flush(self):
53        """Does nothing as yet."""
54        pass
55
56class _Reader(object):
57    """Fake reading input stream object."""
58    def read(self, *args):
59        raise IOError('Interactive input not supported')
60    def readline(self, *args):
61        raise IOError('Interactive input not supported')
62
63class _CommandEdit(qt.QLineEdit):
64    """ A special class to allow entering of the command line.
65
66    emits sigEnter if the return key is pressed, and returns command
67    The edit control has a history (press up and down keys to access)
68    """
69
70    sigEnter = qt.pyqtSignal(cstr)
71
72    def __init__(self, *args):
73        qt.QLineEdit.__init__(self, *args)
74        self.history = []
75        self.history_posn = 0
76        self.entered_text = ''
77
78        self.returnPressed.connect(self.slotReturnPressed)
79
80        self.setToolTip(_("Input a python expression here and press enter"))
81
82    def slotReturnPressed(self):
83        """ Called if the return key is pressed in the edit control."""
84
85        # retrieve the text
86        command = self.text()
87        self.setText("")
88
89        # keep the command for history
90        self.history.append(command)
91        self.history_posn = len(self.history)
92        self.entered_text = ''
93
94        # tell the console we have a command
95        self.sigEnter.emit(command)
96
97    historykeys = (qt.Qt.Key_Up, qt.Qt.Key_Down)
98
99    def keyPressEvent(self, key):
100        """ Overridden to handle history. """
101
102        qt.QLineEdit.keyPressEvent(self, key)
103        code = key.key()
104
105        # check whether one of the "history keys" has been pressed
106        if code in _CommandEdit.historykeys:
107
108            # look for the next or previous history item which our current text
109            # is a prefix of
110            if self.isModified():
111                text = self.text()
112                self.history_posn = len(self.history)
113            else:
114                text = self.entered_text
115
116            if code == qt.Qt.Key_Up:
117                step = -1
118            elif code == qt.Qt.Key_Down:
119                step = 1
120
121            newpos = self.history_posn + step
122
123            while True:
124                if newpos >= len(self.history):
125                    break
126                if newpos < 0:
127                    return
128                if self.history[newpos].startswith(text):
129                    break
130
131                newpos += step
132
133            if newpos >= len(self.history):
134                # go back to whatever the user had typed in
135                self.history_posn = len(self.history)
136                self.setText(self.entered_text)
137                return
138
139            # found a relevant history item
140            self.history_posn = newpos
141
142            # user has modified text since last set
143            if self.isModified():
144                self.entered_text = text
145
146            # replace the text in the control
147            text = self.history[ self.history_posn ]
148            self.setText(text)
149
150introtext=_(u'''Welcome to <b><font color="purple">Veusz %s</font></b> --- a scientific plotting application.<br>
151Copyright \u00a9 2003-2020 Jeremy Sanders &lt;jeremy@jeremysanders.net&gt; and contributors.<br>
152Veusz comes with ABSOLUTELY NO WARRANTY. Veusz is Free Software, and you are<br>
153welcome to redistribute it under certain conditions. Enter "GPL()" for details.<br>
154This window is a Python command line console and acts as a calculator.<br>
155''') % utils.version()
156
157class ConsoleWindow(qt.QDockWidget):
158    """ A python-like qt console."""
159
160    def __init__(self, thedocument, *args):
161        qt.QDockWidget.__init__(self, *args)
162        self.setWindowTitle(_("Console - Veusz"))
163        self.setObjectName("veuszconsolewindow")
164
165        # arrange sub-widgets in a vbox
166        self.vbox = qt.QWidget()
167        self.setWidget(self.vbox)
168        vlayout = qt.QVBoxLayout(self.vbox)
169        s = vlayout.contentsMargins().left()//4
170        vlayout.setContentsMargins(s,s,s,s)
171        vlayout.setSpacing(s)
172
173        # start an interpreter instance to the document
174        self.interpreter = document.CommandInterpreter(thedocument)
175        self.document = thedocument
176
177        # streams the output/input goes to/from
178        self.con_stdout = _Writer(self.output_stdout)
179        self.con_stderr = _Writer(self.output_stderr)
180        self.con_stdin = _Reader()
181
182        self.interpreter.setFiles(
183            self.con_stdout, self.con_stderr, self.con_stdin)
184        self.stdoutbuffer = ""
185        self.stderrbuffer = ""
186
187        # (mostly) hidden notification
188        self._hiddennotify = qt.QLabel()
189        vlayout.addWidget(self._hiddennotify)
190        self._hiddennotify.hide()
191
192        # the output from the console goes here
193        self._outputdisplay = qt.QTextEdit()
194        self._outputdisplay.setReadOnly(True)
195        self._outputdisplay.insertHtml( introtext )
196        vlayout.addWidget(self._outputdisplay)
197
198        self._hbox = qt.QWidget()
199        hlayout = qt.QHBoxLayout(self._hbox)
200        hlayout.setContentsMargins(0,0,0,0)
201        vlayout.addWidget(self._hbox)
202
203        self._prompt = qt.QLabel(">>>")
204        hlayout.addWidget(self._prompt)
205
206        # where commands are typed in
207        self._inputedit = _CommandEdit()
208        hlayout.addWidget(self._inputedit)
209        self._inputedit.setFocus()
210
211        # keep track of multiple line commands
212        self.command_build = ''
213
214        # get called if enter is pressed in the input control
215        self._inputedit.sigEnter.connect(self.slotEnter)
216        # called if document logs something
217        thedocument.sigLog.connect(self.slotDocumentLog)
218
219    def _makeTextFormat(self, cursor, color):
220        fmt = cursor.charFormat()
221
222        if color is not None:
223            brush = qt.QBrush(color)
224            fmt.setForeground(brush)
225        else:
226            # use the default foreground color
227            fmt.clearForeground()
228
229        return fmt
230
231    def appendOutput(self, text, style):
232        """Add text to the tail of the error log, with a specified style"""
233        if style == 'error':
234            color = setting.settingdb.color('error')
235        elif style == 'command':
236            color = setting.settingdb.color('command')
237        else:
238            color = None
239
240        cursor = self._outputdisplay.textCursor()
241        cursor.movePosition(qt.QTextCursor.End)
242        cursor.insertText(text, self._makeTextFormat(cursor, color))
243        self._outputdisplay.setTextCursor(cursor)
244        self._outputdisplay.ensureCursorVisible()
245
246    def runFunction(self, func):
247        """Execute the function within the console window, trapping
248        exceptions."""
249
250        # preserve output streams
251        saved = sys.stdout, sys.stderr, sys.stdin
252        sys.stdout, sys.stderr, sys.stdin = (
253            self.con_stdout, self.con_stderr, self.con_stdin)
254
255        # catch any exceptions, printing problems to stderr
256        with self.document.suspend():
257            try:
258                func()
259            except:
260                # print out the backtrace to stderr
261                info = sys.exc_info()
262                backtrace = traceback.format_exception(*info)
263                for line in backtrace:
264                    sys.stderr.write(line)
265
266        # return output streams
267        sys.stdout, sys.stderr, sys.stdin = saved
268
269    def checkVisible(self):
270        """If this window is hidden, show it, then hide it again in a few
271        seconds."""
272        if self.isHidden():
273            self._hiddennotify.setText(_(
274                "This window will shortly disappear. "
275                "You can bring it back by selecting "
276                "View, Windows, Console Window on the menu."))
277            qt.QTimer.singleShot(5000, self.hideConsole)
278            self.show()
279            self._hiddennotify.show()
280
281    def hideConsole(self):
282        """Hide window and notification widget."""
283        self._hiddennotify.hide()
284        self.hide()
285
286    def output_stdout(self, text):
287        """ Write text in stdout font to the log."""
288        self.checkVisible()
289        self.appendOutput(text, 'normal')
290
291    def output_stderr(self, text):
292        """ Write text in stderr font to the log."""
293        self.checkVisible()
294        self.appendOutput(text, 'error')
295
296    def insertTextInOutput(self, text):
297        """ Inserts the text into the log."""
298        self.appendOutput(text, 'normal')
299
300    def slotEnter(self, command):
301        """ Called if the return key is pressed in the edit control."""
302
303        newc = self.command_build + '\n' + command
304
305        # check whether command can be compiled
306        # c set to None if incomplete
307        try:
308            comp = codeop.compile_command(newc)
309        except Exception:
310            # we want errors to be caught by self.interpreter.run below
311            comp = 1
312
313        # which prompt?
314        prompt = '>>>'
315        if self.command_build != '':
316            prompt = '...'
317
318        # output the command in the log pane
319        self.appendOutput('%s %s\n' % (prompt, command), 'command')
320
321        # are we ready to run this?
322        if comp is None or (
323                len(command) != 0 and
324                len(self.command_build) != 0 and
325                (command[0] == ' ' or command[0] == '\t')):
326            # build up the expression
327            self.command_build = newc
328            # modify the prompt
329            self._prompt.setText('...')
330        else:
331            # actually execute the command
332            self.interpreter.run(newc)
333            self.command_build = ''
334            # modify the prompt
335            self._prompt.setText('>>>')
336
337    def slotDocumentLog(self, text):
338        """Output information if the document logs something."""
339        self.output_stderr(text + '\n')
340