From 0f163a66a9764ea502f99a8f7e08e3558b088af3 Mon Sep 17 00:00:00 2001 From: Ircama Date: Sun, 10 Nov 2024 13:15:29 +0100 Subject: [PATCH 01/11] Use ircama/just-the-docs. Add ET-4800 printer configuration --- README.md | 64 ++++++++++++++--------------- _config.yml | 99 ++++++++++++++++++++++++++++++++++++++++----- epson_print_conf.py | 29 +++++++++++++ 3 files changed, 149 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 08b6096..3b5b31e 100644 --- a/README.md +++ b/README.md @@ -4,46 +4,44 @@ Epson Printer Configuration tool via SNMP (TCP/IP) ## Product Overview -The *Epson Printer Configuration Tool* simplifies the management of Epson printers connected via Wi-Fi over the SNMP protocol. +The Epson Printer Configuration Tool provides an interface for the configuration and monitoring of Epson printers connected via Wi-Fi using the SNMP protocol. A range of features are offered for both end-users and developers. -A range of features are offered for both end-users and developers, making it easier to administer and maintain Epson printers. - -The software provides a configurable printer dictionary, which can be easily extended. In addition, it is possible to import and convert external Epson printer configuration databases. +The software also includes a configurable printer dictionary, which can be easily extended. In addition, it is possible to import and convert external Epson printer configuration databases. ## Key Features -- __SNMP Interface__: Seamlessly connect and manage Epson printers using SNMP over TCP/IP, supporting Wi-Fi connections (not USB). +- __SNMP Interface__: Connect and manage Epson printers using SNMP over TCP/IP, supporting Wi-Fi connections (not USB). - Printers are queried via Simple Network Management Protocol (SNMP) with a set of Object Identifiers (OIDs) used by Epson printers. Some of them are also valid with other printer brands. SNMP is also used to manage the EEPROM and read/set specific Epson configuration. + Printers are queried via Simple Network Management Protocol (SNMP) with a set of Object Identifiers (OIDs) used by Epson printers. Some of them are also valid with other printer brands. SNMP is used to manage the EEPROM and read/set specific Epson configuration. - __Detailed Status Reporting__: Produce a comprehensive printer status report (with options to focus on specific details). Epson printers produce a status response in a proprietary "new binary format" named @BDC ST2, including a data structure which is partially undocumented (such messages - start with `@BDC [SP] ST2 [CR] [LF]` ...). It is used to convey various aspects of the status of the printer, such as errors, paper status, ink and more. The element fields of this format may vary depending on the printer model. The *Epson Printer Configuration Tool* can decode all element fields found in publicly available Epson Programming Manuals of various printer models (a relevant subset of fields used by the Epson printers). + start with `@BDC [SP] ST2 [CR] [LF]` ...). @BDC ST2 is used to convey various aspects of the status of the printer, such as errors, paper status, ink and more. The element fields of this format may vary depending on the printer model. The *Epson Printer Configuration Tool* can decode all element fields found in publicly available Epson Programming Manuals of various printer models (a relevant subset of fields used by the Epson printers). - __Advanced Maintenance Functions__: - Open the Web interface of the printer (via the default browser). - Reset the ink waste counter. - The ink waste counters track the amount of ink discarded during maintenance tasks to prevent overflow in the waste ink pads. Once the counters indicate that one of the printer pads is full, the printer will stop working to avoid potential damage or ink spills. Resetting the ink waste counter extends the printer operation while a pad maintenance or tank replacement is programmed. - - Adjust the power-off timer (for more accurate energy efficiency). + The ink waste counters track the amount of ink discarded during maintenance tasks to prevent overflow in the waste ink pads. Once the counters indicate that one of the printer pads is full, the printer will stop working to avoid potential damage or ink spills. Resetting the ink waste counter extends the printer operation while a pad maintenance or tank replacement is programmed (operation that shall necessarily be pefromed). + - Adjust the power-off timer (for energy efficiency). - Change the _First TI Received Time_, - The *First TI Received Time* in Epson printers typically refers to the timestamp of the first transmission instruction to the printer when it was first set up. This feature tracks when the printer first operated. + The *First TI Received Time* in Epson printers typically refers to the timestamp of the first transmission instruction to the printer. This feature tracks when the printer first operated. - Change the printer WiFi MAC address and the printer serial number (typically used in specialized scenarios where specific device identifiers are required). - - Read and write to EEPROM addresses for advanced configurations. + - Read and write to EEPROM addresses. - Dump and analyze sets of EEPROM addresses. - Detect the access key (*read_key* and *write_key*) and some attributes of the printer configuration. - The GUI includes some features that attempt to detect the attributes of an Epson printer whose model is not included in the configuration, which can also be used with known printers, to detect additional parameters. + The GUI includes some features that attempt to detect the attributes of an Epson printer whose model is not included in the configuration; such features can also be used with known printers, to detect additional parameters. - Import and export printer configuration datasets in various formats: epson_print_conf pickle, Reinkpy XML, Reinkpy TOML. - Access various administrative and debugging options. -- __User-Friendly Interfaces__: - - __Graphical User Interface (GUI)__: Intuitive interface with an autodiscovery function that detects printer IP addresses and model names. +- __Available Interfaces__: + - __Graphical User Interface__: [Tcl/Tk](https://en.wikipedia.org/wiki/Tk_(software)) platform-independent GUI with an autodiscovery function that detects printer IP addresses and model names. - __Command Line Tool__: For users who prefer command-line interactions, providing the full set of features. - __Python API Interface__: For developers to integrate and automate printer management tasks. @@ -81,7 +79,7 @@ Notes (at the time of writing): - [before pysnmp, install pyasn1 with version 0.4.8 and not 0.5](https://github.com/etingof/pysnmp/issues/440#issuecomment-1544341598) - [pull pysnmp from the GitHub master branch, not from PyPI](https://stackoverflow.com/questions/54868134/snmp-reading-from-an-oid-with-three-libraries-gives-different-execution-times#comment96532761_54869361) -This program exploits [pysnmp](https://github.com/etingof/pysnmp), with related [documentation](https://pysnmp.readthedocs.io/). +This program exploits [pysnmp](https://github.com/etingof/pysnmp), basing on the related [documentation](https://pysnmp.readthedocs.io/). It is tested with Ubuntu / Windows Subsystem for Linux, Windows. @@ -130,17 +128,17 @@ With the GUI, the following operations are possible (from the file menu): - Import an XML configuration file or web URL - This option allows to import the XML configuration file downloaded from https://codeberg.org/attachments/147f41a3-a6ea-45f6-8c2a-25bac4495a1d. Alternatively, this option directly accepts the [source Web URL](https://codeberg.org/attachments/147f41a3-a6ea-45f6-8c2a-25bac4495a1d) of this file, incorporating the download operation into the GUI. + This option allows to import the XML configuration file downloaded from . Alternatively, this option directly accepts the [source Web URL](https://codeberg.org/attachments/147f41a3-a6ea-45f6-8c2a-25bac4495a1d) of this file, incorporating the download operation into the GUI. - Import a TOML configuration file or web URL - Similar to the XML import, this option allows to load the TOML configuration file downloaded from https://codeberg.org/atufi/reinkpy/raw/branch/main/reinkpy/epson.toml and also accepts the [source Web URL](https://codeberg.org/atufi/reinkpy/raw/branch/main/reinkpy/epson.toml) of this file, incorporating the download operation into the GUI. + Similar to the XML import, this option allows to load the TOML configuration file downloaded from and also accepts the [source Web URL](https://codeberg.org/atufi/reinkpy/raw/branch/main/reinkpy/epson.toml) of this file, incorporating the download operation into the GUI. Other menu options allow to filter or clean up the configuration list, as well as select a specific printer model and then save data to a PICKLE file. ### How to detect parameters of an unknown printer -First press "Detect Printers". If the printer is not in the configuration, press "Detect Access Keys". If the output does not show errors, press "Detect Configuration". These commands produce a tree view and a text view, which are useful to analyze whether there is a configured model that might be close or possibly same to target one. Notice that these operations take many minutes to complete and the printer shall be kept switched on for the whole period. Temporarily disabling the auto power-off timer is suggested. +First press "Detect Printers". If the printer is not in the configuration, press "Detect Access Keys". If the output does not show errors, press "Detect Configuration". These commands produce a tree view and a text view, which are useful to analyze whether there is a configured model that might be close or possibly same to target one. Use the right key of the mouse to switch between the two views. Notice that these operations take many minutes to complete and the printer shall be kept switched on for the whole period. Temporarily disabling the auto power-off timer is suggested. ### How to revert a change performed through the GUI @@ -277,7 +275,7 @@ This repository includes a Windows *epson_print_conf.exe* executable file which ### parse_devices.py -Within a [report](https://codeberg.org/atufi/reinkpy/issues/12#issue-716809) in repo https://codeberg.org/atufi/reinkpy there is an interesting [attachment](https://codeberg.org/attachments/147f41a3-a6ea-45f6-8c2a-25bac4495a1d) which includes an extensive XML database of Epson model features. +Within a [report](https://codeberg.org/atufi/reinkpy/issues/12#issue-716809) in repo there is an interesting [attachment](https://codeberg.org/attachments/147f41a3-a6ea-45f6-8c2a-25bac4495a1d) which includes an extensive XML database of Epson model features. The program *parse_devices.py* transforms this XML DB into the dictionary that *epson_print_conf.py* can use. It is also able to accept the [TOML](https://toml.io/) input format used by [reinkpy](https://codeberg.org/atufi/reinkpy) in [epson.toml](https://codeberg.org/atufi/reinkpy/src/branch/main/reinkpy/epson.toml), if the `-T` option is used. @@ -694,30 +692,30 @@ snmpget -v1 -d -c public 192.168.1.87 1.3.6.1.4.1.1248.1.2.2.44.1.1.2.1.124.124. ### References -epson-printer-snmp: https://github.com/Zedeldi/epson-printer-snmp (and https://github.com/Zedeldi/epson-printer-snmp/issues/1) +epson-printer-snmp: (and ) -ReInkPy: https://codeberg.org/atufi/reinkpy/ +ReInkPy: -ReInk: https://github.com/lion-simba/reink (especially https://github.com/lion-simba/reink/issues/1) +ReInk: (especially ) -reink-net: https://github.com/gentu/reink-net +reink-net: -epson-l4160-ink-waste-resetter: https://github.com/nicootto/epson-l4160-ink-waste-resetter +epson-l4160-ink-waste-resetter: -epson-l3160-ink-waste-resetter: https://github.com/k3dt/epson-l3160-ink-waste-resetter +epson-l3160-ink-waste-resetter: -emanage x900: https://github.com/abrasive/x900-otsakupuhastajat/ +emanage x900: ### Other programs -- Epson One-Time Maintenance Ink Pad Reset Utility: https://epson.com/Support/wa00369 - - Epson Maintenance Reset Utility: https://epson.com/epsonstorefront/orbeon/fr/us_regular_s03/us_ServiceInk_Pad_Reset/new - - Epson Ink Pads Reset Utility Terms and Conditions: https://epson.com/Support/wa00370 +- Epson One-Time Maintenance Ink Pad Reset Utility: + - Epson Maintenance Reset Utility: + - Epson Ink Pads Reset Utility Terms and Conditions: - Epson Adjustment Program (developed by EPSON) -- WIC-Reset: https://www.wic.support/download/ / https://www.2manuals.com / (Use at your risk) -- PrintHelp: https://printhelp.info/ (Use at your risk) +- WIC-Reset: / (Use at your risk) +- PrintHelp: (Use at your risk) ### Other resources -- https://codeberg.org/attachments/147f41a3-a6ea-45f6-8c2a-25bac4495a1d -- https://codeberg.org/atufi/reinkpy/src/branch/main/reinkpy/epson.toml +- +- diff --git a/_config.yml b/_config.yml index e073309..441855f 100644 --- a/_config.yml +++ b/_config.yml @@ -1,12 +1,11 @@ -remote_theme: bmndc/just-the-docs +remote_theme: Ircama/just-the-docs + +# Enable or disable the site search +# Supports true (default) or false search_enabled: false + +# For copy button on code enable_copy_code_button: true -heading_anchors: true -aux_links: - "epson_print_conf on GitHub": - - "https://github.com/Ircama/epson_print_conf/" -aux_links_new_tab: true -nav_enabled: false # Table of Contents # Enable or disable the Table of Contents globally @@ -26,6 +25,54 @@ toc: # Supports true or false (default) flat_toc: false +# By default, consuming the theme as a gem leaves mermaid disabled; it is opt-in +mermaid: + # Version of mermaid library + # Pick an available version from https://cdn.jsdelivr.net/npm/mermaid/ + version: "9.1.6" + # Put any additional configuration, such as setting the theme, in _includes/mermaid_config.js + # See also docs/ui-components/code + # To load mermaid from a local library, also use the `path` key to specify the location of the library; e.g. + # for (v10+): + # path: "/assets/js/mermaid.esm.min.mjs" + # for ( Date: Sat, 16 Nov 2024 09:33:01 +0100 Subject: [PATCH 02/11] Automatically update version when pushing a tag --- .github/workflows/build.yml | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 860b825..c872484 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,27 @@ jobs: steps: - name: Git Checkout uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 # Fetch all history for accurate commits and tags + + - name: Update VERSION in python file ui.py + $VERSION = "${{ github.ref_name }}" + run: | + $VERSION = "${{ github.ref_name }}" + if ($VERSION.StartsWith('v')) { + $VERSION = $VERSION.Substring(1) + } + (Get-Content ui.py) -replace '^VERSION = ".*"$', "VERSION = `"$VERSION`"" | Set-Content ui.py + + - name: Commit and push version update + run: | + git config --local user.name "github-actions[bot]" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git add ui.py + git commit -m "Update VERSION to ${{ github.ref_name }}" + git remote set-url origin https://Ircama:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} + git push origin main - name: Install Python uses: actions/setup-python@v5 @@ -27,9 +48,7 @@ jobs: pip install -r requirements.txt - name: Run PyInstaller to create epson_print_conf.exe - run: > - python -m PyInstaller epson_print_conf.spec -- --default - --version ${{ github.ref_name }} + run: python -m PyInstaller epson_print_conf.spec -- --default - name: Zip the epson_print_conf.exe asset to epson_print_conf.zip run: | From 4f6728828300962fb7a81244d03784cff23bbd54 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 16 Nov 2024 09:31:12 +0000 Subject: [PATCH 03/11] Update VERSION to v5.2.14 --- .github/workflows/build.yml | 13 +++++-------- ui.py | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c872484..472707c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,22 +15,19 @@ jobs: ref: main fetch-depth: 0 # Fetch all history for accurate commits and tags - - name: Update VERSION in python file ui.py - $VERSION = "${{ github.ref_name }}" + - name: Update VERSION in python file ui.py; commit and push updates run: | + $filePath = "ui.py" $VERSION = "${{ github.ref_name }}" if ($VERSION.StartsWith('v')) { $VERSION = $VERSION.Substring(1) } - (Get-Content ui.py) -replace '^VERSION = ".*"$', "VERSION = `"$VERSION`"" | Set-Content ui.py - - - name: Commit and push version update - run: | + (Get-Content $filePath) -replace '^VERSION = ".*"$', "VERSION = `"$VERSION`"" | Set-Content $filePath git config --local user.name "github-actions[bot]" git config --local user.email "github-actions[bot]@users.noreply.github.com" - git add ui.py + git add $filePath git commit -m "Update VERSION to ${{ github.ref_name }}" - git remote set-url origin https://Ircama:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} + git remote set-url origin https://${GITHUB_USER}:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} git push origin main - name: Install Python diff --git a/ui.py b/ui.py index 28e911d..91d9f8c 100644 --- a/ui.py +++ b/ui.py @@ -32,7 +32,7 @@ from find_printers import PrinterScanner -VERSION = "5.1" +VERSION = "5.2.14" NO_CONF_ERROR = ( "[ERROR] Please select a printer model and a valid IP address," From 1cc622a10deae78b918da380793e40338a577f60 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 16 Nov 2024 09:44:25 +0000 Subject: [PATCH 04/11] Update VERSION to v5.2.16 --- ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui.py b/ui.py index 91d9f8c..85b3d7c 100644 --- a/ui.py +++ b/ui.py @@ -32,7 +32,7 @@ from find_printers import PrinterScanner -VERSION = "5.2.14" +VERSION = "5.2.16" NO_CONF_ERROR = ( "[ERROR] Please select a printer model and a valid IP address," From e4aef680b96400d62cff50688bc1ccef5b8ad17c Mon Sep 17 00:00:00 2001 From: Ircama Date: Sat, 16 Nov 2024 11:20:28 +0100 Subject: [PATCH 05/11] Better debugging --- ui.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ui.py b/ui.py index 85b3d7c..3ade912 100644 --- a/ui.py +++ b/ui.py @@ -1883,7 +1883,7 @@ def printer_config(self, cursor=True): return try: self.text_dump = black.format_str( # used by Copy All - f'"{printer.model}" + " configuration": ' + repr(printer.parm), + f'"{printer.model}": ' + repr(printer.parm), mode=self.mode ) self.show_treeview() @@ -1941,7 +1941,11 @@ def key_values(self, cursor=True): "Hex write sequence": printer.caesar( printer.parm.get("write_key", b''), hex=True - ).upper() + ).upper(), + "OID - Read address 0": + printer.eeprom_oid_read_address(0), + "OID - Write value 0 to address 0": + printer.eeprom_oid_write_address(0, 0), } self.text_dump = black.format_str( # used by Copy All @@ -2046,7 +2050,7 @@ def get_values(addresses): else: self.status_text.insert( tk.END, - f'[ERROR] Cannot read EEPROM values' + f'[ERROR] Cannot read EEPROM values for addresses "{addresses}"' ': invalid printer model selected.\n' ) self.config(cursor="") From 104749b5ce66f983d6d95142cb7e4686a5920b8f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 16 Nov 2024 10:21:31 +0000 Subject: [PATCH 06/11] Update VERSION to v5.2.17 --- ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui.py b/ui.py index 3ade912..d29920a 100644 --- a/ui.py +++ b/ui.py @@ -32,7 +32,7 @@ from find_printers import PrinterScanner -VERSION = "5.2.16" +VERSION = "5.2.17" NO_CONF_ERROR = ( "[ERROR] Please select a printer model and a valid IP address," From 97a10cddcd670a754a09d849b99b43d895c40269 Mon Sep 17 00:00:00 2001 From: Ircama Date: Sun, 17 Nov 2024 07:55:16 +0100 Subject: [PATCH 07/11] Fix bugs, improve prompts and add debug console (F7) ref #42 --- .gitignore | 2 + ui.py | 378 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 372 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 7c02179..984f09a 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,5 @@ devices.xml *.toml *.srs *.pickle + +.python_history diff --git a/ui.py b/ui.py index d29920a..67629fa 100644 --- a/ui.py +++ b/ui.py @@ -18,6 +18,10 @@ import webbrowser import pickle +from code import InteractiveConsole +from contextlib import redirect_stderr, redirect_stdout +from io import StringIO + import black import tkinter as tk from tkinter import ttk, Menu @@ -32,7 +36,7 @@ from find_printers import PrinterScanner -VERSION = "5.2.17" +VERSION = "5.2.18" NO_CONF_ERROR = ( "[ERROR] Please select a printer model and a valid IP address," @@ -48,6 +52,342 @@ ) +class History(list): + def __init__(self, history_file=".python_history"): + super().__init__() + self.history_file = history_file + + # Load history from the file + if os.path.exists(self.history_file): + with open(self.history_file, "r", encoding="utf-8") as file: + self.extend(line.strip() for line in file if line.strip()) + + def __getitem__(self, index): + try: + return super().__getitem__(index) + except IndexError: + return None + + def append(self, item): + # Ensure only strings are added + if isinstance(item, list): + item = " ".join(item) # Convert list to string + super().append(item) + + def save(self): + # Save history back to the file + with open(self.history_file, "w", encoding="utf-8") as file: + file.writelines(item + "\n" for item in self) + + +class TextConsole(tk.Text): + def __init__(self, main, master, **kw): + kw.setdefault('width', 50) + kw.setdefault('wrap', 'word') + kw.setdefault('prompt1', '>>> ') + kw.setdefault('prompt2', '... ') + banner = kw.pop('banner', 'Python %s\n' % sys.version) + self._prompt1 = kw.pop('prompt1') + self._prompt2 = kw.pop('prompt2') + tk.Text.__init__(self, master, **kw) + # --- history + self.history = History() + self._hist_item = 0 + self._hist_match = '' + + # --- initialization + console_locals = { + "self": main, + "master": master, + "kw": kw, + "local": self + } + self._console = InteractiveConsole(locals=console_locals) # python console + self.insert('end', banner, 'banner') + self.prompt() + self.mark_set('input', 'insert') + self.mark_gravity('input', 'left') + + # --- bindings + self.bind('', self.on_ctrl_return) + self.bind('', self.on_shift_return) + self.bind('', self.on_key_press) + self.bind('', self.on_key_release) + self.bind('', self.on_tab) + self.bind('', self.on_down) + self.bind('', self.on_up) + self.bind('', self.on_return) + self.bind('', self.on_backspace) + self.bind('', self.on_ctrl_c) + self.bind('<>', self.on_paste) + + @property + def h(self): + """Override the history property to return the formatted history as a string.""" + self.insert( + 'end', "\n".join( + f"{i + 1}: {command}" for i, command in enumerate(self.history) + ) + '\n' + ) + + def on_ctrl_c(self, event): + """Copy selected code, removing prompts first""" + sel = self.tag_ranges('sel') + if sel: + txt = self.get('sel.first', 'sel.last').splitlines() + lines = [] + for i, line in enumerate(txt): + if line.startswith(self._prompt1): + lines.append(line[len(self._prompt1):]) + elif line.startswith(self._prompt2): + lines.append(line[len(self._prompt2):]) + else: + lines.append(line) + self.clipboard_clear() + self.clipboard_append('\n'.join(lines)) + return 'break' + + def on_paste(self, event): + """Paste commands""" + if self.compare('insert', '<', 'input'): + return "break" + sel = self.tag_ranges('sel') + if sel: + self.delete('sel.first', 'sel.last') + txt = self.clipboard_get() + self.insert("insert", txt) + self.insert_cmd(self.get("input", "end")) + return 'break' + + def prompt(self, result=False): + """Insert a prompt""" + if result: + self.insert('end', self._prompt2, 'prompt') + else: + self.insert('end', self._prompt1, 'prompt') + self.mark_set('input', 'end-1c') + + def on_key_press(self, event): + """Prevent text insertion in command history""" + if self.compare('insert', '<', 'input') and event.keysym not in ['Left', 'Right']: + self._hist_item = len(self.history) + self.mark_set('insert', 'input lineend') + if not event.char.isalnum(): + return 'break' + + def on_key_release(self, event): + """Reset history scrolling""" + if self.compare('insert', '<', 'input') and event.keysym not in ['Left', 'Right']: + self._hist_item = len(self.history) + return 'break' + + def on_up(self, event): + """Handle up arrow key press""" + # Handle cursor position outside the input area + if self.compare('insert', '<', 'input'): + self.mark_set('insert', 'end') + return 'break' + + # Check if at the start of the input line + elif self.index('input linestart') == self.index('insert linestart'): + # Get the current input line for partial matching + line = self.get('input', 'insert') + self._hist_match = line + + # Save the current history index and move one step back + hist_item = self._hist_item + self._hist_item -= 1 + + # Search for a matching history entry + while self._hist_item >= 0: + # Convert the current history item to a string + item = self.history[self._hist_item] + if item.startswith(line): # Match the current input + break + self._hist_item -= 1 + + if self._hist_item >= 0: + # Found a match: insert the command + index = self.index('insert') + self.insert_cmd(item) # Update input with the matched command + self.mark_set('insert', index) + else: + # No match: reset the history index + self._hist_item = hist_item + + return 'break' + + def on_down(self, event): + """Handle down arrow key press""" + # Handle cursor position outside the input area + if self.compare('insert', '<', 'input'): + self.mark_set('insert', 'end') + return 'break' + + # Check if at the end of the last input line + elif self.compare('insert lineend', '==', 'end-1c'): + # Get the prefix to match (from the previous navigation step) + line = self._hist_match + + # Move one step forward in history + self._hist_item += 1 + + # Search for a matching history entry + while self._hist_item < len(self.history): + # Convert the current history item to a string + item = self.history[self._hist_item] + if item.startswith(line): # Match the prefix + break + self._hist_item += 1 + + if self._hist_item < len(self.history): + # Found a match: insert the command + self.insert_cmd(item) + self.mark_set('insert', 'input+%ic' % len(self._hist_match)) + else: + # No match: reset to the end of the history + self._hist_item = len(self.history) + self.delete('input', 'end') + self.insert('insert', line) + + return 'break' + + def on_tab(self, event): + """Handle tab key press""" + if self.compare('insert', '<', 'input'): + self.mark_set('insert', 'input lineend') + return "break" + # indent code + sel = self.tag_ranges('sel') + if sel: + start = str(self.index('sel.first')) + end = str(self.index('sel.last')) + start_line = int(start.split('.')[0]) + end_line = int(end.split('.')[0]) + 1 + for line in range(start_line, end_line): + self.insert('%i.0' % line, ' ') + else: + txt = self.get('insert-1c') + if not txt.isalnum() and txt != '.': + self.insert('insert', ' ') + return "break" + + def on_shift_return(self, event): + """Handle Shift+Return key press""" + if self.compare('insert', '<', 'input'): + self.mark_set('insert', 'input lineend') + return 'break' + else: # execute commands + self.mark_set('insert', 'end') + self.insert('insert', '\n') + self.insert('insert', self._prompt2, 'prompt') + self.eval_current(True) + + def on_return(self, event=None): + """Handle Return key press""" + if self.compare('insert', '<', 'input'): + self.mark_set('insert', 'input lineend') + return 'break' + else: + self.eval_current(True) + self.see('end') + self.history.save() + return 'break' + + def on_ctrl_return(self, event=None): + """Handle Ctrl+Return key press""" + self.insert('insert', '\n' + self._prompt2, 'prompt') + return 'break' + + def on_backspace(self, event): + """Handle delete key press""" + if self.compare('insert', '<=', 'input'): + self.mark_set('insert', 'input lineend') + return 'break' + sel = self.tag_ranges('sel') + if sel: + self.delete('sel.first', 'sel.last') + else: + linestart = self.get('insert linestart', 'insert') + if re.search(r' $', linestart): + self.delete('insert-4c', 'insert') + else: + self.delete('insert-1c') + return 'break' + + def insert_cmd(self, cmd): + """Insert lines of code, adding prompts""" + input_index = self.index('input') + self.delete('input', 'end') + lines = cmd.splitlines() + if lines: + indent = len(re.search(r'^( )*', lines[0]).group()) + self.insert('insert', lines[0][indent:]) + for line in lines[1:]: + line = line[indent:] + self.insert('insert', '\n') + self.prompt(True) + self.insert('insert', line) + self.mark_set('input', input_index) + self.see('end') + + def eval_current(self, auto_indent=False): + """Evaluate code""" + index = self.index('input') + lines = self.get('input', 'insert lineend').splitlines() # commands to execute + self.mark_set('insert', 'insert lineend') + if lines: # there is code to execute + # remove prompts + lines = [lines[0].rstrip()] + [line[len(self._prompt2):].rstrip() for line in lines[1:]] + for i, l in enumerate(lines): + if l.endswith('?'): + lines[i] = 'help(%s)' % l[:-1] + cmds = '\n'.join(lines) + self.insert('insert', '\n') + out = StringIO() # command output + err = StringIO() # command error traceback + with redirect_stderr(err): # redirect error traceback to err + with redirect_stdout(out): # redirect command output + # execute commands in interactive console + res = self._console.push(cmds) + # if res is True, this is a partial command, e.g. 'def test():' and we need to wait for the rest of the code + errors = err.getvalue() + if errors: # there were errors during the execution + self.insert('end', errors) # display the traceback + self.mark_set('input', 'end') + self.see('end') + self.prompt() # insert new prompt + else: + output = out.getvalue() # get output + if output: + self.insert('end', output, 'output') + self.mark_set('input', 'end') + self.see('end') + if not res and self.compare('insert linestart', '>', 'insert'): + self.insert('insert', '\n') + self.prompt(res) + if auto_indent and lines: + # insert indentation similar to previous lines + indent = re.search(r'^( )*', lines[-1]).group() + line = lines[-1].strip() + if line and line[-1] == ':': + indent = indent + ' ' + self.insert('insert', indent) + self.see('end') + if res: + self.mark_set('input', index) + self._console.resetbuffer() # clear buffer since the whole command will be retrieved from the text widget + elif lines: + if not self.history or [self.history[-1]] != lines: + self.history.append(lines) # Add commands to history + self._hist_item = len(self.history) + out.close() + err.close() + else: + self.insert('insert', '\n') + self.prompt() + + class MultiLineInputDialog(simpledialog.Dialog): def __init__(self, parent, title=None, text=""): self.text=text @@ -267,6 +607,9 @@ def __init__( help_menu.add_command(label="Clear printer list", command=self.clear_printer_list) help_menu.entryconfig("Clear printer list", accelerator="F6") + help_menu.add_command(label="Run debug shell", command=self.tk_console) + help_menu.entryconfig("Run debug shell", accelerator="F7") + help_menu.add_command(label="Get next local IP addresss", command=lambda: self.next_ip(0)) help_menu.entryconfig("Get next local IP addresss", accelerator="F9") @@ -337,6 +680,7 @@ def __init__( self.model_dropdown.bind("", lambda event: self.remove_printer_conf()) self.model_dropdown.bind("", lambda event: self.keep_printer_conf()) self.model_dropdown.bind("", lambda event: self.clear_printer_list()) + self.model_dropdown.bind("", lambda event: self.tk_console()) # BOX IP address ip_frame = ttk.LabelFrame( @@ -1021,6 +1365,18 @@ def clear_printer_list(self): f"[INFO] Printer list cleared.\n" ) + def tk_console(self): + console_window = tk.Toplevel(self) + console_window.title("Debug Console") + console_window.geometry("600x400") + + console = TextConsole(self, console_window) + console.pack(fill='both', expand=True) # Use pack within the frame + + # Configure grid resizing for the frame + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + def open_help_browser(self): # Opens a web browser to a help URL url = "https://ircama.github.io/epson_print_conf" @@ -1396,7 +1752,7 @@ def get_current_eeprom_values(self, values, label): self.status_text.insert( tk.END, f'[ERROR] Cannot read EEPROM values for "{label}"' - ': invalid printer model selected.\n' + f': invalid printer model selected: {self.printer.model}.\n' ) self.config(cursor="") self.update_idletasks() @@ -1675,10 +2031,17 @@ def get_ti_date(self, cursor=True): self.update_idletasks() return try: - date_string = datetime.strptime( - self.printer.stats( - )["stats"]["First TI received time"], "%d %b %Y" - ).strftime("%Y-%m-%d") + d = self.printer.stats()["stats"]["First TI received time"] + if d == "?": + self.status_text.insert( + tk.END, + "[ERROR]: No data from 'First TI received time'." + " Check printer configuration.\n", + ) + self.config(cursor="") + self.update_idletasks() + return + date_string = datetime.strptime(d, "%d %b %Y").strftime("%Y-%m-%d") self.status_text.insert( tk.END, f"[INFO] First TI received time (YYYY-MM-DD): {date_string}.\n", @@ -2493,8 +2856,7 @@ def detect_sequence(eeprom, sequence): if not eeprom or eeprom == {0: None}: self.status_text.insert( tk.END, - '[ERROR] Cannot read EEPROM values' - ': invalid printer model selected.\n' + '[ERROR] Cannot read EEPROM values: invalid printer model selected.\n' ) self.update() self.config(cursor="") From 4af1997682d63acd4235d6ff8af550b8ea9b2d92 Mon Sep 17 00:00:00 2001 From: Ircama Date: Sun, 17 Nov 2024 18:59:55 +0100 Subject: [PATCH 08/11] Improved debug console --- .gitignore | 2 +- ui.py | 563 +++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 439 insertions(+), 126 deletions(-) diff --git a/.gitignore b/.gitignore index 984f09a..0a8a145 100644 --- a/.gitignore +++ b/.gitignore @@ -163,4 +163,4 @@ devices.xml *.srs *.pickle -.python_history +.console_history diff --git a/ui.py b/ui.py index 67629fa..cd61248 100644 --- a/ui.py +++ b/ui.py @@ -36,10 +36,10 @@ from find_printers import PrinterScanner -VERSION = "5.2.18" +VERSION = "5.2.19" NO_CONF_ERROR = ( - "[ERROR] Please select a printer model and a valid IP address," + " Please select a printer model and a valid IP address," " or press 'Detect Printers'.\n" ) @@ -53,7 +53,7 @@ class History(list): - def __init__(self, history_file=".python_history"): + def __init__(self, history_file=".console_history"): super().__init__() self.history_file = history_file @@ -89,7 +89,15 @@ def __init__(self, main, master, **kw): banner = kw.pop('banner', 'Python %s\n' % sys.version) self._prompt1 = kw.pop('prompt1') self._prompt2 = kw.pop('prompt2') - tk.Text.__init__(self, master, **kw) + super().__init__(master, **kw) + self.tag_configure("errors", foreground="red") + font_config = self.tag_cget("errors", "font") + font_obj = tkfont.nametofont(self.cget("font")) + font_size = font_obj.actual("size") + self.tag_configure("errors", font=("Courier", font_size - 2)) + self.tag_configure("banner", foreground="darkred") + self.tag_configure("prompt", foreground="green", font=("Courier", font_size - 2)) + self.tag_configure("input_color", foreground="blue") # --- history self.history = History() self._hist_item = 0 @@ -108,6 +116,20 @@ def __init__(self, main, master, **kw): self.mark_set('input', 'insert') self.mark_gravity('input', 'left') + # Bind right-click (context menu) + self.bind("", self.show_context_menu) + + # Create the context menu + self.context_menu = Menu(self, tearoff=0) + self.context_menu.add_command(label="Cut", command=self.cut) + self.context_menu.add_command(label="Copy", command=self.copy) + self.context_menu.add_command(label="Paste", command=self.paste) + self.context_menu.add_separator() + self.context_menu.add_command(label="Clear", command=self.clear) + + # Add the menu bar + self.create_menu(master) + # --- bindings self.bind('', self.on_ctrl_return) self.bind('', self.on_shift_return) @@ -121,14 +143,175 @@ def __init__(self, main, master, **kw): self.bind('', self.on_ctrl_c) self.bind('<>', self.on_paste) - @property - def h(self): - """Override the history property to return the formatted history as a string.""" - self.insert( - 'end', "\n".join( - f"{i + 1}: {command}" for i, command in enumerate(self.history) - ) + '\n' + def create_menu(self, master): + """Create the menu bar at the top of the window.""" + menu_bar = Menu(master) + master.config(menu=menu_bar) + + # File menu + file_menu = Menu(menu_bar, tearoff=0) + file_menu.add_command(label="Clear Console", command=self.clear_console) + file_menu.add_command(label="History", command=self.dump_history) + file_menu.add_command(label="Close Window", command=master.destroy) + file_menu.add_command(label="Quit Application", command=self.quit) + menu_bar.add_cascade(label="File", menu=file_menu) + + # Edit menu + edit_menu = Menu(menu_bar, tearoff=0) + edit_menu.add_command(label="Cut", command=self.cut) + edit_menu.add_command(label="Copy", command=self.copy) + edit_menu.add_command(label="Paste", command=self.paste) + menu_bar.add_cascade(label="Edit", menu=edit_menu) + + # Help menu + help_menu = Menu(menu_bar, tearoff=0) + help_menu.add_command(label="Usage", command=self.show_help) + help_menu.add_command(label="About", command=self.show_about) + menu_bar.add_cascade(label="Help", menu=help_menu) + + def clear_console(self): + """Clear the text in the console.""" + self.clear() + + def show_about(self): + """Show the About dialog.""" + messagebox.showinfo( + "About", + "epson_print_conf Debug Console." + ) + + def show_help(self): + """Open a separate window with help text.""" + help_window = tk.Toplevel(self) + help_window.title("Help") + help_window.geometry("1000x400") + + # Add a scrollbar and text widget + scrollbar = tk.Scrollbar(help_window) + scrollbar.pack(side="right", fill="y") + + help_text = tk.Text(help_window, wrap="word", yscrollcommand=scrollbar.set) + help_text.tag_configure("title", foreground="purple") + help_text.tag_configure("section", foreground="blue") + + help_text.insert( + tk.END, + 'Welcome to the epson_print_conf Debug Console\n\n', + "title" + ) + help_text.insert( + tk.END, + 'Features:\n\n', + "section" + ) + help_text.insert( + tk.END, + ( + "- Clear Console: Clears all text in the console.\n" + "- Context Menu: Right-click for cut, copy, paste, or clear.\n" + "- Help: Provides this text.\n\n" + ) + ) + help_text.insert( + tk.END, + 'Keyboard Shortcuts from the main window:\n\n', + "section" + ) + help_text.insert( + tk.END, + ( + "- F7: Open the debug console.\n\n" + ) + ) + help_text.insert( + tk.END, + 'Tokens:\n\n', + "section" + ) + help_text.insert( + tk.END, + ( + "self: EpsonPrinterUI self\n" + "master: TextConsole widget\n" + "kw: kw dictionary ({'width': 50, 'wrap': 'word'})\n" + "local: TextConsole self\n\n" + ) + ) + help_text.insert( + tk.END, + 'Examples of commands:\n\n', + "section" ) + help_text.insert( + tk.END, + ( + "self.printer.model\n" + "self.printer.reverse_caesar(b'Hpttzqjv')\n" + 'self.printer.reverse_caesar(bytes.fromhex("48 62 7B 62 6F 6A 62 2B"))\n' + 'import pprint;pprint.pprint(self.printer.status_parser(self.printer.snmp_mib("1.3.6.1.4.1.1248.1.2.2.1.1.1.4.1")[1]))\n' + "self.printer.read_eeprom_many([0])\n" + "self.printer.read_eeprom(0)\n" + "self.printer.snmp_mib(self.printer.eeprom_oid_read_address(0))\n" + "self.printer.snmp_mib('1.3.6.1.4.1.1248.1.2.2.44.1.1.2.1.124.124.7.0.25.7.65.190.160.0.0')\n" + "self.get_ti_date(cursor=True)" + ) + ) + help_text.config(state="disabled") # Make the text read-only + help_text.pack(fill="both", expand=True) + scrollbar.config(command=help_text.yview) + + def show_context_menu(self, event): + """Show the context menu at the cursor position.""" + self.context_menu.post(event.x_root, event.y_root) + + def cut(self): + """Cut the selected text to the clipboard.""" + try: + self.event_generate("<>") + except tk.TclError: + pass + + def copy(self): + """Copy the selected text to the clipboard.""" + try: + self.event_generate("<>") + except tk.TclError: + pass + + def paste(self): + """Paste text from the clipboard.""" + try: + self.event_generate("<>") + except tk.TclError: + pass + + def clear(self): + """Clear all text from the console.""" + self.delete("1.0", "end") + self.insert("1.0", self._prompt1) # Reinsert the prompt + self.delete('input', 'insert lineend') + + def dump_history(self): + """Open a separate window with the output of the history.""" + history_window = tk.Toplevel(self) + history_window.title("History") + history_window.geometry("1000x400") + + # Add a scrollbar and text widget + scrollbar = tk.Scrollbar(history_window) + scrollbar.pack(side="right", fill="y") + + history_txt = tk.Text( + history_window, wrap="word", yscrollcommand=scrollbar.set + ) + history_txt.tag_configure("title", foreground="red") + history_txt.tag_configure("counter", foreground="blue") + for i, command in enumerate(self.history): + history_txt.insert('end', f"{i + 1}\t| ", "counter") + history_txt.insert('end', f"{command}\n") + history_txt.config(state="disabled") # Make the text read-only + history_txt.pack(fill="both", expand=True) + scrollbar.config(command=history_txt.yview) def on_ctrl_c(self, event): """Copy selected code, removing prompts first""" @@ -171,15 +354,20 @@ def on_key_press(self, event): """Prevent text insertion in command history""" if self.compare('insert', '<', 'input') and event.keysym not in ['Left', 'Right']: self._hist_item = len(self.history) - self.mark_set('insert', 'input lineend') if not event.char.isalnum(): return 'break' + else: + if event.keysym not in ['Return']: + self.tag_add("input_color", "input", "insert lineend") def on_key_release(self, event): """Reset history scrolling""" if self.compare('insert', '<', 'input') and event.keysym not in ['Left', 'Right']: self._hist_item = len(self.history) return 'break' + else: + if event.keysym not in ['Return']: + self.tag_add("input_color", "input", "insert lineend") def on_up(self, event): """Handle up arrow key press""" @@ -212,8 +400,16 @@ def on_up(self, event): self.insert_cmd(item) # Update input with the matched command self.mark_set('insert', index) else: - # No match: reset the history index - self._hist_item = hist_item + # No match: use the last history item + self._hist_item = len(self.history) - 1 + if self._hist_item >= 0: + item = self.history[self._hist_item] + index = self.index('insert') + self.insert_cmd(item) # Update input with the last command + self.mark_set('insert', index) + else: + # No history at all, do nothing + self._hist_item = hist_item return 'break' @@ -336,6 +532,7 @@ def eval_current(self, auto_indent=False): index = self.index('input') lines = self.get('input', 'insert lineend').splitlines() # commands to execute self.mark_set('insert', 'insert lineend') + #self.tag_add("input_color", "input", "insert lineend") if lines: # there is code to execute # remove prompts lines = [lines[0].rstrip()] + [line[len(self._prompt2):].rstrip() for line in lines[1:]] @@ -353,7 +550,7 @@ def eval_current(self, auto_indent=False): # if res is True, this is a partial command, e.g. 'def test():' and we need to wait for the rest of the code errors = err.getvalue() if errors: # there were errors during the execution - self.insert('end', errors) # display the traceback + self.insert('end', errors, 'errors') # display the traceback self.mark_set('input', 'end') self.see('end') self.prompt() # insert new prompt @@ -535,11 +732,11 @@ def __init__( self.rowconfigure(0, weight=1) # Setup the menu - menubar = tk.Menu(self) + menubar = Menu(self) self.config(menu=menubar) # Create File menu - file_menu = tk.Menu(menubar, tearoff=0) + file_menu = Menu(menubar, tearoff=0) menubar.add_cascade(label="File", menu=file_menu) LOAD_LABEL_NAME = "%s printer configuration file or web URL..." LOAD_LABEL_TITLE = "Select a %s printer configuration file, or enter a Web URL" @@ -587,9 +784,10 @@ def __init__( label="Save the selected printer configuration to a PICKLE file...", command=self.save_to_file ) + file_menu.add_command(label="Quit Application", command=self.quit) # Create Help menu - help_menu = tk.Menu(menubar, tearoff=0) + help_menu = Menu(menubar, tearoff=0) menubar.add_cascade(label="Settings", menu=help_menu) help_menu.add_command(label="Show printer parameters of the selected model", command=self.printer_config) @@ -607,14 +805,14 @@ def __init__( help_menu.add_command(label="Clear printer list", command=self.clear_printer_list) help_menu.entryconfig("Clear printer list", accelerator="F6") - help_menu.add_command(label="Run debug shell", command=self.tk_console) - help_menu.entryconfig("Run debug shell", accelerator="F7") + help_menu.add_command(label="Debug shell", command=self.tk_console) + help_menu.entryconfig("Debug shell", accelerator="F7") help_menu.add_command(label="Get next local IP addresss", command=lambda: self.next_ip(0)) help_menu.entryconfig("Get next local IP addresss", accelerator="F9") # Create Help menu - help_menu = tk.Menu(menubar, tearoff=0) + help_menu = Menu(menubar, tearoff=0) menubar.add_cascade(label="Help", menu=help_menu) help_menu.add_command(label="Help", command=self.open_help_browser) help_menu.add_command(label="Program Information", command=self.show_program_info) @@ -675,12 +873,12 @@ def __init__( "Select the model of the printer, or press 'Detect Printers'." " Special features are allowed via F2, F3, F4, F5, or F6.\n" ) - self.model_dropdown.bind("", self.printer_config) - self.model_dropdown.bind("", self.key_values) - self.model_dropdown.bind("", lambda event: self.remove_printer_conf()) - self.model_dropdown.bind("", lambda event: self.keep_printer_conf()) - self.model_dropdown.bind("", lambda event: self.clear_printer_list()) - self.model_dropdown.bind("", lambda event: self.tk_console()) + self.bind_all("", self.printer_config) + self.bind_all("", self.key_values) + self.bind_all("", lambda event: self.remove_printer_conf()) + self.bind_all("", lambda event: self.keep_printer_conf()) + self.bind_all("", lambda event: self.clear_printer_list()) + self.bind_all("", lambda event: self.tk_console()) # BOX IP address ip_frame = ttk.LabelFrame( @@ -1070,6 +1268,10 @@ def __init__( self.status_text = ScrolledText( status_frame, wrap=tk.WORD, font=("TkDefaultFont") ) + self.status_text.tag_configure("error", foreground="red") + self.status_text.tag_configure("warn", foreground="blue") + self.status_text.tag_configure("note", foreground="purple") + self.status_text.tag_configure("info", foreground="green") self.status_text.grid( row=0, column=0, @@ -1180,9 +1382,10 @@ def __init__( def save_to_file(self): if not self.model_var.get(): self.show_status_text_view() + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - '[ERROR]: Unknown printer model.' + ': Unknown printer model.' ) return if not self.printer: @@ -1192,9 +1395,10 @@ def save_to_file(self): ) if not self.printer or not self.printer.parm: self.show_status_text_view() + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - '[ERROR]: No printer configuration defined.' + ': No printer configuration defined.' ) return # Open file dialog to enter the file @@ -1224,14 +1428,16 @@ def save_to_file(self): pickle.dump(normalized_config, file) # serialize the list except Exception: self.show_status_text_view() + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR] File save operation failed.\n" + f" File save operation failed.\n" ) return + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f'[INFO] "{os.path.basename(file_path)}" file save operation completed.\n' + f' "{os.path.basename(file_path)}" file save operation completed.\n' ) def load_from_file(self, file_type, type): @@ -1259,14 +1465,16 @@ def load_from_file(self, file_type, type): self.update_idletasks() self.show_status_text_view() if not file_path.tell(): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR] Empty PICKLE FILE {file_path}.\n" + f" Empty PICKLE FILE {file_path}.\n" ) else: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR] Cannot load PICKLE file {file_path}. {e}\n" + f" Cannot load PICKLE file {file_path}. {e}\n" ) return if ( @@ -1281,9 +1489,10 @@ def load_from_file(self, file_type, type): self.model_var.set(self.conf_dict["internal_data"]["default_model"]) else: self.config(cursor="watch") + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Converting file, please wait...\n" + f" Converting file, please wait...\n" ) self.update_idletasks() if type == 1: @@ -1293,9 +1502,10 @@ def load_from_file(self, file_type, type): if not printer_config: self.config(cursor="") self.show_status_text_view() + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR] Cannot load file {file_path}\n" + f" Cannot load file {file_path}\n" ) return self.conf_dict = normalize_config(config=printer_config) @@ -1307,17 +1517,19 @@ def load_from_file(self, file_type, type): self.update_idletasks() if file_path: self.show_status_text_view() + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Loaded file {os.path.basename(file_path)}.\n" + f" Loaded file {os.path.basename(file_path)}.\n" ) def keep_printer_conf(self): self.show_status_text_view() if not self.model_var.get(): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - '[ERROR]: Select a valid printer model.\n' + ': Select a valid printer model.\n' ) return keep_model = self.model_var.get() @@ -1327,17 +1539,19 @@ def keep_printer_conf(self): self.replace_conf = True self.show_status_text_view() self.update_idletasks() + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Printer {keep_model} is the only one in the list.\n" + f" Printer {keep_model} is the only one in the list.\n" ) def remove_printer_conf(self): self.show_status_text_view() if not self.model_var.get(): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - '[ERROR]: Select a valid printer model.\n' + ': Select a valid printer model.\n' ) return remove_model = self.model_var.get() @@ -1348,9 +1562,10 @@ def remove_printer_conf(self): self.replace_conf = True self.show_status_text_view() self.update_idletasks() + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Configuation of printer {remove_model} removed.\n" + f" Configuation of printer {remove_model} removed.\n" ) def clear_printer_list(self): @@ -1360,15 +1575,16 @@ def clear_printer_list(self): self.replace_conf = True self.show_status_text_view() self.update_idletasks() + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Printer list cleared.\n" + f" Printer list cleared.\n" ) def tk_console(self): console_window = tk.Toplevel(self) console_window.title("Debug Console") - console_window.geometry("600x400") + console_window.geometry("800x400") console = TextConsole(self, console_window) console.pack(fill='both', expand=True) # Use pack within the frame @@ -1384,16 +1600,19 @@ def open_help_browser(self): try: ret = webbrowser.open(url) if ret: + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, f"[INFO] The browser is being opened.\n" + tk.END, f" The browser is being opened.\n" ) else: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, f"[ERROR] Cannot open browser.\n" + tk.END, f" Cannot open browser.\n" ) except Exception as e: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, f"[ERROR] Cannot open web browser: {e}\n" + tk.END, f" Cannot open web browser: {e}\n" ) finally: self.config(cursor="") @@ -1605,12 +1824,14 @@ def copy_to_clipboard(self, text_widget): def handle_printer_error(self, e): self.show_status_text_view() if isinstance(e, TimeoutError): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, f"[ERROR] Printer is unreachable or offline.\n" + tk.END, f" Printer is unreachable or offline.\n" ) else: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, f"[ERROR] {e}\n{traceback.format_exc()}\n" + tk.END, f" {e}\n{traceback.format_exc()}\n" ) def get_po_mins(self, cursor=True): @@ -1624,6 +1845,7 @@ def get_po_mins(self, cursor=True): self.show_status_text_view() ip_address = self.ip_var.get() if not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, NO_CONF_ERROR) self.config(cursor="") self.update() @@ -1631,17 +1853,19 @@ def get_po_mins(self, cursor=True): if not self.printer: return if not self.printer.parm.get("stats", {}).get("Power off timer"): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR]: Missing 'Power off timer' in configuration\n", + f": Missing 'Power off timer' in configuration\n", ) self.config(cursor="") self.update_idletasks() return try: po_timer = self.printer.stats()["stats"]["Power off timer"] + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, f"[INFO] Power off timer: {po_timer} minutes.\n" + tk.END, f" Power off timer: {po_timer} minutes.\n" ) self.po_timer_var.set(po_timer) except Exception as e: @@ -1661,6 +1885,7 @@ def get_ser_number(self, cursor=True): self.show_status_text_view() ip_address = self.ip_var.get() if not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, NO_CONF_ERROR) self.config(cursor="") self.update() @@ -1675,23 +1900,26 @@ def get_ser_number(self, cursor=True): self.update_idletasks() return if ser_num is False: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR]: Improper values in printer serial number.\n", + f": Improper values in printer serial number.\n", ) self.config(cursor="") self.update_idletasks() return if not ser_num or "?" in ser_num: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR]: Cannot retrieve the printer serial number.\n", + f": Cannot retrieve the printer serial number.\n", ) self.config(cursor="") self.update_idletasks() return + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, f"[INFO] Printer serial number: {ser_num}.\n" + tk.END, f" Printer serial number: {ser_num}.\n" ) self.ser_num_var.set(ser_num) self.config(cursor="") @@ -1708,6 +1936,7 @@ def get_mac_address(self, cursor=True): self.show_status_text_view() ip_address = self.ip_var.get() if not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, NO_CONF_ERROR) self.config(cursor="") self.update() @@ -1722,15 +1951,17 @@ def get_mac_address(self, cursor=True): self.update_idletasks() return if not mac_addr: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR]: Cannot retrieve the printer WiFi MAC address.\n", + f": Cannot retrieve the printer WiFi MAC address.\n", ) self.config(cursor="") self.update_idletasks() return + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, f"[INFO] Printer WiFi MAC address: {mac_addr}.\n" + tk.END, f" Printer WiFi MAC address: {mac_addr}.\n" ) self.mac_addr_var.set(mac_addr) self.config(cursor="") @@ -1744,14 +1975,16 @@ def get_current_eeprom_values(self, values, label): ) ) if org_values: + self.status_text.insert(tk.END, '[NOTE]', "note") self.status_text.insert( tk.END, - f"[NOTE] Current EEPROM values for {label}: {org_values}.\n" + f" Current EEPROM values for {label}: {org_values}.\n" ) else: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f'[ERROR] Cannot read EEPROM values for "{label}"' + f' Cannot read EEPROM values for "{label}"' f': invalid printer model selected: {self.printer.model}.\n' ) self.config(cursor="") @@ -1777,6 +2010,7 @@ def set_po_mins(self, cursor=True): self.show_status_text_view() ip_address = self.ip_var.get() if not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, NO_CONF_ERROR) self.config(cursor="") self.update_idletasks() @@ -1784,9 +2018,10 @@ def set_po_mins(self, cursor=True): if not self.printer: return if not self.printer.parm.get("stats", {}).get("Power off timer"): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR]: Missing 'Power off timer' in configuration\n", + f": Missing 'Power off timer' in configuration\n", ) self.config(cursor="") self.update_idletasks() @@ -1795,8 +2030,9 @@ def set_po_mins(self, cursor=True): self.config(cursor="") self.update_idletasks() if not po_timer.isnumeric(): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, "[ERROR] Please Use a valid value for minutes.\n" + tk.END, " Please Use a valid value for minutes.\n" ) self.config(cursor="") self.update_idletasks() @@ -1814,17 +2050,19 @@ def set_po_mins(self, cursor=True): self.config(cursor="") self.update_idletasks() return + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Set Power off timer: {po_timer} minutes. Restarting" + f" Set Power off timer: {po_timer} minutes. Restarting" " the printer is required for this change to take effect.\n" ) response = messagebox.askyesno(*CONFIRM_MESSAGE, default='no') if response: try: self.printer.write_poweroff_timer(int(po_timer)) + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, "[INFO] Update operation completed.\n" + tk.END, " Update operation completed.\n" ) except Exception as e: self.handle_printer_error(e) @@ -1856,8 +2094,9 @@ def set_mac_address(self, cursor=True): if not mac or not self.validate_mac_address( self.mac_addr_var.get() ): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, "[ERROR] Please Use a valid MAC address.\n" + tk.END, " Please Use a valid MAC address.\n" ) self.config(cursor="") self.update_idletasks() @@ -1889,9 +2128,10 @@ def set_mac_address(self, cursor=True): self.config(cursor="") self.update_idletasks() return + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Set WiFi MAC Address: {self.mac_addr_var.get()}.\n" + f" Set WiFi MAC Address: {self.mac_addr_var.get()}.\n" ) response = messagebox.askyesno(*CONFIRM_MESSAGE, default='no') if not response: @@ -1901,9 +2141,10 @@ def set_mac_address(self, cursor=True): self.config(cursor="") self.update_idletasks() return + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - "[INFO] Changing the WiFi MAC address of the printer. Restarting" + " Changing the WiFi MAC address of the printer. Restarting" " the printer is required for this change to take effect.\n" ) ret = None @@ -1916,12 +2157,14 @@ def set_mac_address(self, cursor=True): except Exception as e: self.handle_printer_error(e) if ret: + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, "[INFO] Update operation completed.\n" + tk.END, " Update operation completed.\n" ) else: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, f"[ERROR] Write operation failed.\n" + tk.END, f" Write operation failed.\n" ) self.config(cursor="") self.update_idletasks() @@ -1937,6 +2180,7 @@ def set_ser_number(self, cursor=True): self.show_status_text_view() ip_address = self.ip_var.get() if not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, NO_CONF_ERROR) self.config(cursor="") self.update_idletasks() @@ -1946,8 +2190,9 @@ def set_ser_number(self, cursor=True): if not self.ser_num_var.get() or not self.validate_ser_number( self.ser_num_var.get() ): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, "[ERROR] Please Use a valid serial number.\n" + tk.END, " Please Use a valid serial number.\n" ) self.config(cursor="") self.update_idletasks() @@ -1968,9 +2213,10 @@ def set_ser_number(self, cursor=True): self.config(cursor="") self.update_idletasks() return + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Set Printer Serial Number: {self.ser_num_var.get()}.\n" + f" Set Printer Serial Number: {self.ser_num_var.get()}.\n" ) response = messagebox.askyesno(*CONFIRM_MESSAGE, default='no') if not response: @@ -1980,9 +2226,10 @@ def set_ser_number(self, cursor=True): self.config(cursor="") self.update_idletasks() return + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - "[INFO] Changing the serial number of the printer. Restarting" + " Changing the serial number of the printer. Restarting" " the printer is required for this change to take effect.\n" ) ret = None @@ -1995,12 +2242,14 @@ def set_ser_number(self, cursor=True): except Exception as e: self.handle_printer_error(e) if ret: + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, "[INFO] Update operation completed.\n" + tk.END, " Update operation completed.\n" ) else: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, f"[ERROR] Write operation failed.\n" + tk.END, f" Write operation failed.\n" ) self.config(cursor="") self.update_idletasks() @@ -2016,6 +2265,7 @@ def get_ti_date(self, cursor=True): self.show_status_text_view() ip_address = self.ip_var.get() if not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, NO_CONF_ERROR) self.config(cursor="") self.update_idletasks() @@ -2023,9 +2273,10 @@ def get_ti_date(self, cursor=True): if not self.printer: return if not self.printer.parm.get("stats", {}).get("First TI received time"): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR]: Missing 'First TI received time' in configuration\n", + f": Missing 'First TI received time' in configuration\n", ) self.config(cursor="") self.update_idletasks() @@ -2033,18 +2284,20 @@ def get_ti_date(self, cursor=True): try: d = self.printer.stats()["stats"]["First TI received time"] if d == "?": + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - "[ERROR]: No data from 'First TI received time'." + ": No data from 'First TI received time'." " Check printer configuration.\n", ) self.config(cursor="") self.update_idletasks() return date_string = datetime.strptime(d, "%d %b %Y").strftime("%Y-%m-%d") + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] First TI received time (YYYY-MM-DD): {date_string}.\n", + f" First TI received time (YYYY-MM-DD): {date_string}.\n", ) self.date_entry.set_date(date_string) except Exception as e: @@ -2064,6 +2317,7 @@ def set_ti_date(self, cursor=True): self.show_status_text_view() ip_address = self.ip_var.get() if not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, NO_CONF_ERROR) self.config(cursor="") self.update_idletasks() @@ -2071,9 +2325,10 @@ def set_ti_date(self, cursor=True): if not self.printer: return if not self.printer.parm.get("stats", {}).get("First TI received time"): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR]: Missing 'First TI received time' in configuration\n", + f": Missing 'First TI received time' in configuration\n", ) self.config(cursor="") self.update_idletasks() @@ -2092,9 +2347,10 @@ def set_ti_date(self, cursor=True): self.config(cursor="") self.update_idletasks() return + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Set 'First TI received time' (YYYY-MM-DD) to: " + f" Set 'First TI received time' (YYYY-MM-DD) to: " f"{date_string.strftime('%Y-%m-%d')}.\n", ) response = messagebox.askyesno(*CONFIRM_MESSAGE, default='no') @@ -2103,8 +2359,9 @@ def set_ti_date(self, cursor=True): self.printer.write_first_ti_received_time( date_string.year, date_string.month, date_string.day ) + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, "[INFO] Update operation completed.\n" + tk.END, " Update operation completed.\n" ) except Exception as e: self.handle_printer_error(e) @@ -2172,9 +2429,10 @@ def printer_status(self, cursor=True): model = self.model_var.get() ip_address = self.ip_var.get() if not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - "[ERROR] Please enter a valid IP address, or " + " Please enter a valid IP address, or " "press 'Detect Printers'.\n" ) self.config(cursor="") @@ -2215,15 +2473,17 @@ def printer_status(self, cursor=True): def reset_printer_model(self): self.show_status_text_view() if self.model_var.get(): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - '[ERROR]: Unknown printer model ' + ': Unknown printer model ' f'"{self.model_var.get()}"\n', ) else: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - '[ERROR]: Select a valid printer model.\n' + ': Select a valid printer model.\n' ) self.config(cursor="") self.update() @@ -2406,14 +2666,16 @@ def get_values(addresses): self.update_idletasks() return if values: + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] EEPROM values: {values}.\n" + f" EEPROM values: {values}.\n" ) else: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f'[ERROR] Cannot read EEPROM values for addresses "{addresses}"' + f' Cannot read EEPROM values for addresses "{addresses}"' ': invalid printer model selected.\n' ) self.config(cursor="") @@ -2422,6 +2684,7 @@ def get_values(addresses): self.show_status_text_view() ip_address = self.ip_var.get() if not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, NO_CONF_ERROR) self.config(cursor="") self.update_idletasks() @@ -2445,6 +2708,7 @@ def run_detection(): current_log_level = logging.getLogger().getEffectiveLevel() logging.getLogger().setLevel(logging.ERROR) if not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, NO_CONF_ERROR) logging.getLogger().setLevel(current_log_level) self.config(cursor="") @@ -2458,9 +2722,10 @@ def run_detection(): self.printer.parm = {'read_key': None} # Detect the read_key + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Detecting the read_key...\n" + f" Detecting the read_key...\n" ) self.update_idletasks() read_key = None @@ -2473,12 +2738,14 @@ def run_detection(): self.update_idletasks() return if read_key: + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, f"[INFO] Detected read_key: {read_key}.\n" + tk.END, f" Detected read_key: {read_key}.\n" ) else: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, f"[ERROR] Could not detect read_key.\n" + tk.END, f" Could not detect read_key.\n" ) logging.getLogger().setLevel(current_log_level) self.config(cursor="") @@ -2497,17 +2764,19 @@ def run_detection(): and self.printer.parm['read_key'] != read_key ): if self.printer.parm['read_key']: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR] You selected a model with the wrong read_key " + f" You selected a model with the wrong read_key " f"{self.printer.parm['read_key']} instead of " f"{read_key}. Using the detected one to go on.\n" ) self.printer.PRINTER_CONFIG[DETECTED] = {'read_key': read_key} self.printer.parm = self.printer.PRINTER_CONFIG[DETECTED] + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Detecting the serial number...\n" + f" Detecting the serial number...\n" ) try: hex_bytes, matches = self.printer.find_serial_number( @@ -2520,23 +2789,26 @@ def run_detection(): self.update_idletasks() return if not matches: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR] Cannot detect the serial number.\n" + f" Cannot detect the serial number.\n" ) left_ser_num = None for match in matches: tmp_ser_num = match.group() if left_ser_num is not None and tmp_ser_num != left_ser_num: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - "[ERROR] More than one pattern appears to be" + " More than one pattern appears to be" " a serial number with different values:\n" ) for match in matches: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f'[ERROR] - found pattern "{match.group()}"' + f' - found pattern "{match.group()}"' f" at address {match.start()}\n" ) left_ser_num = None @@ -2550,24 +2822,27 @@ def run_detection(): serial_number_address, serial_number_address + len_ser_num ) + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f'[INFO] Detected serial number "{serial_number}"' + f' Detected serial number "{serial_number}"' f" at address {serial_number_address}.\n" ) last_ser_num_addr = serial_number_address + len_ser_num - 1 last_ser_num_value = int(hex_bytes[last_ser_num_addr], 16) + self.status_text.insert(tk.END, '[NOTE]', "note") self.status_text.insert( tk.END, - f"[NOTE] Current EEPROM value for the last byte of the" + f" Current EEPROM value for the last byte of the" f" serial number:" f" {last_ser_num_addr}: {last_ser_num_value}.\n" ) if last_ser_num_addr is None: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - "[ERROR] Could not detect serial number.\n" + " Could not detect serial number.\n" ) logging.getLogger().setLevel(current_log_level) self.config(cursor="") @@ -2578,9 +2853,10 @@ def run_detection(): or self.printer.parm['serial_number'] != serial_number_range ): if 'serial_number' in self.printer.parm: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR] The serial number addresses" + f" The serial number addresses" f" {self.printer.parm['serial_number']} of the" f" selected printer is different from the detected" f" one {serial_number_range}," @@ -2596,9 +2872,10 @@ def run_detection(): write_key_list = self.printer.write_key_list(read_key) # Validate the write_key against any of the known values + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - "[INFO] Detecting the write_key," + " Detecting the write_key," " do not power off the printer now...\n" ) old_write_key = self.printer.parm.get('write_key') @@ -2614,9 +2891,10 @@ def run_detection(): ) assert valid is not None except AssertionError: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - "[ERROR] Write operation failed. Check whether the" + " Write operation failed. Check whether the" " serial number is changed and restore it manually.\n" ) self.printer.parm['write_key'] = old_write_key @@ -2632,8 +2910,9 @@ def run_detection(): self.update_idletasks() return if valid is None: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, "[ERROR] Operation interrupted with errors.\n" + tk.END, " Operation interrupted with errors.\n" ) self.printer.parm['write_key'] = old_write_key logging.getLogger().setLevel(current_log_level) @@ -2645,14 +2924,16 @@ def run_detection(): found_write_key = write_key self.printer.parm['write_key'] = old_write_key + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, f"[INFO] Detected write_key: {found_write_key}\n" + tk.END, f" Detected write_key: {found_write_key}\n" ) if not old_write_key or old_write_key != found_write_key: if old_write_key and old_write_key != found_write_key: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f"[ERROR] The selected write key {old_write_key}" + f" The selected write key {old_write_key}" f" is different from the detected one, which will" f" be used to go on.\n" ) @@ -2684,39 +2965,45 @@ def run_detection(): ): rwk_kist.append(p) if rk_kist: + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Models with same read_key: {rk_kist}\n" + f" Models with same read_key: {rk_kist}\n" ) if wk_kist: + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Models with same write_key: {wk_kist}\n" + f" Models with same write_key: {wk_kist}\n" ) if rwk_kist: + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Models with same access keys: {rwk_kist}\n" + f" Models with same access keys: {rwk_kist}\n" ) if ( DETECTED in self.printer.PRINTER_CONFIG and self.printer.PRINTER_CONFIG[DETECTED] ): + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f'[INFO] Found data: ' + f' Found data: ' f'{self.printer.PRINTER_CONFIG[DETECTED]}.\n' ) self.detect_configuration_button.state(["!disabled"]) + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, "[INFO] Detect operation completed.\n" + tk.END, " Detect operation completed.\n" ) break if not found_write_key: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - "[ERROR] Unable to detect the write key by validating" + " Unable to detect the write key by validating" " against any of the known ones.\n" ) logging.getLogger().setLevel(current_log_level) @@ -2727,6 +3014,7 @@ def run_detection(): self.show_status_text_view() ip_address = self.ip_var.get() if not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, NO_CONF_ERROR) return response = messagebox.askyesno( @@ -2740,9 +3028,10 @@ def run_detection(): default='no' ) if response: + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Starting the access key detection, please wait for many minutes...\n" + f" Starting the access key detection, please wait for many minutes...\n" ) self.config(cursor="watch") self.update() @@ -2765,6 +3054,7 @@ def web_interface(self, cursor=True): self.show_status_text_view() ip_address = self.ip_var.get() if not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, NO_CONF_ERROR) self.config(cursor="") self.update() @@ -2774,16 +3064,19 @@ def web_interface(self, cursor=True): try: ret = webbrowser.open(ip_address) if ret: + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, f"[INFO] The browser is being opened.\n" + tk.END, f" The browser is being opened.\n" ) else: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, f"[ERROR] Cannot open browser.\n" + tk.END, f" Cannot open browser.\n" ) except Exception as e: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, f"[ERROR] Cannot open web browser: {e}\n" + tk.END, f" Cannot open web browser: {e}\n" ) finally: self.config(cursor="") @@ -2812,15 +3105,17 @@ def detect_sequence(eeprom, sequence): self.show_status_text_view() ip_address = self.ip_var.get() if not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, NO_CONF_ERROR) self.config(cursor="") self.update() return if not self.printer: return + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Reading Printer SNMP values...\n" + f" Reading Printer SNMP values...\n" ) try: stats = self.printer.stats() @@ -2830,17 +3125,19 @@ def detect_sequence(eeprom, sequence): self.update_idletasks() return False if not "snmp_info" in stats: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - '[ERROR] No SNMP values could be found.\n' + ' No SNMP values could be found.\n' ) self.update() self.config(cursor="") self.update_idletasks() return False + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Reading EEPROM values, please wait for some minutes...\n" + f" Reading EEPROM values, please wait for some minutes...\n" ) self.update() try: @@ -2854,9 +3151,10 @@ def detect_sequence(eeprom, sequence): ) } if not eeprom or eeprom == {0: None}: + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - '[ERROR] Cannot read EEPROM values: invalid printer model selected.\n' + ' Cannot read EEPROM values: invalid printer model selected.\n' ) self.update() self.config(cursor="") @@ -2867,9 +3165,10 @@ def detect_sequence(eeprom, sequence): self.config(cursor="") self.update_idletasks() return False + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Analyzing EEPROM values...\n" + f" Analyzing EEPROM values...\n" ) self.update() @@ -2970,9 +3269,10 @@ def detect_sequence(eeprom, sequence): except Exception as e: self.handle_printer_error(e) finally: + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Operation completed.\n" + f" Operation completed.\n" ) self.update_idletasks() self.config(cursor="") @@ -3066,8 +3366,9 @@ def write_eeprom_values(dict_addr_val): return False except Exception as e: self.handle_printer_error(e) + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, f"[INFO] Write EEPROM completed.\n" + tk.END, f" Write EEPROM completed.\n" ) self.config(cursor="") self.update_idletasks() @@ -3075,14 +3376,16 @@ def write_eeprom_values(dict_addr_val): self.show_status_text_view() ip_address = self.ip_var.get() if not self._is_valid_ip(ip_address): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, NO_CONF_ERROR) return dict_addr_val = get_input() if dict_addr_val is not None: self.config(cursor="watch") self.update() + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, f"[INFO] Going to write EEPROM: {dict_addr_val}.\n" + tk.END, f" Going to write EEPROM: {dict_addr_val}.\n" ) self.after(200, lambda: dialog_write_values(dict_addr_val)) @@ -3103,6 +3406,7 @@ def reset_waste_ink(self, cursor=True): or "read_key" not in self.printer.parm or "write_key" not in self.printer.parm ): + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert(tk.END, NO_CONF_ERROR) self.config(cursor="") self.update_idletasks() @@ -3136,9 +3440,10 @@ def reset_waste_ink(self, cursor=True): if response: try: self.printer.reset_waste_ink_levels() + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - "[INFO] Waste ink levels have been reset." + " Waste ink levels have been reset." " Perform a power cycle of the printer now.\n" ) except Exception as e: @@ -3152,8 +3457,9 @@ def reset_waste_ink(self, cursor=True): def start_detect_printers(self): self.show_status_text_view() + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, "[INFO] Detecting printers... (this might take a while)\n" + tk.END, " Detecting printers... (this might take a while)\n" ) # run printer detection in new thread, as it can take a while @@ -3175,9 +3481,10 @@ def detect_printers_thread(self, cursor=True): ) if len(printers) > 0: if len(printers) == 1: + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] Found printer '{printers[0]['name']}' " + f" Found printer '{printers[0]['name']}' " f"at {printers[0]['ip']} " f"(hostname: {printers[0]['hostname']})\n", ) @@ -3190,23 +3497,27 @@ def detect_printers_thread(self, cursor=True): self.model_var.set(model) break if self.model_var.get() == "": + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( tk.END, - f'[ERROR] Printer model unknown.\n' + f' Printer model unknown.\n' ) self.model_var.set("") else: + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( - tk.END, f"[INFO] Found {len(printers)} printers:\n" + tk.END, f" Found {len(printers)} printers:\n" ) for printer in printers: + self.status_text.insert(tk.END, '[INFO]', "info") self.status_text.insert( tk.END, - f"[INFO] {printer['name']} found at {printer['ip']}" + f" {printer['name']} found at {printer['ip']}" f" (hostname: {printer['hostname']})\n", ) else: - self.status_text.insert(tk.END, "[WARN] No printers found.\n") + self.status_text.insert(tk.END, '[WARN]', "warn") + self.status_text.insert(tk.END, " No printers found.\n") except Exception as e: self.handle_printer_error(e) finally: @@ -3346,8 +3657,9 @@ def print_items(self, text): ip_address = self.ip_var.get() if not self._is_valid_ip(ip_address): self.show_status_text_view() + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, f"[ERROR] Missing IP address or printer host name.\n" + tk.END, f" Missing IP address or printer host name.\n" ) return try: @@ -3362,8 +3674,9 @@ def print_items(self, text): ) except Exception as e: self.show_status_text_view() + self.status_text.insert(tk.END, '[ERROR]', "error") self.status_text.insert( - tk.END, f"[ERROR] Printer is unreachable or offline.\n" + tk.END, f" Printer is unreachable or offline.\n" ) From 96dce0b91d0b3bd1593961ac1e3fd13d5e7d70e5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 17 Nov 2024 18:51:44 +0000 Subject: [PATCH 09/11] Update VERSION to v5.3.0 --- ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui.py b/ui.py index cd61248..23fae9b 100644 --- a/ui.py +++ b/ui.py @@ -36,7 +36,7 @@ from find_printers import PrinterScanner -VERSION = "5.2.19" +VERSION = "5.3.0" NO_CONF_ERROR = ( " Please select a printer model and a valid IP address," From cf2d13c4801865f9d1322c4b3280b9b78d8e54dd Mon Sep 17 00:00:00 2001 From: Ircama Date: Mon, 18 Nov 2024 05:21:07 +0100 Subject: [PATCH 10/11] Usage of the text_console module --- README.md | 17 -- SECURITY.md | 14 ++ requirements.txt | 1 + ui.py | 455 +---------------------------------------------- 4 files changed, 19 insertions(+), 468 deletions(-) create mode 100644 SECURITY.md diff --git a/README.md b/README.md index 3b5b31e..ab16cd3 100644 --- a/README.md +++ b/README.md @@ -57,23 +57,6 @@ cd epson_print_conf pip install -r requirements.txt ``` -Alternatively, install requirements via command line: - -``` -git clone https://github.com/Ircama/epson_print_conf -pip3 install pyyaml -pip3 install pyasn1==0.4.8 -pip3 install git+https://github.com/etingof/pysnmp.git -pip3 install tkcalendar -pip3 install pyperclip -pip3 install black -pip3 install tomli - -cd epson_print_conf -``` - -With Python 12, also: `pip3 install pyasyncore`. - Notes (at the time of writing): - [before pysnmp, install pyasn1 with version 0.4.8 and not 0.5](https://github.com/etingof/pysnmp/issues/440#issuecomment-1544341598) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a12b7df --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# ELM327-emulator Security Policy + +*ELM327-emulator* is a testing software which is not expected to run in production environments. Considering that the dictionary allows executing user-defined commands, +it is important to revise any third-party customization to avoid that security flaws are introduced. + +Security bugs will be taken seriously and, +if confirmed upon investigation, a new patch will be released within a reasonable amount of time, including a security bulletin and the credit to the discoverer. + +Warning: when using the TCP/IP networking, the INET socket is bound to all interfaces. + +## Reporting a Security Bug + +The way to report a security bug is to open an [issue](https://github.com/Ircama/ELM327-emulator/issues) including related information +(e.g., reproduction steps, version). \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 6e0d40d..0da1ba4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ tkcalendar pyperclip black tomli +text-console diff --git a/ui.py b/ui.py index 23fae9b..627d01c 100644 --- a/ui.py +++ b/ui.py @@ -34,6 +34,7 @@ from epson_print_conf import EpsonPrinter, get_printer_models from parse_devices import generate_config_from_toml, generate_config_from_xml, normalize_config from find_printers import PrinterScanner +from text_console import TextConsole VERSION = "5.3.0" @@ -52,133 +53,9 @@ ) -class History(list): - def __init__(self, history_file=".console_history"): - super().__init__() - self.history_file = history_file - - # Load history from the file - if os.path.exists(self.history_file): - with open(self.history_file, "r", encoding="utf-8") as file: - self.extend(line.strip() for line in file if line.strip()) +class EpcTextConsole(TextConsole): - def __getitem__(self, index): - try: - return super().__getitem__(index) - except IndexError: - return None - - def append(self, item): - # Ensure only strings are added - if isinstance(item, list): - item = " ".join(item) # Convert list to string - super().append(item) - - def save(self): - # Save history back to the file - with open(self.history_file, "w", encoding="utf-8") as file: - file.writelines(item + "\n" for item in self) - - -class TextConsole(tk.Text): - def __init__(self, main, master, **kw): - kw.setdefault('width', 50) - kw.setdefault('wrap', 'word') - kw.setdefault('prompt1', '>>> ') - kw.setdefault('prompt2', '... ') - banner = kw.pop('banner', 'Python %s\n' % sys.version) - self._prompt1 = kw.pop('prompt1') - self._prompt2 = kw.pop('prompt2') - super().__init__(master, **kw) - self.tag_configure("errors", foreground="red") - font_config = self.tag_cget("errors", "font") - font_obj = tkfont.nametofont(self.cget("font")) - font_size = font_obj.actual("size") - self.tag_configure("errors", font=("Courier", font_size - 2)) - self.tag_configure("banner", foreground="darkred") - self.tag_configure("prompt", foreground="green", font=("Courier", font_size - 2)) - self.tag_configure("input_color", foreground="blue") - # --- history - self.history = History() - self._hist_item = 0 - self._hist_match = '' - - # --- initialization - console_locals = { - "self": main, - "master": master, - "kw": kw, - "local": self - } - self._console = InteractiveConsole(locals=console_locals) # python console - self.insert('end', banner, 'banner') - self.prompt() - self.mark_set('input', 'insert') - self.mark_gravity('input', 'left') - - # Bind right-click (context menu) - self.bind("", self.show_context_menu) - - # Create the context menu - self.context_menu = Menu(self, tearoff=0) - self.context_menu.add_command(label="Cut", command=self.cut) - self.context_menu.add_command(label="Copy", command=self.copy) - self.context_menu.add_command(label="Paste", command=self.paste) - self.context_menu.add_separator() - self.context_menu.add_command(label="Clear", command=self.clear) - - # Add the menu bar - self.create_menu(master) - - # --- bindings - self.bind('', self.on_ctrl_return) - self.bind('', self.on_shift_return) - self.bind('', self.on_key_press) - self.bind('', self.on_key_release) - self.bind('', self.on_tab) - self.bind('', self.on_down) - self.bind('', self.on_up) - self.bind('', self.on_return) - self.bind('', self.on_backspace) - self.bind('', self.on_ctrl_c) - self.bind('<>', self.on_paste) - - def create_menu(self, master): - """Create the menu bar at the top of the window.""" - menu_bar = Menu(master) - master.config(menu=menu_bar) - - # File menu - file_menu = Menu(menu_bar, tearoff=0) - file_menu.add_command(label="Clear Console", command=self.clear_console) - file_menu.add_command(label="History", command=self.dump_history) - file_menu.add_command(label="Close Window", command=master.destroy) - file_menu.add_command(label="Quit Application", command=self.quit) - menu_bar.add_cascade(label="File", menu=file_menu) - - # Edit menu - edit_menu = Menu(menu_bar, tearoff=0) - edit_menu.add_command(label="Cut", command=self.cut) - edit_menu.add_command(label="Copy", command=self.copy) - edit_menu.add_command(label="Paste", command=self.paste) - menu_bar.add_cascade(label="Edit", menu=edit_menu) - - # Help menu - help_menu = Menu(menu_bar, tearoff=0) - help_menu.add_command(label="Usage", command=self.show_help) - help_menu.add_command(label="About", command=self.show_about) - menu_bar.add_cascade(label="Help", menu=help_menu) - - def clear_console(self): - """Clear the text in the console.""" - self.clear() - - def show_about(self): - """Show the About dialog.""" - messagebox.showinfo( - "About", - "epson_print_conf Debug Console." - ) + show_about_message = "epson_print_conf Debug Console." def show_help(self): """Open a separate window with help text.""" @@ -260,330 +137,6 @@ def show_help(self): help_text.pack(fill="both", expand=True) scrollbar.config(command=help_text.yview) - def show_context_menu(self, event): - """Show the context menu at the cursor position.""" - self.context_menu.post(event.x_root, event.y_root) - - def cut(self): - """Cut the selected text to the clipboard.""" - try: - self.event_generate("<>") - except tk.TclError: - pass - - def copy(self): - """Copy the selected text to the clipboard.""" - try: - self.event_generate("<>") - except tk.TclError: - pass - - def paste(self): - """Paste text from the clipboard.""" - try: - self.event_generate("<>") - except tk.TclError: - pass - - def clear(self): - """Clear all text from the console.""" - self.delete("1.0", "end") - self.insert("1.0", self._prompt1) # Reinsert the prompt - self.delete('input', 'insert lineend') - - def dump_history(self): - """Open a separate window with the output of the history.""" - history_window = tk.Toplevel(self) - history_window.title("History") - history_window.geometry("1000x400") - - # Add a scrollbar and text widget - scrollbar = tk.Scrollbar(history_window) - scrollbar.pack(side="right", fill="y") - - history_txt = tk.Text( - history_window, wrap="word", yscrollcommand=scrollbar.set - ) - history_txt.tag_configure("title", foreground="red") - history_txt.tag_configure("counter", foreground="blue") - for i, command in enumerate(self.history): - history_txt.insert('end', f"{i + 1}\t| ", "counter") - history_txt.insert('end', f"{command}\n") - history_txt.config(state="disabled") # Make the text read-only - history_txt.pack(fill="both", expand=True) - scrollbar.config(command=history_txt.yview) - - def on_ctrl_c(self, event): - """Copy selected code, removing prompts first""" - sel = self.tag_ranges('sel') - if sel: - txt = self.get('sel.first', 'sel.last').splitlines() - lines = [] - for i, line in enumerate(txt): - if line.startswith(self._prompt1): - lines.append(line[len(self._prompt1):]) - elif line.startswith(self._prompt2): - lines.append(line[len(self._prompt2):]) - else: - lines.append(line) - self.clipboard_clear() - self.clipboard_append('\n'.join(lines)) - return 'break' - - def on_paste(self, event): - """Paste commands""" - if self.compare('insert', '<', 'input'): - return "break" - sel = self.tag_ranges('sel') - if sel: - self.delete('sel.first', 'sel.last') - txt = self.clipboard_get() - self.insert("insert", txt) - self.insert_cmd(self.get("input", "end")) - return 'break' - - def prompt(self, result=False): - """Insert a prompt""" - if result: - self.insert('end', self._prompt2, 'prompt') - else: - self.insert('end', self._prompt1, 'prompt') - self.mark_set('input', 'end-1c') - - def on_key_press(self, event): - """Prevent text insertion in command history""" - if self.compare('insert', '<', 'input') and event.keysym not in ['Left', 'Right']: - self._hist_item = len(self.history) - if not event.char.isalnum(): - return 'break' - else: - if event.keysym not in ['Return']: - self.tag_add("input_color", "input", "insert lineend") - - def on_key_release(self, event): - """Reset history scrolling""" - if self.compare('insert', '<', 'input') and event.keysym not in ['Left', 'Right']: - self._hist_item = len(self.history) - return 'break' - else: - if event.keysym not in ['Return']: - self.tag_add("input_color", "input", "insert lineend") - - def on_up(self, event): - """Handle up arrow key press""" - # Handle cursor position outside the input area - if self.compare('insert', '<', 'input'): - self.mark_set('insert', 'end') - return 'break' - - # Check if at the start of the input line - elif self.index('input linestart') == self.index('insert linestart'): - # Get the current input line for partial matching - line = self.get('input', 'insert') - self._hist_match = line - - # Save the current history index and move one step back - hist_item = self._hist_item - self._hist_item -= 1 - - # Search for a matching history entry - while self._hist_item >= 0: - # Convert the current history item to a string - item = self.history[self._hist_item] - if item.startswith(line): # Match the current input - break - self._hist_item -= 1 - - if self._hist_item >= 0: - # Found a match: insert the command - index = self.index('insert') - self.insert_cmd(item) # Update input with the matched command - self.mark_set('insert', index) - else: - # No match: use the last history item - self._hist_item = len(self.history) - 1 - if self._hist_item >= 0: - item = self.history[self._hist_item] - index = self.index('insert') - self.insert_cmd(item) # Update input with the last command - self.mark_set('insert', index) - else: - # No history at all, do nothing - self._hist_item = hist_item - - return 'break' - - def on_down(self, event): - """Handle down arrow key press""" - # Handle cursor position outside the input area - if self.compare('insert', '<', 'input'): - self.mark_set('insert', 'end') - return 'break' - - # Check if at the end of the last input line - elif self.compare('insert lineend', '==', 'end-1c'): - # Get the prefix to match (from the previous navigation step) - line = self._hist_match - - # Move one step forward in history - self._hist_item += 1 - - # Search for a matching history entry - while self._hist_item < len(self.history): - # Convert the current history item to a string - item = self.history[self._hist_item] - if item.startswith(line): # Match the prefix - break - self._hist_item += 1 - - if self._hist_item < len(self.history): - # Found a match: insert the command - self.insert_cmd(item) - self.mark_set('insert', 'input+%ic' % len(self._hist_match)) - else: - # No match: reset to the end of the history - self._hist_item = len(self.history) - self.delete('input', 'end') - self.insert('insert', line) - - return 'break' - - def on_tab(self, event): - """Handle tab key press""" - if self.compare('insert', '<', 'input'): - self.mark_set('insert', 'input lineend') - return "break" - # indent code - sel = self.tag_ranges('sel') - if sel: - start = str(self.index('sel.first')) - end = str(self.index('sel.last')) - start_line = int(start.split('.')[0]) - end_line = int(end.split('.')[0]) + 1 - for line in range(start_line, end_line): - self.insert('%i.0' % line, ' ') - else: - txt = self.get('insert-1c') - if not txt.isalnum() and txt != '.': - self.insert('insert', ' ') - return "break" - - def on_shift_return(self, event): - """Handle Shift+Return key press""" - if self.compare('insert', '<', 'input'): - self.mark_set('insert', 'input lineend') - return 'break' - else: # execute commands - self.mark_set('insert', 'end') - self.insert('insert', '\n') - self.insert('insert', self._prompt2, 'prompt') - self.eval_current(True) - - def on_return(self, event=None): - """Handle Return key press""" - if self.compare('insert', '<', 'input'): - self.mark_set('insert', 'input lineend') - return 'break' - else: - self.eval_current(True) - self.see('end') - self.history.save() - return 'break' - - def on_ctrl_return(self, event=None): - """Handle Ctrl+Return key press""" - self.insert('insert', '\n' + self._prompt2, 'prompt') - return 'break' - - def on_backspace(self, event): - """Handle delete key press""" - if self.compare('insert', '<=', 'input'): - self.mark_set('insert', 'input lineend') - return 'break' - sel = self.tag_ranges('sel') - if sel: - self.delete('sel.first', 'sel.last') - else: - linestart = self.get('insert linestart', 'insert') - if re.search(r' $', linestart): - self.delete('insert-4c', 'insert') - else: - self.delete('insert-1c') - return 'break' - - def insert_cmd(self, cmd): - """Insert lines of code, adding prompts""" - input_index = self.index('input') - self.delete('input', 'end') - lines = cmd.splitlines() - if lines: - indent = len(re.search(r'^( )*', lines[0]).group()) - self.insert('insert', lines[0][indent:]) - for line in lines[1:]: - line = line[indent:] - self.insert('insert', '\n') - self.prompt(True) - self.insert('insert', line) - self.mark_set('input', input_index) - self.see('end') - - def eval_current(self, auto_indent=False): - """Evaluate code""" - index = self.index('input') - lines = self.get('input', 'insert lineend').splitlines() # commands to execute - self.mark_set('insert', 'insert lineend') - #self.tag_add("input_color", "input", "insert lineend") - if lines: # there is code to execute - # remove prompts - lines = [lines[0].rstrip()] + [line[len(self._prompt2):].rstrip() for line in lines[1:]] - for i, l in enumerate(lines): - if l.endswith('?'): - lines[i] = 'help(%s)' % l[:-1] - cmds = '\n'.join(lines) - self.insert('insert', '\n') - out = StringIO() # command output - err = StringIO() # command error traceback - with redirect_stderr(err): # redirect error traceback to err - with redirect_stdout(out): # redirect command output - # execute commands in interactive console - res = self._console.push(cmds) - # if res is True, this is a partial command, e.g. 'def test():' and we need to wait for the rest of the code - errors = err.getvalue() - if errors: # there were errors during the execution - self.insert('end', errors, 'errors') # display the traceback - self.mark_set('input', 'end') - self.see('end') - self.prompt() # insert new prompt - else: - output = out.getvalue() # get output - if output: - self.insert('end', output, 'output') - self.mark_set('input', 'end') - self.see('end') - if not res and self.compare('insert linestart', '>', 'insert'): - self.insert('insert', '\n') - self.prompt(res) - if auto_indent and lines: - # insert indentation similar to previous lines - indent = re.search(r'^( )*', lines[-1]).group() - line = lines[-1].strip() - if line and line[-1] == ':': - indent = indent + ' ' - self.insert('insert', indent) - self.see('end') - if res: - self.mark_set('input', index) - self._console.resetbuffer() # clear buffer since the whole command will be retrieved from the text widget - elif lines: - if not self.history or [self.history[-1]] != lines: - self.history.append(lines) # Add commands to history - self._hist_item = len(self.history) - out.close() - err.close() - else: - self.insert('insert', '\n') - self.prompt() - class MultiLineInputDialog(simpledialog.Dialog): def __init__(self, parent, title=None, text=""): @@ -1586,7 +1139,7 @@ def tk_console(self): console_window.title("Debug Console") console_window.geometry("800x400") - console = TextConsole(self, console_window) + console = EpcTextConsole(self, console_window) console.pack(fill='both', expand=True) # Use pack within the frame # Configure grid resizing for the frame From 2ed931b13260dfeae3afb89ec51890f635bc32ad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 18 Nov 2024 12:02:15 +0000 Subject: [PATCH 11/11] Update VERSION to v5.3.1 --- ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui.py b/ui.py index 627d01c..c02ab92 100644 --- a/ui.py +++ b/ui.py @@ -37,7 +37,7 @@ from text_console import TextConsole -VERSION = "5.3.0" +VERSION = "5.3.1" NO_CONF_ERROR = ( " Please select a printer model and a valid IP address,"