Просмотр исходного кода

- refactored some of the code in the App class and created a new Tcl Command named Help

Marius Stanciu 5 лет назад
Родитель
Сommit
a1499158c2

+ 1 - 0
CHANGELOG.md

@@ -12,6 +12,7 @@ CHANGELOG for FlatCAM beta
 - added a new feature, project auto-saving controlled from Edit -> Preferences -> General -> APP. Preferences -> Enable Auto Save checkbox
 - added a new feature, project auto-saving controlled from Edit -> Preferences -> General -> APP. Preferences -> Enable Auto Save checkbox
 - fixed some bugs in the Tcl Commands
 - fixed some bugs in the Tcl Commands
 - modified the Tcl Commands to be able to use as boolean values keywords with lower case like 'false' instead of expected 'False'
 - modified the Tcl Commands to be able to use as boolean values keywords with lower case like 'false' instead of expected 'False'
+- refactored some of the code in the App class and created a new Tcl Command named Help
 
 
 20.04.2020
 20.04.2020
 
 

+ 391 - 534
FlatCAMApp.py

@@ -2556,6 +2556,9 @@ class App(QtCore.QObject):
         # this will hold the TCL instance
         # this will hold the TCL instance
         self.tcl = None
         self.tcl = None
 
 
+        # the actual variable will be redeclared in setup_tcl()
+        self.tcl_commands_storage = None
+
         self.init_tcl()
         self.init_tcl()
 
 
         self.shell = FCShell(self, version=self.version)
         self.shell = FCShell(self, version=self.version)
@@ -2566,14 +2569,7 @@ class App(QtCore.QObject):
         self.shell.append_output("FlatCAM %s - " % self.version)
         self.shell.append_output("FlatCAM %s - " % self.version)
         self.shell.append_output(_("Type >help< to get started\n\n"))
         self.shell.append_output(_("Type >help< to get started\n\n"))
 
 
-        self.ui.shell_dock = QtWidgets.QDockWidget("FlatCAM TCL Shell")
-        self.ui.shell_dock.setObjectName('Shell_DockWidget')
         self.ui.shell_dock.setWidget(self.shell)
         self.ui.shell_dock.setWidget(self.shell)
-        self.ui.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas)
-        self.ui.shell_dock.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
-                                       QtWidgets.QDockWidget.DockWidgetFloatable |
-                                       QtWidgets.QDockWidget.DockWidgetClosable)
-        self.ui.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.ui.shell_dock)
 
 
         # show TCL shell at start-up based on the Menu -? Edit -> Preferences setting.
         # show TCL shell at start-up based on the Menu -? Edit -> Preferences setting.
         if self.defaults["global_shell_at_startup"]:
         if self.defaults["global_shell_at_startup"]:
@@ -2883,7 +2879,7 @@ class App(QtCore.QObject):
                     # if there are Windows paths then replace the path separator with a Unix like one
                     # if there are Windows paths then replace the path separator with a Unix like one
                     if sys.platform == 'win32':
                     if sys.platform == 'win32':
                         command_tcl_formatted = command_tcl_formatted.replace('\\', '/')
                         command_tcl_formatted = command_tcl_formatted.replace('\\', '/')
-                    self.shell._sysShell.exec_command(command_tcl_formatted, no_echo=True)
+                    self.shell.exec_command(command_tcl_formatted, no_echo=True)
             except Exception as ext:
             except Exception as ext:
                 print("ERROR: ", ext)
                 print("ERROR: ", ext)
                 sys.exit(2)
                 sys.exit(2)
@@ -2903,9 +2899,9 @@ class App(QtCore.QObject):
                     #                             color=QtGui.QColor("gray"))
                     #                             color=QtGui.QColor("gray"))
                     cmd_line_shellfile_text = myfile.read()
                     cmd_line_shellfile_text = myfile.read()
                     if self.cmd_line_headless != 1:
                     if self.cmd_line_headless != 1:
-                        self.shell._sysShell.exec_command(cmd_line_shellfile_text)
+                        self.shell.exec_command(cmd_line_shellfile_text)
                     else:
                     else:
-                        self.shell._sysShell.exec_command(cmd_line_shellfile_text, no_echo=True)
+                        self.shell.exec_command(cmd_line_shellfile_text, no_echo=True)
 
 
             except Exception as ext:
             except Exception as ext:
                 print("ERROR: ", ext)
                 print("ERROR: ", ext)
@@ -3703,209 +3699,6 @@ class App(QtCore.QObject):
         else:
         else:
             self.defaults['global_stats'][resource] = 1
             self.defaults['global_stats'][resource] = 1
 
 
-    def init_tcl(self):
-        """
-        Initialize the TCL Shell. A dock widget that holds the GUI interface to the FlatCAM command line.
-        :return: None
-        """
-        if hasattr(self, 'tcl') and self.tcl is not None:
-            # self.tcl = None
-            # TODO  we need  to clean  non default variables and procedures here
-            # new object cannot be used here as it  will not remember values created for next passes,
-            # because tcl  was execudted in old instance of TCL
-            pass
-        else:
-            self.tcl = tk.Tcl()
-            self.setup_shell()
-        self.log.debug("TCL Shell has been initialized.")
-
-    # TODO: This shouldn't be here.
-    class TclErrorException(Exception):
-        """
-        this exception is defined here, to be able catch it if we successfully handle all errors from shell command
-        """
-        pass
-
-    def shell_message(self, msg, show=False, error=False, warning=False, success=False, selected=False):
-        """
-        Shows a message on the FlatCAM Shell
-
-        :param msg: Message to display.
-        :param show: Opens the shell.
-        :param error: Shows the message as an error.
-        :param warning: Shows the message as an warning.
-        :param success: Shows the message as an success.
-        :param selected: Indicate that something was selected on canvas
-        :return: None
-        """
-        if show:
-            self.ui.shell_dock.show()
-        try:
-            if error:
-                self.shell.append_error(msg + "\n")
-            elif warning:
-                self.shell.append_warning(msg + "\n")
-            elif success:
-                self.shell.append_success(msg + "\n")
-            elif selected:
-                self.shell.append_selected(msg + "\n")
-            else:
-                self.shell.append_output(msg + "\n")
-        except AttributeError:
-            log.debug("shell_message() is called before Shell Class is instantiated. The message is: %s", str(msg))
-
-    def raise_tcl_unknown_error(self, unknownException):
-        """
-        Raise exception if is different type than TclErrorException
-        this is here mainly to show unknown errors inside TCL shell console.
-
-        :param unknownException:
-        :return:
-        """
-
-        if not isinstance(unknownException, self.TclErrorException):
-            self.raise_tcl_error("Unknown error: %s" % str(unknownException))
-        else:
-            raise unknownException
-
-    def display_tcl_error(self, error, error_info=None):
-        """
-        Escape bracket [ with '\' otherwise there is error
-        "ERROR: missing close-bracket" instead of real error
-
-        :param error: it may be text  or exception
-        :param error_info: Some informations about the error
-        :return: None
-        """
-
-        if isinstance(error, Exception):
-            exc_type, exc_value, exc_traceback = error_info
-            if not isinstance(error, self.TclErrorException):
-                show_trace = 1
-            else:
-                show_trace = int(self.defaults['global_verbose_error_level'])
-
-            if show_trace > 0:
-                trc = traceback.format_list(traceback.extract_tb(exc_traceback))
-                trc_formated = []
-                for a in reversed(trc):
-                    trc_formated.append(a.replace("    ", " > ").replace("\n", ""))
-                text = "%s\nPython traceback: %s\n%s" % (exc_value, exc_type, "\n".join(trc_formated))
-            else:
-                text = "%s" % error
-        else:
-            text = error
-
-        text = text.replace('[', '\\[').replace('"', '\\"')
-        self.tcl.eval('return -code error "%s"' % text)
-
-    def raise_tcl_error(self, text):
-        """
-        This method  pass exception from python into TCL as error, so we get stacktrace and reason
-
-        :param text: text of error
-        :return: raise exception
-        """
-
-        self.display_tcl_error(text)
-        raise self.TclErrorException(text)
-
-    def exec_command(self, text, no_echo=False):
-        """
-        Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
-        Also handles execution in separated threads
-
-        :param text: FlatCAM TclCommand with parameters
-        :param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it
-        will create crashes of the _Expandable_Edit widget
-        :return: output if there was any
-        """
-
-        self.report_usage('exec_command')
-
-        result = self.exec_command_test(text, False, no_echo=no_echo)
-
-        # MS: added this method call so the geometry is updated once the TCL command is executed
-        # if no_plot is None:
-        #     self.plot_all()
-
-        return result
-
-    def exec_command_test(self, text, reraise=True, no_echo=False):
-        """
-        Same as exec_command(...) with additional control over  exceptions.
-        Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
-
-        :param text: Input command
-        :param reraise: Re-raise TclError exceptions in Python (mostly for unittests).
-        :param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it
-        will create crashes of the _Expandable_Edit widget
-        :return: Output from the command
-        """
-
-        tcl_command_string = str(text)
-
-        try:
-            if no_echo is False:
-                self.shell.open_processing()  # Disables input box.
-
-            result = self.tcl.eval(str(tcl_command_string))
-            if result != 'None' and no_echo is False:
-                self.shell.append_output(result + '\n')
-
-        except tk.TclError as e:
-
-            # This will display more precise answer if something in TCL shell fails
-            result = self.tcl.eval("set errorInfo")
-            self.log.error("Exec command Exception: %s" % (result + '\n'))
-            if no_echo is False:
-                self.shell.append_error('ERROR: ' + result + '\n')
-            # Show error in console and just return or in test raise exception
-            if reraise:
-                raise e
-        finally:
-            if no_echo is False:
-                self.shell.close_processing()
-            pass
-        return result
-
-        # """
-        # Code below is unsused. Saved for later.
-        # """
-
-        # 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 Exception as e:
-        #     #self.shell.append_error(''.join(traceback.format_exc()))
-        #     #self.shell.append_error("?\n")
-        #     self.shell.append_error(str(e) + "\n")
-
     def info(self, msg):
     def info(self, msg):
         """
         """
         Informs the user. Normally on the status bar, optionally
         Informs the user. Normally on the status bar, optionally
@@ -10464,9 +10257,9 @@ class App(QtCore.QObject):
                 with open(filename, "r") as tcl_script:
                 with open(filename, "r") as tcl_script:
                     cmd_line_shellfile_content = tcl_script.read()
                     cmd_line_shellfile_content = tcl_script.read()
                     if self.cmd_line_headless != 1:
                     if self.cmd_line_headless != 1:
-                        self.shell._sysShell.exec_command(cmd_line_shellfile_content)
+                        self.shell.exec_command(cmd_line_shellfile_content)
                     else:
                     else:
-                        self.shell._sysShell.exec_command(cmd_line_shellfile_content, no_echo=True)
+                        self.shell.exec_command(cmd_line_shellfile_content, no_echo=True)
 
 
                 if silent is False:
                 if silent is False:
                     self.inform.emit('[success] %s' % _("TCL script file opened in Code Editor and executed."))
                     self.inform.emit('[success] %s' % _("TCL script file opened in Code Editor and executed."))
@@ -11865,341 +11658,116 @@ class App(QtCore.QObject):
         """
         """
         self.ui.progress_bar.setValue(int(percentage))
         self.ui.progress_bar.setValue(int(percentage))
 
 
-    def setup_shell(self):
+    def setup_recent_items(self):
         """
         """
-        Creates shell functions. Runs once at startup.
+        Setup a dictionary with the recent files accessed, organized by type
 
 
-        :return: None
+        :return:
         """
         """
+        # TODO: Move this to constructor
+        icons = {
+            "gerber": self.resource_location + "/flatcam_icon16.png",
+            "excellon": self.resource_location + "/drill16.png",
+            'geometry': self.resource_location + "/geometry16.png",
+            "cncjob": self.resource_location + "/cnc16.png",
+            "script": self.resource_location + "/script_new24.png",
+            "document": self.resource_location + "/notes16_1.png",
+            "project": self.resource_location + "/project16.png",
+            "svg": self.resource_location + "/geometry16.png",
+            "dxf": self.resource_location + "/dxf16.png",
+            "pdf": self.resource_location + "/pdf32.png",
+            "image": self.resource_location + "/image16.png"
 
 
-        self.log.debug("setup_shell()")
+        }
 
 
-        def shelp(p=None):
-            if not p:
-                cmd_enum = _("Available commands:\n")
+        try:
+            image_opener = self.image_tool.import_image
+        except AttributeError:
+            image_opener = None
 
 
-                displayed_text = []
-                try:
-                    # find the maximum length of a command name
-                    max_len = 0
-                    for cmd_name in commands:
-                        curr_len = len(cmd_name)
-                        if curr_len > max_len:
-                            max_len = curr_len
-                    max_tabs = math.ceil(max_len / 8)
-
-                    for cmd_name in sorted(commands):
-                        cmd_description = commands[cmd_name]['description']
-
-                        curr_len = len(cmd_name)
-                        tabs = '\t'
-
-                        # make sure to add the right number of tabs (1 tab = 8 spaces) so all the commands
-                        # descriptions are aligned
-                        if curr_len == max_len:
-                            cmd_line_txt = ' %s%s%s' % (str(cmd_name), tabs, cmd_description)
-                        else:
-                            nr_tabs = 0
+        openers = {
+            'gerber': lambda fname: self.worker_task.emit({'fcn': self.open_gerber, 'params': [fname]}),
+            'excellon': lambda fname: self.worker_task.emit({'fcn': self.open_excellon, 'params': [fname]}),
+            'geometry': lambda fname: self.worker_task.emit({'fcn': self.import_dxf, 'params': [fname]}),
+            'cncjob': lambda fname: self.worker_task.emit({'fcn': self.open_gcode, 'params': [fname]}),
+            "script": lambda fname: self.worker_task.emit({'fcn': self.open_script, 'params': [fname]}),
+            "document": None,
+            'project': self.open_project,
+            'svg': self.import_svg,
+            'dxf': self.import_dxf,
+            'image': image_opener,
+            'pdf': lambda fname: self.worker_task.emit({'fcn': self.pdf_tool.open_pdf, 'params': [fname]})
+        }
 
 
-                            for x in range(max_tabs):
-                                if curr_len <= (x*8):
-                                    nr_tabs += 1
+        # Open recent file for files
+        try:
+            f = open(self.data_path + '/recent.json')
+        except IOError:
+            App.log.error("Failed to load recent item list.")
+            self.inform.emit('[ERROR_NOTCL] %s' %
+                             _("Failed to load recent item list."))
+            return
 
 
-                            # nr_tabs = 2 if curr_len <= 8 else 1
-                            cmd_line_txt = ' %s%s%s' % (str(cmd_name), nr_tabs*tabs, cmd_description)
+        try:
+            self.recent = json.load(f)
+        except json.scanner.JSONDecodeError:
+            App.log.error("Failed to parse recent item list.")
+            self.inform.emit('[ERROR_NOTCL] %s' %
+                             _("Failed to parse recent item list."))
+            f.close()
+            return
+        f.close()
 
 
-                        displayed_text.append(cmd_line_txt)
-                except Exception as err:
-                    log.debug("App.setup_shell.shelp() when run as 'help' --> %s" % str(err))
-                    displayed_text = [' %s' % cmd for cmd in sorted(commands)]
+        # Open recent file for projects
+        try:
+            fp = open(self.data_path + '/recent_projects.json')
+        except IOError:
+            App.log.error("Failed to load recent project item list.")
+            self.inform.emit('[ERROR_NOTCL] %s' %
+                             _("Failed to load recent projects item list."))
+            return
 
 
-                cmd_enum += '\n'.join(displayed_text)
-                cmd_enum += '\n\n%s\n%s' % (_("Type help <command_name> for usage."), _("Example: help open_gerber"))
-                return cmd_enum
+        try:
+            self.recent_projects = json.load(fp)
+        except json.scanner.JSONDecodeError:
+            App.log.error("Failed to parse recent project item list.")
+            self.inform.emit('[ERROR_NOTCL] %s' %
+                             _("Failed to parse recent project item list."))
+            fp.close()
+            return
+        fp.close()
 
 
-            if p not in commands:
-                return "Unknown command: %s" % p
+        # Closure needed to create callbacks in a loop.
+        # Otherwise late binding occurs.
+        def make_callback(func, fname):
+            def opener():
+                func(fname)
+            return opener
 
 
-            return commands[p]["help"]
+        def reset_recent_files():
+            # Reset menu
+            self.ui.recent.clear()
+            self.recent = []
+            try:
+                ff = open(self.data_path + '/recent.json', 'w')
+            except IOError:
+                App.log.error("Failed to open recent items file for writing.")
+                return
 
 
-        # --- Migrated to new architecture ---
-        # def options(name):
-        #     ops = self.collection.get_by_name(str(name)).options
-        #     return '\n'.join(["%s: %s" % (o, ops[o]) for o in ops])
+            json.dump(self.recent, ff)
 
 
-        def h(*args):
-            """
-            Pre-processes arguments to detect '-keyword value' pairs into dictionary
-            and standalone parameters into list.
-            """
+        def reset_recent_projects():
+            # Reset menu
+            self.ui.recent_projects.clear()
+            self.recent_projects = []
 
 
-            kwa = {}
-            a = []
-            n = len(args)
-            name = None
-            for i in range(n):
-                match = re.search(r'^-([a-zA-Z].*)', args[i])
-                if match:
-                    assert name is None
-                    name = match.group(1)
-                    continue
+            try:
+                fp = open(self.data_path + '/recent_projects.json', 'w')
+            except IOError:
+                App.log.error("Failed to open recent projects items file for writing.")
+                return
 
 
-                if name is None:
-                    a.append(args[i])
-                else:
-                    kwa[name] = args[i]
-                    name = None
-
-            return a, kwa
-
-        @contextmanager
-        def wait_signal(signal, timeout=10000):
-            """
-            Block loop until signal emitted, timeout (ms) elapses
-            or unhandled exception happens in a thread.
-
-            :param timeout: time after which the loop is exited
-            :param signal: Signal to wait for.
-            """
-            loop = QtCore.QEventLoop()
-
-            # Normal termination
-            signal.connect(loop.quit)
-
-            # Termination by exception in thread
-            self.thread_exception.connect(loop.quit)
-
-            status = {'timed_out': False}
-
-            def report_quit():
-                status['timed_out'] = True
-                loop.quit()
-
-            yield
-
-            # Temporarily change how exceptions are managed.
-            oeh = sys.excepthook
-            ex = []
-
-            def except_hook(type_, value, traceback_):
-                ex.append(value)
-                oeh(type_, value, traceback_)
-            sys.excepthook = except_hook
-
-            # Terminate on timeout
-            if timeout is not None:
-                QtCore.QTimer.singleShot(timeout, report_quit)
-
-            # # ## Block ## ##
-            loop.exec_()
-
-            # Restore exception management
-            sys.excepthook = oeh
-            if ex:
-                self.raiseTclError(str(ex[0]))
-
-            if status['timed_out']:
-                raise Exception('Timed out!')
-
-        def make_docs():
-            output = ''
-            import collections
-            od = collections.OrderedDict(sorted(commands.items()))
-            for cmd_, val in od.items():
-                output += cmd_ + ' \n' + ''.join(['~'] * len(cmd_)) + '\n'
-
-                t = val['help']
-                usage_i = t.find('>')
-                if usage_i < 0:
-                    expl = t
-                    output += expl + '\n\n'
-                    continue
-
-                expl = t[:usage_i - 1]
-                output += expl + '\n\n'
-
-                end_usage_i = t[usage_i:].find('\n')
-
-                if end_usage_i < 0:
-                    end_usage_i = len(t[usage_i:])
-                    output += '    ' + t[usage_i:] + '\n       No parameters.\n'
-                else:
-                    extras = t[usage_i+end_usage_i+1:]
-                    parts = [s.strip() for s in extras.split('\n')]
-
-                    output += '    ' + t[usage_i:usage_i+end_usage_i] + '\n'
-                    for p in parts:
-                        output += '       ' + p + '\n\n'
-
-            return output
-
-        '''
-            Howto implement TCL shell commands:
-
-            All parameters passed to command should be possible to set as None and test it afterwards.
-            This is because we need to see error caused in tcl,
-            if None value as default parameter is not allowed TCL will return empty error.
-            Use:
-                def mycommand(name=None,...):
-
-            Test it like this:
-            if name is None:
-
-                self.raise_tcl_error('Argument name is missing.')
-
-            When error ocurre, always use raise_tcl_error, never return "sometext" on error,
-            otherwise we will miss it and processing will silently continue.
-            Method raise_tcl_error  pass error into TCL interpreter, then raise python exception,
-            which is catched in exec_command and displayed in TCL shell console with red background.
-            Error in console is displayed  with TCL  trace.
-
-            This behavior works only within main thread,
-            errors with promissed tasks can be catched and detected only with log.
-            TODO: this problem have to be addressed somehow, maybe rewrite promissing to be blocking somehow for
-            TCL shell.
-
-            Kamil's comment: I will rewrite existing TCL commands from time to time to follow this rules.
-
-        '''
-
-        commands = {
-            'help': {
-                'fcn': shelp,
-                'help': _("Shows list of commands."),
-                'description': ''
-            },
-        }
-
-        # Import/overwrite tcl commands as objects of TclCommand descendants
-        # This modifies the variable 'commands'.
-        tclCommands.register_all_commands(self, commands)
-
-        # Add commands to the tcl interpreter
-        for cmd in commands:
-            self.tcl.createcommand(cmd, commands[cmd]['fcn'])
-
-        # Make the tcl puts function return instead of print to stdout
-        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):
-        """
-        Setup a dictionary with the recent files accessed, organized by type
-
-        :return:
-        """
-        # TODO: Move this to constructor
-        icons = {
-            "gerber": self.resource_location + "/flatcam_icon16.png",
-            "excellon": self.resource_location + "/drill16.png",
-            'geometry': self.resource_location + "/geometry16.png",
-            "cncjob": self.resource_location + "/cnc16.png",
-            "script": self.resource_location + "/script_new24.png",
-            "document": self.resource_location + "/notes16_1.png",
-            "project": self.resource_location + "/project16.png",
-            "svg": self.resource_location + "/geometry16.png",
-            "dxf": self.resource_location + "/dxf16.png",
-            "pdf": self.resource_location + "/pdf32.png",
-            "image": self.resource_location + "/image16.png"
-
-        }
-
-        try:
-            image_opener = self.image_tool.import_image
-        except AttributeError:
-            image_opener = None
-
-        openers = {
-            'gerber': lambda fname: self.worker_task.emit({'fcn': self.open_gerber, 'params': [fname]}),
-            'excellon': lambda fname: self.worker_task.emit({'fcn': self.open_excellon, 'params': [fname]}),
-            'geometry': lambda fname: self.worker_task.emit({'fcn': self.import_dxf, 'params': [fname]}),
-            'cncjob': lambda fname: self.worker_task.emit({'fcn': self.open_gcode, 'params': [fname]}),
-            "script": lambda fname: self.worker_task.emit({'fcn': self.open_script, 'params': [fname]}),
-            "document": None,
-            'project': self.open_project,
-            'svg': self.import_svg,
-            'dxf': self.import_dxf,
-            'image': image_opener,
-            'pdf': lambda fname: self.worker_task.emit({'fcn': self.pdf_tool.open_pdf, 'params': [fname]})
-        }
-
-        # Open recent file for files
-        try:
-            f = open(self.data_path + '/recent.json')
-        except IOError:
-            App.log.error("Failed to load recent item list.")
-            self.inform.emit('[ERROR_NOTCL] %s' %
-                             _("Failed to load recent item list."))
-            return
-
-        try:
-            self.recent = json.load(f)
-        except json.scanner.JSONDecodeError:
-            App.log.error("Failed to parse recent item list.")
-            self.inform.emit('[ERROR_NOTCL] %s' %
-                             _("Failed to parse recent item list."))
-            f.close()
-            return
-        f.close()
-
-        # Open recent file for projects
-        try:
-            fp = open(self.data_path + '/recent_projects.json')
-        except IOError:
-            App.log.error("Failed to load recent project item list.")
-            self.inform.emit('[ERROR_NOTCL] %s' %
-                             _("Failed to load recent projects item list."))
-            return
-
-        try:
-            self.recent_projects = json.load(fp)
-        except json.scanner.JSONDecodeError:
-            App.log.error("Failed to parse recent project item list.")
-            self.inform.emit('[ERROR_NOTCL] %s' %
-                             _("Failed to parse recent project item list."))
-            fp.close()
-            return
-        fp.close()
-
-        # Closure needed to create callbacks in a loop.
-        # Otherwise late binding occurs.
-        def make_callback(func, fname):
-            def opener():
-                func(fname)
-            return opener
-
-        def reset_recent_files():
-            # Reset menu
-            self.ui.recent.clear()
-            self.recent = []
-            try:
-                ff = open(self.data_path + '/recent.json', 'w')
-            except IOError:
-                App.log.error("Failed to open recent items file for writing.")
-                return
-
-            json.dump(self.recent, ff)
-
-        def reset_recent_projects():
-            # Reset menu
-            self.ui.recent_projects.clear()
-            self.recent_projects = []
-
-            try:
-                fp = open(self.data_path + '/recent_projects.json', 'w')
-            except IOError:
-                App.log.error("Failed to open recent projects items file for writing.")
-                return
-
-            json.dump(self.recent, fp)
+            json.dump(self.recent, fp)
 
 
         # Reset menu
         # Reset menu
         self.ui.recent.clear()
         self.ui.recent.clear()
@@ -13040,6 +12608,295 @@ class App(QtCore.QObject):
         self.options.update(self.defaults)
         self.options.update(self.defaults)
         # self.options_write_form()
         # self.options_write_form()
 
 
+    def init_tcl(self):
+        """
+        Initialize the TCL Shell. A dock widget that holds the GUI interface to the FlatCAM command line.
+        :return: None
+        """
+        if hasattr(self, 'tcl') and self.tcl is not None:
+            # self.tcl = None
+            # TODO  we need  to clean  non default variables and procedures here
+            # new object cannot be used here as it  will not remember values created for next passes,
+            # because tcl  was execudted in old instance of TCL
+            pass
+        else:
+            self.tcl = tk.Tcl()
+            self.setup_shell()
+        self.log.debug("TCL Shell has been initialized.")
+
+    def setup_shell(self):
+        """
+        Creates shell functions. Runs once at startup.
+
+        :return: None
+        """
+
+        self.log.debug("setup_shell()")
+
+        def shelp(p=None):
+            pass
+
+        # --- Migrated to new architecture ---
+        # 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 h(*args):
+            """
+            Pre-processes arguments to detect '-keyword value' pairs into dictionary
+            and standalone parameters into list.
+            """
+
+            kwa = {}
+            a = []
+            n = len(args)
+            name = None
+            for i in range(n):
+                match = re.search(r'^-([a-zA-Z].*)', args[i])
+                if match:
+                    assert name is None
+                    name = match.group(1)
+                    continue
+
+                if name is None:
+                    a.append(args[i])
+                else:
+                    kwa[name] = args[i]
+                    name = None
+
+            return a, kwa
+
+        @contextmanager
+        def wait_signal(signal, timeout=10000):
+            """
+            Block loop until signal emitted, timeout (ms) elapses
+            or unhandled exception happens in a thread.
+
+            :param timeout: time after which the loop is exited
+            :param signal: Signal to wait for.
+            """
+            loop = QtCore.QEventLoop()
+
+            # Normal termination
+            signal.connect(loop.quit)
+
+            # Termination by exception in thread
+            self.thread_exception.connect(loop.quit)
+
+            status = {'timed_out': False}
+
+            def report_quit():
+                status['timed_out'] = True
+                loop.quit()
+
+            yield
+
+            # Temporarily change how exceptions are managed.
+            oeh = sys.excepthook
+            ex = []
+
+            def except_hook(type_, value, traceback_):
+                ex.append(value)
+                oeh(type_, value, traceback_)
+
+            sys.excepthook = except_hook
+
+            # Terminate on timeout
+            if timeout is not None:
+                QtCore.QTimer.singleShot(timeout, report_quit)
+
+            # # ## Block ## ##
+            loop.exec_()
+
+            # Restore exception management
+            sys.excepthook = oeh
+            if ex:
+                self.raise_tcl_error(str(ex[0]))
+
+            if status['timed_out']:
+                raise Exception('Timed out!')
+
+        def make_docs():
+            output = ''
+            import collections
+            od = collections.OrderedDict(sorted(self.tcl_commands_storage.items()))
+            for cmd_, val in od.items():
+                output += cmd_ + ' \n' + ''.join(['~'] * len(cmd_)) + '\n'
+
+                t = val['help']
+                usage_i = t.find('>')
+                if usage_i < 0:
+                    expl = t
+                    output += expl + '\n\n'
+                    continue
+
+                expl = t[:usage_i - 1]
+                output += expl + '\n\n'
+
+                end_usage_i = t[usage_i:].find('\n')
+
+                if end_usage_i < 0:
+                    end_usage_i = len(t[usage_i:])
+                    output += '    ' + t[usage_i:] + '\n       No parameters.\n'
+                else:
+                    extras = t[usage_i + end_usage_i + 1:]
+                    parts = [s.strip() for s in extras.split('\n')]
+
+                    output += '    ' + t[usage_i:usage_i + end_usage_i] + '\n'
+                    for p in parts:
+                        output += '       ' + p + '\n\n'
+
+            return output
+
+        '''
+            Howto implement TCL shell commands:
+
+            All parameters passed to command should be possible to set as None and test it afterwards.
+            This is because we need to see error caused in tcl,
+            if None value as default parameter is not allowed TCL will return empty error.
+            Use:
+                def mycommand(name=None,...):
+
+            Test it like this:
+            if name is None:
+
+                self.raise_tcl_error('Argument name is missing.')
+
+            When error ocurre, always use raise_tcl_error, never return "sometext" on error,
+            otherwise we will miss it and processing will silently continue.
+            Method raise_tcl_error  pass error into TCL interpreter, then raise python exception,
+            which is catched in exec_command and displayed in TCL shell console with red background.
+            Error in console is displayed  with TCL  trace.
+
+            This behavior works only within main thread,
+            errors with promissed tasks can be catched and detected only with log.
+            TODO: this problem have to be addressed somehow, maybe rewrite promissing to be blocking somehow for
+            TCL shell.
+
+            Kamil's comment: I will rewrite existing TCL commands from time to time to follow this rules.
+
+        '''
+
+        self.tcl_commands_storage = {}
+        # commands = {
+        #     'help': {
+        #         'fcn': shelp,
+        #         'help': _("Shows list of commands."),
+        #         'description': ''
+        #     },
+        # }
+
+        # Import/overwrite tcl commands as objects of TclCommand descendants
+        # This modifies the variable 'commands'.
+        tclCommands.register_all_commands(self, self.tcl_commands_storage)
+
+        # Add commands to the tcl interpreter
+        for cmd in self.tcl_commands_storage:
+            self.tcl.createcommand(cmd, self.tcl_commands_storage[cmd]['fcn'])
+
+        # Make the tcl puts function return instead of print to stdout
+        self.tcl.eval('''
+            rename puts original_puts
+            proc puts {args} {
+                if {[llength $args] == 1} {
+                    return "[lindex $args 0]"
+                } else {
+                    eval original_puts $args
+                }
+            }
+            ''')
+
+    # TODO: This shouldn't be here.
+    class TclErrorException(Exception):
+        """
+        this exception is defined here, to be able catch it if we successfully handle all errors from shell command
+        """
+        pass
+
+    def shell_message(self, msg, show=False, error=False, warning=False, success=False, selected=False):
+        """
+        Shows a message on the FlatCAM Shell
+
+        :param msg: Message to display.
+        :param show: Opens the shell.
+        :param error: Shows the message as an error.
+        :param warning: Shows the message as an warning.
+        :param success: Shows the message as an success.
+        :param selected: Indicate that something was selected on canvas
+        :return: None
+        """
+        if show:
+            self.ui.shell_dock.show()
+        try:
+            if error:
+                self.shell.append_error(msg + "\n")
+            elif warning:
+                self.shell.append_warning(msg + "\n")
+            elif success:
+                self.shell.append_success(msg + "\n")
+            elif selected:
+                self.shell.append_selected(msg + "\n")
+            else:
+                self.shell.append_output(msg + "\n")
+        except AttributeError:
+            log.debug("shell_message() is called before Shell Class is instantiated. The message is: %s", str(msg))
+
+    def raise_tcl_unknown_error(self, unknownException):
+        """
+        Raise exception if is different type than TclErrorException
+        this is here mainly to show unknown errors inside TCL shell console.
+
+        :param unknownException:
+        :return:
+        """
+
+        if not isinstance(unknownException, self.TclErrorException):
+            self.raise_tcl_error("Unknown error: %s" % str(unknownException))
+        else:
+            raise unknownException
+
+    def display_tcl_error(self, error, error_info=None):
+        """
+        Escape bracket [ with '\' otherwise there is error
+        "ERROR: missing close-bracket" instead of real error
+
+        :param error: it may be text  or exception
+        :param error_info: Some informations about the error
+        :return: None
+        """
+
+        if isinstance(error, Exception):
+            exc_type, exc_value, exc_traceback = error_info
+            if not isinstance(error, self.TclErrorException):
+                show_trace = 1
+            else:
+                show_trace = int(self.defaults['global_verbose_error_level'])
+
+            if show_trace > 0:
+                trc = traceback.format_list(traceback.extract_tb(exc_traceback))
+                trc_formated = []
+                for a in reversed(trc):
+                    trc_formated.append(a.replace("    ", " > ").replace("\n", ""))
+                text = "%s\nPython traceback: %s\n%s" % (exc_value, exc_type, "\n".join(trc_formated))
+            else:
+                text = "%s" % error
+        else:
+            text = error
+
+        text = text.replace('[', '\\[').replace('"', '\\"')
+        self.tcl.eval('return -code error "%s"' % text)
+
+    def raise_tcl_error(self, text):
+        """
+        This method  pass exception from python into TCL as error, so we get stacktrace and reason
+
+        :param text: text of error
+        :return: raise exception
+        """
+
+        self.display_tcl_error(text)
+        raise self.TclErrorException(text)
+
 
 
 class ArgsThread(QtCore.QObject):
 class ArgsThread(QtCore.QObject):
     open_signal = pyqtSignal(list)
     open_signal = pyqtSignal(list)

+ 8 - 0
FlatCAMTool.py

@@ -252,3 +252,11 @@ class FlatCAMTool(QtWidgets.QWidget):
                                  (_("Edited value is out of range"), minval, maxval))
                                  (_("Edited value is out of range"), minval, maxval))
         else:
         else:
             self.app.inform.emit('[success] %s' % _("Edited value is within limits."))
             self.app.inform.emit('[success] %s' % _("Edited value is within limits."))
+
+    def sizeHint(self):
+        """
+        I've overloaded this just in case I will need to make changes in the future to enforce dimensions
+        :return:
+        """
+        default_hint_size = super(FlatCAMTool, self).sizeHint()
+        return QtCore.QSize(default_hint_size.width(), default_hint_size.height())

+ 19 - 8
flatcamGUI/FlatCAMGUI.py

@@ -42,13 +42,24 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
 
 
         # Divine icon pack by Ipapun @ finicons.com
         # Divine icon pack by Ipapun @ finicons.com
 
 
-        # ################################## ##
-        # ## BUILDING THE GUI IS DONE HERE # ##
-        # ################################## ##
-
-        # ######### ##
-        # ## Menu # ##
-        # ######### ##
+        # #######################################################################
+        # ############ BUILDING THE GUI IS EXECUTED HERE ########################
+        # #######################################################################
+
+        # #######################################################################
+        # ####################### TCL Shell DOCK ################################
+        # #######################################################################
+        self.shell_dock = QtWidgets.QDockWidget("FlatCAM TCL Shell")
+        self.shell_dock.setObjectName('Shell_DockWidget')
+        self.shell_dock.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas)
+        self.shell_dock.setFeatures(QtWidgets.QDockWidget.DockWidgetMovable |
+                                       QtWidgets.QDockWidget.DockWidgetFloatable |
+                                       QtWidgets.QDockWidget.DockWidgetClosable)
+        self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, self.shell_dock)
+
+        # #######################################################################
+        # ###################### Menu BUILDING ##################################
+        # #######################################################################
         self.menu = self.menuBar()
         self.menu = self.menuBar()
 
 
         self.menu_toggle_nb = QtWidgets.QAction(
         self.menu_toggle_nb = QtWidgets.QAction(
@@ -735,7 +746,7 @@ class FlatCAMGUI(QtWidgets.QMainWindow):
         # ########################################################################
         # ########################################################################
 
 
         # IMPORTANT #
         # IMPORTANT #
-        # The order: SPITTER -> NOTEBOOK -> SNAP TOOLBAR is important and without it the GUI will not be initialized as
+        # The order: SPLITTER -> NOTEBOOK -> SNAP TOOLBAR is important and without it the GUI will not be initialized as
         # desired.
         # desired.
         self.splitter = QtWidgets.QSplitter()
         self.splitter = QtWidgets.QSplitter()
         self.setCentralWidget(self.splitter)
         self.setCentralWidget(self.splitter)

+ 1 - 1
flatcamTools/ToolDblSided.py

@@ -159,7 +159,7 @@ class DblSidedTool(FlatCAMTool):
         self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Mirror Parameters"))
         self.param_label = QtWidgets.QLabel("<b>%s:</b>" % _("Mirror Parameters"))
         self.param_label.setToolTip('%s.' % _("Parameters for the mirror operation"))
         self.param_label.setToolTip('%s.' % _("Parameters for the mirror operation"))
 
 
-        grid_lay1.addWidget(self.param_label, 0, 0, 1, 3)
+        grid_lay1.addWidget(self.param_label, 0, 0, 1, 2)
 
 
         # ## Axis
         # ## Axis
         self.mirax_label = QtWidgets.QLabel('%s:' % _("Mirror Axis"))
         self.mirax_label = QtWidgets.QLabel('%s:' % _("Mirror Axis"))

+ 99 - 1
flatcamTools/ToolShell.py

@@ -14,6 +14,8 @@ from flatcamGUI.GUIElements import _BrowserTextEdit, _ExpandableTextEdit
 import html
 import html
 import sys
 import sys
 
 
+import tkinter as tk
+
 import gettext
 import gettext
 import FlatCAMTranslation as fcTranslate
 import FlatCAMTranslation as fcTranslate
 import builtins
 import builtins
@@ -227,6 +229,12 @@ class TermWidget(QWidget):
 
 
 class FCShell(TermWidget):
 class FCShell(TermWidget):
     def __init__(self, sysShell, version, *args):
     def __init__(self, sysShell, version, *args):
+        """
+
+        :param sysShell:    When instantiated the sysShell will be actually the FlatCAMApp.App() class
+        :param version:     FlatCAM version string
+        :param args:        Parameters passed to the TermWidget parent class
+        """
         TermWidget.__init__(self, version, *args)
         TermWidget.__init__(self, version, *args)
         self._sysShell = sysShell
         self._sysShell = sysShell
 
 
@@ -246,4 +254,94 @@ class FCShell(TermWidget):
         return True
         return True
 
 
     def child_exec_command(self, text):
     def child_exec_command(self, text):
-        self._sysShell.exec_command(text)
+        self.exec_command(text)
+
+    def exec_command(self, text, no_echo=False):
+        """
+        Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
+        Also handles execution in separated threads
+
+        :param text: FlatCAM TclCommand with parameters
+        :param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it
+        will create crashes of the _Expandable_Edit widget
+        :return: output if there was any
+        """
+
+        self._sysShell.report_usage('exec_command')
+
+        return self.exec_command_test(text, False, no_echo=no_echo)
+
+    def exec_command_test(self, text, reraise=True, no_echo=False):
+        """
+        Same as exec_command(...) with additional control over  exceptions.
+        Handles input from the shell. See FlatCAMApp.setup_shell for shell commands.
+
+        :param text: Input command
+        :param reraise: Re-raise TclError exceptions in Python (mostly for unittests).
+        :param no_echo: If True it will not try to print to the Shell because most likely the shell is hidden and it
+        will create crashes of the _Expandable_Edit widget
+        :return: Output from the command
+        """
+
+        tcl_command_string = str(text)
+
+        try:
+            if no_echo is False:
+                self.open_processing()  # Disables input box.
+
+            result = self._sysShell.tcl.eval(str(tcl_command_string))
+            if result != 'None' and no_echo is False:
+                self.append_output(result + '\n')
+
+        except tk.TclError as e:
+
+            # This will display more precise answer if something in TCL shell fails
+            result = self._sysShell.tcl.eval("set errorInfo")
+            self._sysShell.log.error("Exec command Exception: %s" % (result + '\n'))
+            if no_echo is False:
+                self.append_error('ERROR: ' + result + '\n')
+            # Show error in console and just return or in test raise exception
+            if reraise:
+                raise e
+        finally:
+            if no_echo is False:
+                self.close_processing()
+            pass
+        return result
+
+        # """
+        # Code below is unsused. Saved for later.
+        # """
+
+        # 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 Exception as e:
+        #     #self.shell.append_error(''.join(traceback.format_exc()))
+        #     #self.shell.append_error("?\n")
+        #     self.shell.append_error(str(e) + "\n")

+ 6 - 8
flatcamTools/ToolTransform.py

@@ -33,8 +33,6 @@ class ToolTransform(FlatCAMTool):
         FlatCAMTool.__init__(self, app)
         FlatCAMTool.__init__(self, app)
         self.decimals = self.app.decimals
         self.decimals = self.app.decimals
 
 
-        self.transform_lay = QtWidgets.QVBoxLayout()
-        self.layout.addLayout(self.transform_lay)
         # ## Title
         # ## Title
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label = QtWidgets.QLabel("%s" % self.toolName)
         title_label.setStyleSheet("""
         title_label.setStyleSheet("""
@@ -44,12 +42,12 @@ class ToolTransform(FlatCAMTool):
                             font-weight: bold;
                             font-weight: bold;
                         }
                         }
                         """)
                         """)
-        self.transform_lay.addWidget(title_label)
-        self.transform_lay.addWidget(QtWidgets.QLabel(''))
+        self.layout.addWidget(title_label)
+        self.layout.addWidget(QtWidgets.QLabel(''))
 
 
         # ## Layout
         # ## Layout
         grid0 = QtWidgets.QGridLayout()
         grid0 = QtWidgets.QGridLayout()
-        self.transform_lay.addLayout(grid0)
+        self.layout.addLayout(grid0)
         grid0.setColumnStretch(0, 0)
         grid0.setColumnStretch(0, 0)
         grid0.setColumnStretch(1, 1)
         grid0.setColumnStretch(1, 1)
         grid0.setColumnStretch(2, 0)
         grid0.setColumnStretch(2, 0)
@@ -206,7 +204,7 @@ class ToolTransform(FlatCAMTool):
         self.ois_scale = OptionalInputSection(self.scale_link_cb, [self.scaley_entry, self.scaley_button], logic=False)
         self.ois_scale = OptionalInputSection(self.scale_link_cb, [self.scaley_entry, self.scaley_button], logic=False)
 
 
         grid0.addWidget(self.scale_link_cb, 10, 0)
         grid0.addWidget(self.scale_link_cb, 10, 0)
-        grid0.addWidget(self.scale_zero_ref_cb, 10, 1)
+        grid0.addWidget(self.scale_zero_ref_cb, 10, 1, 1, 2)
 
 
         separator_line = QtWidgets.QFrame()
         separator_line = QtWidgets.QFrame()
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
         separator_line.setFrameShape(QtWidgets.QFrame.HLine)
@@ -395,7 +393,7 @@ class ToolTransform(FlatCAMTool):
 
 
         grid0.addWidget(QtWidgets.QLabel(''), 26, 0, 1, 3)
         grid0.addWidget(QtWidgets.QLabel(''), 26, 0, 1, 3)
 
 
-        self.transform_lay.addStretch()
+        self.layout.addStretch()
 
 
         # ## Reset Tool
         # ## Reset Tool
         self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
         self.reset_button = QtWidgets.QPushButton(_("Reset Tool"))
@@ -408,7 +406,7 @@ class ToolTransform(FlatCAMTool):
                             font-weight: bold;
                             font-weight: bold;
                         }
                         }
                         """)
                         """)
-        self.transform_lay.addWidget(self.reset_button)
+        self.layout.addWidget(self.reset_button)
 
 
         # ## Signals
         # ## Signals
         self.rotate_button.clicked.connect(self.on_rotate)
         self.rotate_button.clicked.connect(self.on_rotate)

+ 1 - 2
tclCommands/TclCommand.py

@@ -3,8 +3,7 @@ import re
 import FlatCAMApp
 import FlatCAMApp
 import abc
 import abc
 import collections
 import collections
-from PyQt5 import QtCore, QtGui
-from PyQt5.QtCore import Qt
+from PyQt5 import QtCore
 from contextlib import contextmanager
 from contextlib import contextmanager
 
 
 
 

+ 119 - 0
tclCommands/TclCommandHelp.py

@@ -0,0 +1,119 @@
+# ##########################################################
+# FlatCAM: 2D Post-processing for Manufacturing            #
+# File Author: Marius Adrian Stanciu (c)                   #
+# Content was borrowed from FlatCAM proper                 #
+# Date: 4/22/2020                                          #
+# MIT Licence                                              #
+# ##########################################################
+
+from tclCommands.TclCommand import TclCommand
+
+import collections
+import math
+
+import gettext
+import FlatCAMTranslation as fcTranslate
+import builtins
+
+fcTranslate.apply_language('strings')
+if '_' not in builtins.__dict__:
+    _ = gettext.gettext
+
+
+class TclCommandHelp(TclCommand):
+    """
+    Tcl shell command to get the value of a system variable
+
+    example:
+        get_sys excellon_zeros
+    """
+
+    # List of all command aliases, to be able use old names for backward compatibility (add_poly, add_polygon)
+    aliases = ['help']
+
+    description = '%s %s' % ("--", "PRINTS to TCL the HELP.")
+
+    # Dictionary of types from Tcl command, needs to be ordered
+    arg_names = collections.OrderedDict([
+        ('name', str)
+    ])
+
+    # Dictionary of types from Tcl command, needs to be ordered , this  is  for options  like -optionname value
+    option_types = collections.OrderedDict([
+
+    ])
+
+    # array of mandatory options for current Tcl command: required = {'name','outname'}
+    required = []
+
+    # structured help for current command, args needs to be ordered
+    help = {
+        'main': "Returns to TCL the value for the entered system variable.",
+        'args': collections.OrderedDict([
+            ('name', 'Name of a Tcl Command for which to display the Help.'),
+        ]),
+        'examples': ['help add_circle']
+    }
+
+    def execute(self, args, unnamed_args):
+        """
+
+        :param args:
+        :param unnamed_args:
+        :return:
+        """
+        print(self.app.tcl_commands_storage)
+
+        if 'name' in args:
+            name = args['name']
+            if name not in self.app.tcl_commands_storage:
+                return "Unknown command: %s" % name
+
+            print(self.app.tcl_commands_storage[name]["help"])
+        else:
+            if args is None:
+                cmd_enum = _("Available commands:\n")
+
+                displayed_text = []
+                try:
+                    # find the maximum length of a command name
+                    max_len = 0
+                    for cmd_name in self.app.tcl_commands_storage:
+                        curr_len = len(cmd_name)
+                        if curr_len > max_len:
+                            max_len = curr_len
+                    max_tabs = math.ceil(max_len / 8)
+
+                    for cmd_name in sorted(self.app.tcl_commands_storage):
+                        cmd_description = self.app.tcl_commands_storage[cmd_name]['description']
+
+                        curr_len = len(cmd_name)
+                        tabs = '\t'
+
+                        cmd_name_colored = "<span style=\" color:#ff0000;\" >"
+                        cmd_name_colored += str(cmd_name)
+                        cmd_name_colored += "</span>"
+
+                        # make sure to add the right number of tabs (1 tab = 8 spaces) so all the commands
+                        # descriptions are aligned
+                        if curr_len == max_len:
+                            cmd_line_txt = ' %s%s%s' % (cmd_name_colored, tabs, cmd_description)
+                        else:
+                            nr_tabs = 0
+
+                            for x in range(max_tabs):
+                                if curr_len <= (x * 8):
+                                    nr_tabs += 1
+
+                            # nr_tabs = 2 if curr_len <= 8 else 1
+                            cmd_line_txt = ' %s%s%s' % (cmd_name_colored, nr_tabs * tabs, cmd_description)
+
+                        displayed_text.append(cmd_line_txt)
+                except Exception as err:
+                    self.app.log.debug("App.setup_shell.shelp() when run as 'help' --> %s" % str(err))
+                    displayed_text = [' %s' % cmd for cmd in sorted(self.app.tcl_commands_storage)]
+
+                cmd_enum += '\n'.join(displayed_text)
+                cmd_enum += '\n\n%s\n%s' % (_("Type help <command_name> for usage."), _("Example: help open_gerber"))
+
+                print(cmd_enum)

+ 1 - 0
tclCommands/__init__.py

@@ -93,6 +93,7 @@ def register_all_commands(app, commands):
     tcl_modules = {k: v for k, v in list(sys.modules.items()) if k.startswith('tclCommands.TclCommand')}
     tcl_modules = {k: v for k, v in list(sys.modules.items()) if k.startswith('tclCommands.TclCommand')}
 
 
     for key, mod in list(tcl_modules.items()):
     for key, mod in list(tcl_modules.items()):
+        print(key)
         if key != 'tclCommands.TclCommand':
         if key != 'tclCommands.TclCommand':
             class_name = key.split('.')[1]
             class_name = key.split('.')[1]
             class_type = getattr(mod, class_name)
             class_type = getattr(mod, class_name)