Преглед на файлове

Initial implementation of console.

Juan Pablo Caram преди 11 години
родител
ревизия
8cb509d6f3
променени са 7 файла, в които са добавени 526 реда и са изтрити 14 реда
  1. 5 5
      FlatCAM.py
  2. 235 4
      FlatCAMApp.py
  3. 3 1
      FlatCAMGUI.py
  4. 17 3
      FlatCAMObj.py
  5. 27 0
      FlatCAMShell.py
  6. 1 1
      recent.json
  7. 238 0
      termwidget.py

+ 5 - 5
FlatCAM.py

@@ -3,11 +3,11 @@ from PyQt4 import QtGui
 from FlatCAMApp import App
 
 def debug_trace():
-  '''Set a tracepoint in the Python debugger that works with Qt'''
-  from PyQt4.QtCore import pyqtRemoveInputHook
-  #from pdb import set_trace
-  pyqtRemoveInputHook()
-  #set_trace()
+    '''Set a tracepoint in the Python debugger that works with Qt'''
+    from PyQt4.QtCore import pyqtRemoveInputHook
+    #from pdb import set_trace
+    pyqtRemoveInputHook()
+    #set_trace()
 
 debug_trace()
 app = QtGui.QApplication(sys.argv)

+ 235 - 4
FlatCAMApp.py

@@ -8,6 +8,7 @@ import simplejson as json
 import re
 import webbrowser
 import os
+import Tkinter
 
 from PyQt4 import QtCore
 
@@ -22,6 +23,8 @@ from FlatCAMGUI import *
 from FlatCAMCommon import LoudDict
 from FlatCAMTool import *
 
+from FlatCAMShell import FCShell
+
 
 ########################################
 ##                App                 ##
@@ -251,7 +254,7 @@ class App(QtCore.QObject):
 
         #### Check for updates ####
         # Separate thread (Not worker)
-        self.version = 6
+        self.version = 7
         App.log.info("Checking for updates in backgroud (this is version %s)." % str(self.version))
 
         self.worker2 = Worker(self, name="worker2")
@@ -293,6 +296,7 @@ class App(QtCore.QObject):
         self.ui.menuviewdisableall.triggered.connect(self.disable_plots)
         self.ui.menuviewdisableother.triggered.connect(lambda: self.disable_plots(except_current=True))
         self.ui.menuviewenable.triggered.connect(self.enable_all_plots)
+        self.ui.menutoolshell.triggered.connect(lambda: self.shell.show())
         self.ui.menuhelp_about.triggered.connect(self.on_about)
         self.ui.menuhelp_manual.triggered.connect(lambda: webbrowser.open(self.app_url))
         # Toolbar
@@ -302,6 +306,7 @@ class App(QtCore.QObject):
         self.ui.clear_plot_btn.triggered.connect(self.plotcanvas.clear)
         self.ui.replot_btn.triggered.connect(self.on_toolbar_replot)
         self.ui.delete_btn.triggered.connect(self.on_delete)
+        self.ui.shell_btn.triggered.connect(lambda: self.shell.show())
         # Object list
         self.collection.view.activated.connect(self.on_row_activated)
         # Options
@@ -324,6 +329,20 @@ class App(QtCore.QObject):
         self.measeurement_tool = Measurement(self)
         self.measeurement_tool.install()
 
+        #############
+        ### Shell ###
+        #############
+        # TODO: Move this to its own class
+        self.shell = FCShell(self)
+        self.shell.setWindowIcon(self.ui.app_icon)
+        self.shell.setWindowTitle("FlatCAM Shell")
+        self.shell.show()
+        self.shell.resize(550, 300)
+        self.shell.append_output("FlatCAM Alpha 7\n(c) 2014 Juan Pablo Caram\n\n")
+        self.shell.append_output("Type help to get started.\n\n")
+        self.tcl = Tkinter.Tcl()
+        self.setup_shell()
+
         App.log.debug("END of constructor. Releasing control.")
 
     def defaults_read_form(self):
@@ -368,6 +387,110 @@ class App(QtCore.QObject):
         # Send to worker
         self.worker_task.emit({'fcn': worker_task, 'params': [self]})
 
+    def execCommand(self, text):
+        """
+        Hadles input from the shell.
+
+        :param text: Input command
+        :return: None
+        """
+        text = str(text)
+
+        try:
+            result = self.tcl.eval(str(text))
+            self.shell.append_output(result + '\n')
+        except Tkinter.TclError, e:
+            self.shell.append_error('ERROR: ' + str(e) + '\n')
+            raise
+        return
+
+        def shhelp(p=None):
+            if not p:
+                return "Available commands:\n" + '\n'.join(['  ' + cmd for cmd in commands])
+
+            if p not in commands:
+                return "Unknown command: %s" % p
+
+            return commands[p]["help"]
+
+
+        commands = {
+            "open_gerber": {
+                "fcn": self.open_gerber,
+                "params": 1,
+                "converters": [lambda x: x],
+                "retfcn": lambda x: None,
+                "help": "Opens a Gerber file.\n> open_gerber <filename>\n   filename: Path to file to open."
+            },
+            "open_excellon": {
+                "fcn": self.open_excellon,
+                "params": 1,
+                "converters": [lambda x: x],
+                "retfcn": lambda x: None,
+                "help": "Opens an Excellon file.\n> open_excellon <filename>\n   filename: Path to file to open."
+            },
+            "open_gcode": {
+                "fcn": self.open_gcode,
+                "params": 1,
+                "converters": [lambda x: x],
+                "retfcn": lambda x: None,
+                "help": "Opens an G-Code file.\n> open_gcode <filename>\n   filename: Path to file to open."
+            },
+            "open_project": {
+                "fcn": self.open_project,
+                "params": 1,
+                "converters": [lambda x: x],
+                "retfcn": lambda x: None,
+                "help": "Opens a FlatCAM project.\n> open_project <filename>\n   filename: Path to file to open."
+            },
+            "save_project": {
+                "fcn": self.save_project,
+                "params": 1,
+                "converters": [lambda x: x],
+                "retfcn": lambda x: None,
+                "help": "Saves the FlatCAM project to file.\n> save_project <filename>\n   filename: Path to file to save."
+            },
+            "help": {
+                "fcn": shhelp,
+                "params": [0, 1],
+                "converters": [lambda x: x],
+                "retfcn": lambda x: x,
+                "help": "Shows list of commands."
+            }
+        }
+
+        parts = re.findall(r'([\w\\:\.]+|".*?")+', text)
+        parts = [p.replace('\n', '').replace('"', '') for p in parts]
+        self.log.debug(parts)
+        try:
+            if parts[0] not in commands:
+                self.shell.append_error("Unknown command\n")
+                return
+
+            #import inspect
+            #inspect.getargspec(someMethod)
+            if (type(commands[parts[0]]["params"]) is not list and len(parts)-1 != commands[parts[0]]["params"]) or \
+                    (type(commands[parts[0]]["params"]) is list and len(parts)-1 not in commands[parts[0]]["params"]):
+                self.shell.append_error(
+                    "Command %s takes %d arguments. %d given.\n" %
+                    (parts[0], commands[parts[0]]["params"], len(parts)-1)
+                )
+                return
+
+            cmdfcn = commands[parts[0]]["fcn"]
+            cmdconv = commands[parts[0]]["converters"]
+            if len(parts)-1 > 0:
+                retval = cmdfcn(*[cmdconv[i](parts[i+1]) for i in range(len(parts)-1)])
+            else:
+                retval = cmdfcn()
+            retfcn = commands[parts[0]]["retfcn"]
+            if retval and retfcn(retval):
+                self.shell.append_output(retfcn(retval) + "\n")
+
+        except:
+            self.shell.append_error(''.join(traceback.format_exc()))
+            #self.shell.append_error("?\n")
+
     def info(self, text):
         self.ui.info_label.setText(QtCore.QString(text))
 
@@ -540,7 +663,7 @@ class App(QtCore.QObject):
 
                 title = QtGui.QLabel(
                     "<font size=8><B>FlatCAM</B></font><BR>"
-                    "Version Alpha 6 (2014/09)<BR>"
+                    "Version Alpha 7 (2014/10)<BR>"
                     "<BR>"
                     "2D Post-processing for Manufacturing specialized in<BR>"
                     "Printed Circuit Boards<BR>"
@@ -1379,6 +1502,114 @@ class App(QtCore.QObject):
     def set_progress_bar(self, percentage, text=""):
         self.ui.progress_bar.setValue(int(percentage))
 
+    def setup_shell(self):
+        self.log.debug("setup_shell()")
+
+        def shelp(p=None):
+            if not p:
+                return "Available commands:\n" + '\n'.join(['  ' + cmd for cmd in commands]) + \
+                "\n\nType help <command_name> for usage.\n Example: help open_gerber"
+
+            if p not in commands:
+                return "Unknown command: %s" % p
+
+            return commands[p]["help"]
+
+        def options(name):
+            ops = self.collection.get_by_name(str(name)).options
+            return '\n'.join(["%s: %s" % (o, ops[o]) for o in ops])
+
+        def isolate(name, dia=None, passes=None, overlap=None):
+            dia = float(dia) if dia is not None else None
+            passes = int(passes) if passes is not None else None
+            overlap = float(overlap) if overlap is not None else None
+            self.collection.get_by_name(str(name)).isolate(dia, passes, overlap)
+
+        commands = {
+            'help': {
+                'fcn': shelp,
+                'help': "Shows list of commands."
+            },
+            'open_gerber': {
+                'fcn': self.open_gerber,
+                'help': "Opens a Gerber file.\n> open_gerber <filename>\n   filename: Path to file to open."
+            },
+            'open_excellon': {
+                'fcn': self.open_excellon,
+                'help': "Opens an Excellon file.\n> open_excellon <filename>\n   filename: Path to file to open."
+            },
+            'open_gcode': {
+                'fcn': self.open_gcode,
+                'help': "Opens an G-Code file.\n> open_gcode <filename>\n   filename: Path to file to open."
+            },
+            'open_project': {
+                'fcn': self.open_project,
+                "help": "Opens a FlatCAM project.\n> open_project <filename>\n   filename: Path to file to open."
+            },
+            'save_project': {
+                'fcn': self.save_project,
+                'help': "Saves the FlatCAM project to file.\n> save_project <filename>\n   filename: Path to file to save."
+            },
+            'set_active': {
+                'fcn': self.collection.set_active,
+                'help': "Sets a FlatCAM object as active.\n > set_active <name>\n   name: Name of the object."
+            },
+            'get_names': {
+                'fcn': lambda: '\n'.join(self.collection.get_names()),
+                'help': "Lists the names of objects in the project.\n > get_names"
+            },
+            'new': {
+                'fcn': self.on_file_new,
+                'help': "Starts a new project. Clears objects from memory.\n > new"
+            },
+            'options': {
+                'fcn': options,
+                'help': "Shows the settings for an object.\n > options <name>\n   name: Object name."
+            },
+            'isolate': {
+                'fcn': isolate,
+                'help': "Creates isolation routing geometry for the given Gerber.\n" +
+                        "> isolate <name> [dia [passes [overlap]]]\n" +
+                        "   name: Name if the object\n"
+                        "   dia: Tool diameter\n   passes: # of tool width\n" +
+                        "   overlap: Fraction of tool diameter to overlap passes"
+            },
+            'scale': {
+                'fcn': lambda name, factor: self.collection.get_by_name(str(name)).scale(float(factor)),
+                'help': "Resizes the object by a factor.\n" +
+                        "> scale <name> <factor>\n" +
+                        "   name: Name of the object\n   factor: Fraction by which to scale"
+            },
+            'offset': {
+                'fcn': lambda name, x, y: self.collection.get_by_name(str(name)).offset([float(x), float(y)]),
+                'help': "Changes the position of the object.\n" +
+                        "> offset <name> <x> <y>\n" +
+                        "   name: Name of the object\n" +
+                        "   x: X-axis distance\n" +
+                        "   y: Y-axis distance"
+            },
+            'plot': {
+                'fcn': self.plot_all,
+                'help': 'Updates the plot on the user interface'
+            }
+        }
+
+        for cmd in commands:
+            self.tcl.createcommand(cmd, commands[cmd]['fcn'])
+
+        self.tcl.eval('''
+            rename puts original_puts
+            proc puts {args} {
+                if {[llength $args] == 1} {
+                    return "[lindex $args 0]"
+                } else {
+                    eval original_puts $args
+                }
+            }
+            ''')
+
+
+
     def setup_recent_items(self):
         self.log.debug("setup_recent_items()")
 
@@ -1473,8 +1704,8 @@ class App(QtCore.QObject):
         try:
             data = json.load(f)
         except:
-            App.log.error("Could nor parse information about latest version.")
-            self.inform.emit("Could nor parse information about latest version.")
+            App.log.error("Could not parse information about latest version.")
+            self.inform.emit("Could not parse information about latest version.")
             f.close()
             return
 

+ 3 - 1
FlatCAMGUI.py

@@ -88,6 +88,7 @@ class FlatCAMGUI(QtGui.QMainWindow):
 
         ### Tool ###
         self.menutool = self.menu.addMenu('&Tool')
+        self.menutoolshell = self.menutool.addAction(QtGui.QIcon('share/shell16.png'), '&Command Line')
 
         ### Help ###
         self.menuhelp = self.menu.addMenu('&Help')
@@ -106,6 +107,7 @@ class FlatCAMGUI(QtGui.QMainWindow):
         self.clear_plot_btn = self.toolbar.addAction(QtGui.QIcon('share/clear_plot32.png'), "&Clear Plot")
         self.replot_btn = self.toolbar.addAction(QtGui.QIcon('share/replot32.png'), "&Replot")
         self.delete_btn = self.toolbar.addAction(QtGui.QIcon('share/delete32.png'), "&Delete")
+        self.shell_btn = self.toolbar.addAction(QtGui.QIcon('share/shell32.png'), "&Command Line")
 
         ################
         ### Splitter ###
@@ -217,7 +219,7 @@ class FlatCAMGUI(QtGui.QMainWindow):
         self.setWindowIcon(self.app_icon)
 
         self.setGeometry(100, 100, 1024, 650)
-        self.setWindowTitle('FlatCAM - Alpha 6')
+        self.setWindowTitle('FlatCAM - Alpha 7')
         self.show()
 
 

+ 17 - 3
FlatCAMObj.py

@@ -388,9 +388,23 @@ class FlatCAMGerber(FlatCAMObj, Gerber):
 
     def on_iso_button_click(self, *args):
         self.read_form()
-        dia = self.options["isotooldia"]
-        passes = int(self.options["isopasses"])
-        overlap = self.options["isooverlap"] * dia
+        self.isolate()
+
+    def isolate(self, dia=None, passes=None, overlap=None):
+        """
+        Creates an isolation routing geometry object in the project.
+
+        :param dia: Tool diameter
+        :param passes: Number of tool widths to cut
+        :param overlap: Overlap between passes in fraction of tool diameter
+        :return: None
+        """
+        if dia is None:
+            dia = self.options["isotooldia"]
+        if passes is None:
+            passes = int(self.options["isopasses"])
+        if overlap is None:
+            overlap = self.options["isooverlap"] * dia
 
         for i in range(passes):
 

+ 27 - 0
FlatCAMShell.py

@@ -0,0 +1,27 @@
+import sys
+from PyQt4.QtGui import QApplication
+import termwidget
+
+
+class FCShell(termwidget.TermWidget):
+    def __init__(self, sysShell, *args):
+        termwidget.TermWidget.__init__(self, *args)
+        self._sysShell = sysShell
+
+    def is_command_complete(self, text):
+        def skipQuotes(text):
+            quote = text[0]
+            text = text[1:]
+            endIndex = str(text).index(quote)
+            return text[endIndex:]
+        while text:
+            if text[0] in ('"', "'"):
+                try:
+                    text = skipQuotes(text)
+                except ValueError:
+                    return False
+            text = text[1:]
+        return True
+
+    def child_exec_command(self, text):
+        self._sysShell.execCommand(text)

+ 1 - 1
recent.json

@@ -1 +1 @@
-[{"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/not_loaded.gtl"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/diag_1TOP.art"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/22TOP.art"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Top.gbr"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/holes.drl"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/CBS-F_Cu.gtl"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/BLDC2003Through.drl"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/CBS-B_Cu.gbl"}, {"kind": "project", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/gerber_project.fcam"}, {"kind": "project", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/drill_project.fcam"}]
+[{"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/maitest.gtl"}, {"kind": "gerber", "filename": "C:\\Users\\jpcaram\\Dropbox\\CNC\\pcbcam\\test_files\\maitest.gtl"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/CBS-F_Cu.gtl"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/cirkuix/tests/CBS.drl"}, {"kind": "gerber", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Gerbers/AVR_Transistor_Tester_copper_top.GTL"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/LockController_v1.0_pcb-RoundHoles.TXT/LockController_v1.0_pcb-RoundHoles.TXT"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Gerbers/AVR_Transistor_Tester.DRL"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/FlatCam_Drilling_Test/FlatCam_Drilling_Test.drl"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Excellon_Planck/X-Y CONTROLLER - Drill Data - Through Hole.drl"}, {"kind": "excellon", "filename": "C:/Users/jpcaram/Dropbox/CNC/pcbcam/test_files/Scrivania/girasettori_v3.drd"}]

+ 238 - 0
termwidget.py

@@ -0,0 +1,238 @@
+"""
+Terminal emulator widget.
+Shows intput and output text. Allows to enter commands. Supports history.
+"""
+
+import cgi
+
+from PyQt4.QtCore import pyqtSignal
+from PyQt4.QtGui import QColor, QKeySequence, QLineEdit, QPalette, \
+                        QSizePolicy, QTextCursor, QTextEdit, \
+                        QVBoxLayout, QWidget
+
+
+class _ExpandableTextEdit(QTextEdit):
+    """
+    Class implements edit line, which expands themselves automatically
+    """
+
+    historyNext = pyqtSignal()
+    historyPrev = pyqtSignal()
+
+    def __init__(self, termWidget, *args):
+        QTextEdit.__init__(self, *args)
+        self.setStyleSheet("font: 9pt \"Courier\";")
+        self._fittedHeight = 1
+        self.textChanged.connect(self._fit_to_document)
+        self._fit_to_document()
+        self._termWidget = termWidget
+
+    def sizeHint(self):
+        """
+        QWidget sizeHint impelemtation
+        """
+        hint = QTextEdit.sizeHint(self)
+        hint.setHeight(self._fittedHeight)
+        return hint
+
+    def _fit_to_document(self):
+        """
+        Update widget height to fit all text
+        """
+        documentSize = self.document().size().toSize()
+        self._fittedHeight = documentSize.height() + (self.height() - self.viewport().height())
+        self.setMaximumHeight(self._fittedHeight)
+        self.updateGeometry();
+
+    def keyPressEvent(self, event):
+        """
+        Catch keyboard events. Process Enter, Up, Down
+        """
+        if event.matches(QKeySequence.InsertParagraphSeparator):
+            text = self.toPlainText()
+            if self._termWidget.is_command_complete(text):
+                self._termWidget.exec_current_command()
+                return
+        elif event.matches(QKeySequence.MoveToNextLine):
+            text = self.toPlainText()
+            cursorPos = self.textCursor().position()
+            textBeforeEnd = text[cursorPos:]
+            # if len(textBeforeEnd.splitlines()) <= 1:
+            if len(textBeforeEnd.split('\n')) <= 1:
+                self.historyNext.emit()
+                return
+        elif event.matches(QKeySequence.MoveToPreviousLine):
+            text = self.toPlainText()
+            cursorPos = self.textCursor().position()
+            textBeforeStart = text[:cursorPos]
+            # lineCount = len(textBeforeStart.splitlines())
+            lineCount = len(textBeforeStart.split('\n'))
+            if len(textBeforeStart) > 0 and \
+                    (textBeforeStart[-1] == '\n' or textBeforeStart[-1] == '\r'):
+                lineCount += 1
+            if lineCount <= 1:
+                self.historyPrev.emit()
+                return
+        elif event.matches(QKeySequence.MoveToNextPage) or \
+             event.matches(QKeySequence.MoveToPreviousPage):
+            return self._termWidget.browser().keyPressEvent(event)
+
+        QTextEdit.keyPressEvent(self, event)
+
+
+class TermWidget(QWidget):
+    """
+    Widget wich represents terminal. It only displays text and allows to enter text.
+    All highlevel logic should be implemented by client classes
+
+    User pressed Enter. Client class should decide, if command must be executed or user may continue edit it
+    """
+
+    def __init__(self, *args):
+        QWidget.__init__(self, *args)
+
+        self._browser = QTextEdit(self)
+        self._browser.setStyleSheet("font: 9pt \"Courier\";")
+        self._browser.setReadOnly(True)
+        self._browser.document().setDefaultStyleSheet(self._browser.document().defaultStyleSheet() +
+                                                      "span {white-space:pre;}")
+
+        self._edit = _ExpandableTextEdit(self, self)
+        self._edit.historyNext.connect(self._on_history_next)
+        self._edit.historyPrev.connect(self._on_history_prev)
+        self.setFocusProxy(self._edit)
+
+        layout = QVBoxLayout(self)
+        layout.setSpacing(0)
+        layout.setContentsMargins(0, 0, 0, 0)
+        layout.addWidget(self._browser)
+        layout.addWidget(self._edit)
+
+        self._history = ['']  # current empty line
+        self._historyIndex = 0
+
+        self._edit.setFocus()
+
+    def _append_to_browser(self, style, text):
+        """
+        Convert text to HTML for inserting it to browser
+        """
+        assert style in ('in', 'out', 'err')
+
+        text = cgi.escape(text)
+
+        text = text.replace('\n', '<br/>')
+
+        if style != 'out':
+            def_bg = self._browser.palette().color(QPalette.Base)
+            h, s, v, a = def_bg.getHsvF()
+
+            if style == 'in':
+                if v > 0.5:  # white background
+                    v = v - (v / 8)  # make darker
+                else:
+                    v = v + ((1 - v) / 4)  # make ligher
+            else:  # err
+                if v < 0.5:
+                    v = v + ((1 - v) / 4)  # make ligher
+
+                if h == -1:  # make red
+                    h = 0
+                    s = .4
+                else:
+                    h = h + ((1 - h) * 0.5)  # make more red
+
+            bg = QColor.fromHsvF(h, s, v).name()
+            text = '<span style="background-color: %s; font-weight: bold;">%s</span>' % (str(bg), text)
+        else:
+            text = '<span>%s</span>' % text  # without span <br/> is ignored!!!
+
+        scrollbar = self._browser.verticalScrollBar()
+        old_value = scrollbar.value()
+        scrollattheend = old_value == scrollbar.maximum()
+
+        self._browser.moveCursor(QTextCursor.End)
+        self._browser.insertHtml(text)
+
+        """TODO When user enters second line to the input, and input is resized, scrollbar changes its positon
+        and stops moving. As quick fix of this problem, now we always scroll down when add new text.
+        To fix it correctly, srcoll to the bottom, if before intput has been resized,
+        scrollbar was in the bottom, and remove next lien
+        """
+        scrollattheend = True
+
+        if scrollattheend:
+            scrollbar.setValue(scrollbar.maximum())
+        else:
+            scrollbar.setValue(old_value)
+
+    def exec_current_command(self):
+        """
+        Save current command in the history. Append it to the log. Clear edit line
+        Reimplement in the child classes to actually execute command
+        """
+        text = self._edit.toPlainText()
+        self._append_to_browser('in', '> ' + text + '\n')
+
+        if len(self._history) < 2 or\
+           self._history[-2] != text:  # don't insert duplicating items
+            self._history.insert(-1, text)
+
+        self._historyIndex = len(self._history) - 1
+
+        self._history[-1] = ''
+        self._edit.clear()
+
+        if not text[-1] == '\n':
+            text += '\n'
+
+        self.child_exec_command(text)
+
+    def child_exec_command(self, text):
+        """
+        Reimplement in the child classes
+        """
+        pass
+
+    def add_line_break_to_input(self):
+        self._edit.textCursor().insertText('\n')
+
+    def append_output(self, text):
+        """Appent text to output widget
+        """
+        self._append_to_browser('out', text)
+
+    def append_error(self, text):
+        """Appent error text to output widget. Text is drawn with red background
+        """
+        self._append_to_browser('err', text)
+
+    def is_command_complete(self, text):
+        """
+        Executed by _ExpandableTextEdit. Reimplement this function in the child classes.
+        """
+        return True
+
+    def browser(self):
+        return self._browser
+
+    def _on_history_next(self):
+        """
+        Down pressed, show next item from the history
+        """
+        if (self._historyIndex + 1) < len(self._history):
+            self._historyIndex += 1
+            self._edit.setPlainText(self._history[self._historyIndex])
+            self._edit.moveCursor(QTextCursor.End)
+
+    def _on_history_prev(self):
+        """
+        Up pressed, show previous item from the history
+        """
+        if self._historyIndex > 0:
+            if self._historyIndex == (len(self._history) - 1):
+                self._history[-1] = self._edit.toPlainText()
+            self._historyIndex -= 1
+            self._edit.setPlainText(self._history[self._historyIndex])
+            self._edit.moveCursor(QTextCursor.End)
+