From 633f930cd8045dae69d78cfe65d6875e36cbc0e9 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Mon, 1 Dec 2025 23:27:40 +0800 Subject: [PATCH 01/28] [Configure] Inherit Toplevel and Tk directly --- configure.py | 162 ++++++++++++++++---------------- library/lcd/color.py | 4 +- library/lcd/lcd_comm_rev_a.py | 2 - library/lcd/lcd_comm_weact_a.py | 6 +- 4 files changed, 86 insertions(+), 88 deletions(-) diff --git a/configure.py b/configure.py index 365a3910..83322595 100755 --- a/configure.py +++ b/configure.py @@ -127,11 +127,11 @@ circular_mask = Image.open(MAIN_DIRECTORY + "res/backgrounds/circular-mask.png") def get_theme_data(name: str): - dir = os.path.join(THEMES_DIR, name) + folder = os.path.join(THEMES_DIR, name) # checking if it is a directory - if os.path.isdir(dir): + if os.path.isdir(folder): # Check if a theme.yaml file exists - theme = os.path.join(dir, 'theme.yaml') + theme = os.path.join(folder, 'theme.yaml') if os.path.isfile(theme): # Get display size from theme.yaml with open(theme, "rt", encoding='utf8') as stream: @@ -181,15 +181,15 @@ def get_fans(): return fan_list -class TuringConfigWindow: +class TuringConfigWindow(Tk): def __init__(self): - self.window = Tk() - self.window.title('Turing System Monitor configuration') - self.window.geometry("820x580") - self.window.iconphoto(True, PhotoImage(file=MAIN_DIRECTORY + "res/icons/monitor-icon-17865/64.png")) + super().__init__() + self.title('Turing System Monitor configuration') + self.geometry("820x580") + self.iconphoto(True, PhotoImage(file=MAIN_DIRECTORY + "res/icons/monitor-icon-17865/64.png")) # When window gets focus again, reload theme preview in case it has been updated by theme editor - self.window.bind("", self.on_theme_change) - self.window.after(0, self.on_fan_speed_update) + self.bind("", self.on_theme_change) + self.after(0, self.on_fan_speed_update) # Subwindow for weather/ping config. self.more_config_window = MoreConfigWindow(self) @@ -198,84 +198,84 @@ def __init__(self): sv_ttk.set_theme("light") self.theme_preview_img = None - self.theme_preview = ttk.Label(self.window) + self.theme_preview = ttk.Label(self) self.theme_preview.place(x=10, y=10) - self.theme_author = ttk.Label(self.window) + self.theme_author = ttk.Label(self) - sysmon_label = ttk.Label(self.window, text='Display configuration', font='bold') + sysmon_label = ttk.Label(self, text='Display configuration', font='bold') sysmon_label.place(x=370, y=0) - self.model_label = ttk.Label(self.window, text='Smart screen model') + self.model_label = ttk.Label(self, text='Smart screen model') self.model_label.place(x=370, y=35) - self.model_cb = ttk.Combobox(self.window, values=list(dict.fromkeys((revision_and_size_to_model_map.values()))), + self.model_cb = ttk.Combobox(self, values=list(dict.fromkeys((revision_and_size_to_model_map.values()))), state='readonly') self.model_cb.bind('<>', self.on_model_change) self.model_cb.place(x=550, y=30, width=250) - self.size_label = ttk.Label(self.window, text='Smart screen size') + self.size_label = ttk.Label(self, text='Smart screen size') self.size_label.place(x=370, y=75) - self.size_cb = ttk.Combobox(self.window, values=size_list, state='readonly') + self.size_cb = ttk.Combobox(self, values=size_list, state='readonly') self.size_cb.bind('<>', self.on_size_change) self.size_cb.place(x=550, y=70, width=250) - self.com_label = ttk.Label(self.window, text='COM port') + self.com_label = ttk.Label(self, text='COM port') self.com_label.place(x=370, y=115) - self.com_cb = ttk.Combobox(self.window, values=get_com_ports(), state='readonly') + self.com_cb = ttk.Combobox(self, values=get_com_ports(), state='readonly') self.com_cb.place(x=550, y=110, width=250) - self.orient_label = ttk.Label(self.window, text='Orientation') + self.orient_label = ttk.Label(self, text='Orientation') self.orient_label.place(x=370, y=155) - self.orient_cb = ttk.Combobox(self.window, values=list(reverse_map.values()), state='readonly') + self.orient_cb = ttk.Combobox(self, values=list(reverse_map.values()), state='readonly') self.orient_cb.place(x=550, y=150, width=250) self.brightness_string = StringVar() - self.brightness_label = ttk.Label(self.window, text='Brightness') + self.brightness_label = ttk.Label(self, text='Brightness') self.brightness_label.place(x=370, y=195) - self.brightness_slider = ttk.Scale(self.window, from_=0, to=100, orient=HORIZONTAL, + self.brightness_slider = ttk.Scale(self, from_=0, to=100, orient=HORIZONTAL, command=self.on_brightness_change) self.brightness_slider.place(x=600, y=195, width=180) - self.brightness_val_label = ttk.Label(self.window, textvariable=self.brightness_string) + self.brightness_val_label = ttk.Label(self, textvariable=self.brightness_string) self.brightness_val_label.place(x=550, y=195) - self.brightness_warning_label = ttk.Label(self.window, + self.brightness_warning_label = ttk.Label(self, text="⚠ Turing 3.5\" displays can get hot at high brightness!", foreground='#ff8c00') - sysmon_label = ttk.Label(self.window, text='System Monitor Configuration', font='bold') + sysmon_label = ttk.Label(self, text='System Monitor Configuration', font='bold') sysmon_label.place(x=370, y=260) - self.theme_label = ttk.Label(self.window, text='Theme') + self.theme_label = ttk.Label(self, text='Theme') self.theme_label.place(x=370, y=300) - self.theme_cb = ttk.Combobox(self.window, state='readonly') + self.theme_cb = ttk.Combobox(self, state='readonly') self.theme_cb.place(x=550, y=295, width=250) self.theme_cb.bind('<>', self.on_theme_change) - self.hwlib_label = ttk.Label(self.window, text='Hardware monitoring') + self.hwlib_label = ttk.Label(self, text='Hardware monitoring') self.hwlib_label.place(x=370, y=340) if sys.platform != "win32": del hw_lib_map["LHM"] # LHM is for Windows platforms only - self.hwlib_cb = ttk.Combobox(self.window, values=list(hw_lib_map.values()), state='readonly') + self.hwlib_cb = ttk.Combobox(self, values=list(hw_lib_map.values()), state='readonly') self.hwlib_cb.place(x=550, y=335, width=250) self.hwlib_cb.bind('<>', self.on_hwlib_change) - self.eth_label = ttk.Label(self.window, text='Ethernet interface') + self.eth_label = ttk.Label(self, text='Ethernet interface') self.eth_label.place(x=370, y=380) - self.eth_cb = ttk.Combobox(self.window, values=get_net_if(), state='readonly') + self.eth_cb = ttk.Combobox(self, values=get_net_if(), state='readonly') self.eth_cb.place(x=550, y=375, width=250) - self.wl_label = ttk.Label(self.window, text='Wi-Fi interface') + self.wl_label = ttk.Label(self, text='Wi-Fi interface') self.wl_label.place(x=370, y=420) - self.wl_cb = ttk.Combobox(self.window, values=get_net_if(), state='readonly') + self.wl_cb = ttk.Combobox(self, values=get_net_if(), state='readonly') self.wl_cb.place(x=550, y=415, width=250) # For Windows platform only - self.lhm_admin_warning = ttk.Label(self.window, + self.lhm_admin_warning = ttk.Label(self, text="❌ Restart as admin. or select another Hardware monitoring", foreground='#f00') # For platform != Windows - self.cpu_fan_label = ttk.Label(self.window, text='CPU fan (?)') + self.cpu_fan_label = ttk.Label(self, text='CPU fan (?)') self.cpu_fan_label.config(foreground="#a3a3ff", cursor="hand2") - self.cpu_fan_cb = ttk.Combobox(self.window, values=get_fans(), state='readonly') + self.cpu_fan_cb = ttk.Combobox(self, values=get_fans(), state='readonly') self.tooltip = ToolTip(self.cpu_fan_label, msg="If \"None\" is selected, CPU fan was not auto-detected.\n" @@ -283,29 +283,29 @@ def __init__(self): "Fans missing from the list? Install lm-sensors package\n" "and run 'sudo sensors-detect' command, then reboot.") - self.weather_ping_btn = ttk.Button(self.window, text="Weather & ping", + self.weather_ping_btn = ttk.Button(self, text="Weather & ping", command=lambda: self.on_weatherping_click()) self.weather_ping_btn.place(x=80, y=520, height=50, width=130) - self.open_theme_folder_btn = ttk.Button(self.window, text="Open themes\nfolder", + self.open_theme_folder_btn = ttk.Button(self, text="Open themes\nfolder", command=lambda: self.on_open_theme_folder_click()) self.open_theme_folder_btn.place(x=220, y=520, height=50, width=130) - self.edit_theme_btn = ttk.Button(self.window, text="Edit theme", command=lambda: self.on_theme_editor_click()) + self.edit_theme_btn = ttk.Button(self, text="Edit theme", command=lambda: self.on_theme_editor_click()) self.edit_theme_btn.place(x=360, y=520, height=50, width=130) - self.save_btn = ttk.Button(self.window, text="Save settings", command=lambda: self.on_save_click()) + self.save_btn = ttk.Button(self, text="Save settings", command=lambda: self.on_save_click()) self.save_btn.place(x=500, y=520, height=50, width=130) - self.save_run_btn = ttk.Button(self.window, text="Save and run", command=lambda: self.on_saverun_click()) + self.save_run_btn = ttk.Button(self, text="Save and run", command=lambda: self.on_saverun_click()) self.save_run_btn.place(x=640, y=520, height=50, width=130) self.config = None self.load_config_values() def run(self): - self.window.mainloop() + self.mainloop() def load_theme_preview(self): theme_data = get_theme_data(self.theme_cb.get()) @@ -485,7 +485,7 @@ def on_save_click(self): def on_saverun_click(self): self.save_config_values() subprocess.Popen(f'"{MAIN_DIRECTORY}{glob.glob("main.*", root_dir=MAIN_DIRECTORY)[0]}"', shell=True) - self.window.destroy() + self.destroy() def on_brightness_change(self, e=None): self.brightness_string.set(str(int(self.brightness_slider.get())) + "%") @@ -556,104 +556,104 @@ def on_fan_speed_update(self): self.cpu_fan_cb.config(values=get_fans()) if prev_value != -1: self.cpu_fan_cb.current(prev_value) # Force select same index to refresh displayed value - self.window.after(500, self.on_fan_speed_update) + self.after(500, self.on_fan_speed_update) -class MoreConfigWindow: +class MoreConfigWindow(Toplevel): def __init__(self, main_window: TuringConfigWindow): - self.window = Toplevel() - self.window.withdraw() - self.window.title('Configure weather & ping') - self.window.geometry("750x680") + super().__init__(main_window) + self.withdraw() + self.title('Configure weather & ping') + self.geometry("750x680") self.main_window = main_window # Make TK look better with Sun Valley ttk theme sv_ttk.set_theme("light") - self.ping_label = ttk.Label(self.window, text='Hostname / IP to ping') + self.ping_label = ttk.Label(self, text='Hostname / IP to ping') self.ping_label.place(x=10, y=10) - self.ping_entry = ttk.Entry(self.window) + self.ping_entry = ttk.Entry(self) self.ping_entry.place(x=190, y=5, width=250) - weather_label = ttk.Label(self.window, text='Weather forecast (OpenWeatherMap API)', font='bold') + weather_label = ttk.Label(self, text='Weather forecast (OpenWeatherMap API)', font='bold') weather_label.place(x=10, y=70) - weather_info_label = ttk.Label(self.window, + weather_info_label = ttk.Label(self, text="To display weather forecast on themes that support it, you need an OpenWeatherMap \"One Call API 3.0\" key.\n" "You will get 1,000 API calls per day for free. This program is configured to stay under this threshold (~300 calls/day).") weather_info_label.place(x=10, y=100) - weather_api_link_label = ttk.Label(self.window, + weather_api_link_label = ttk.Label(self, text="Click here to subscribe to OpenWeatherMap One Call API 3.0.") weather_api_link_label.place(x=10, y=140) weather_api_link_label.config(foreground="#a3a3ff", cursor="hand2") weather_api_link_label.bind("", lambda e: webbrowser.open_new_tab("https://openweathermap.org/api")) - self.api_label = ttk.Label(self.window, text='OpenWeatherMap API key') + self.api_label = ttk.Label(self, text='OpenWeatherMap API key') self.api_label.place(x=10, y=170) - self.api_entry = ttk.Entry(self.window) + self.api_entry = ttk.Entry(self) self.api_entry.place(x=190, y=165, width=250) - latlong_label = ttk.Label(self.window, + latlong_label = ttk.Label(self, text="You can use online services to get your latitude/longitude e.g. latlong.net (click here)") latlong_label.place(x=10, y=210) latlong_label.config(foreground="#a3a3ff", cursor="hand2") latlong_label.bind("", lambda e: webbrowser.open_new_tab("https://www.latlong.net/")) - self.lat_label = ttk.Label(self.window, text='Latitude') + self.lat_label = ttk.Label(self, text='Latitude') self.lat_label.place(x=10, y=250) - self.lat_entry = ttk.Entry(self.window, validate='key', - validatecommand=(self.window.register(self.validateCoord), '%P')) + self.lat_entry = ttk.Entry(self, validate='key', + validatecommand=(self.register(self.validateCoord), '%P')) self.lat_entry.place(x=80, y=245, width=100) - self.long_label = ttk.Label(self.window, text='Longitude') + self.long_label = ttk.Label(self, text='Longitude') self.long_label.place(x=270, y=250) - self.long_entry = ttk.Entry(self.window, validate='key', - validatecommand=(self.window.register(self.validateCoord), '%P')) + self.long_entry = ttk.Entry(self, validate='key', + validatecommand=(self.register(self.validateCoord), '%P')) self.long_entry.place(x=340, y=245, width=100) - self.unit_label = ttk.Label(self.window, text='Units') + self.unit_label = ttk.Label(self, text='Units') self.unit_label.place(x=10, y=290) - self.unit_cb = ttk.Combobox(self.window, values=list(weather_unit_map.values()), state='readonly') + self.unit_cb = ttk.Combobox(self, values=list(weather_unit_map.values()), state='readonly') self.unit_cb.place(x=190, y=285, width=250) - self.lang_label = ttk.Label(self.window, text='Language') + self.lang_label = ttk.Label(self, text='Language') self.lang_label.place(x=10, y=330) - self.lang_cb = ttk.Combobox(self.window, values=list(weather_lang_map.values()), state='readonly') + self.lang_cb = ttk.Combobox(self, values=list(weather_lang_map.values()), state='readonly') self.lang_cb.place(x=190, y=325, width=250) - self.citysearch1_label = ttk.Label(self.window, text='Location search', font='bold') + self.citysearch1_label = ttk.Label(self, text='Location search', font='bold') self.citysearch1_label.place(x=80, y=370) - self.citysearch2_label = ttk.Label(self.window, text="Enter location to automatically get coordinates (latitude/longitude).\n" + self.citysearch2_label = ttk.Label(self, text="Enter location to automatically get coordinates (latitude/longitude).\n" "For example \"Berlin\" \"London, GB\", \"London, Quebec\".\n" "Remember to set valid API key and pick language first!") self.citysearch2_label.place(x=10, y=396) - self.citysearch3_label = ttk.Label(self.window, text="Enter location") + self.citysearch3_label = ttk.Label(self, text="Enter location") self.citysearch3_label.place(x=10, y=474) - self.citysearch_entry = ttk.Entry(self.window) + self.citysearch_entry = ttk.Entry(self) self.citysearch_entry.place(x=140, y=470, width=300) - self.citysearch_btn = ttk.Button(self.window, text="Search", command=lambda: self.on_search_click()) + self.citysearch_btn = ttk.Button(self, text="Search", command=lambda: self.on_search_click()) self.citysearch_btn.place(x=450, y=468, height=40, width=130) - self.citysearch4_label = ttk.Label(self.window, text="Select location\n(use after Search)") + self.citysearch4_label = ttk.Label(self, text="Select location\n(use after Search)") self.citysearch4_label.place(x=10, y=540) - self.citysearch_cb = ttk.Combobox(self.window, values=[], state='readonly') + self.citysearch_cb = ttk.Combobox(self, values=[], state='readonly') self.citysearch_cb.place(x=140, y=544, width=360) - self.citysearch_btn2 = ttk.Button(self.window, text="Fill in lat/long", command=lambda: self.on_filllatlong_click()) + self.citysearch_btn2 = ttk.Button(self, text="Fill in lat/long", command=lambda: self.on_filllatlong_click()) self.citysearch_btn2.place(x=520, y=540, height=40, width=130) - self.citysearch_warn_label = ttk.Label(self.window, text="") + self.citysearch_warn_label = ttk.Label(self, text="") self.citysearch_warn_label.place(x=20, y=600) self.citysearch_warn_label.config(foreground="#ff0000") - self.save_btn = ttk.Button(self.window, text="Save settings", command=lambda: self.on_save_click()) + self.save_btn = ttk.Button(self, text="Save settings", command=lambda: self.on_save_click()) self.save_btn.place(x=590, y=620, height=50, width=130) - self.window.protocol("WM_DELETE_WINDOW", self.on_closing) + self.protocol("WM_DELETE_WINDOW", self.on_closing) self._city_entries = [] @@ -667,10 +667,10 @@ def validateCoord(self, coord: str): return True def show(self): - self.window.deiconify() + self.deiconify() def on_closing(self): - self.window.withdraw() + self.withdraw() def load_config_values(self, config): self.config = config diff --git a/library/lcd/color.py b/library/lcd/color.py index 5882fdcc..10ca7efe 100644 --- a/library/lcd/color.py +++ b/library/lcd/color.py @@ -22,7 +22,7 @@ def parse_color(color: Color) -> RGBColor: if isinstance(color, tuple) or isinstance(color, list): if len(color) != 3: raise ValueError("RGB color must have 3 values") - return (int(color[0]), int(color[1]), int(color[2])) + return int(color[0]), int(color[1]), int(color[2]) if not isinstance(color, str): raise ValueError("Color must be either an RGB tuple or a string") @@ -43,6 +43,6 @@ def parse_color(color: Color) -> RGBColor: # fallback as a PIL color rgbcolor = ImageColor.getrgb(color) if len(rgbcolor) == 4: - return (rgbcolor[0], rgbcolor[1], rgbcolor[2]) + return rgbcolor[0], rgbcolor[1], rgbcolor[2] return rgbcolor diff --git a/library/lcd/lcd_comm_rev_a.py b/library/lcd/lcd_comm_rev_a.py index 520b8364..2a7377d4 100644 --- a/library/lcd/lcd_comm_rev_a.py +++ b/library/lcd/lcd_comm_rev_a.py @@ -18,9 +18,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import time from enum import Enum -from typing import Optional from serial.tools.list_ports import comports diff --git a/library/lcd/lcd_comm_weact_a.py b/library/lcd/lcd_comm_weact_a.py index 1f76484d..fa7c6f39 100644 --- a/library/lcd/lcd_comm_weact_a.py +++ b/library/lcd/lcd_comm_weact_a.py @@ -198,11 +198,11 @@ def HandleSensorReport(self): if self.lcd_serial.in_waiting > 0: cmd = self.ReadData(1) if ( - cmd != None - and cmd[0] == Command.CMD_ENABLE_HUMITURE_REPORT | Command.CMD_READ + cmd is not None + and cmd[0] == Command.CMD_ENABLE_HUMITURE_REPORT | Command.CMD_READ ): data = self.ReadData(5) - if data != None and len(data) == 5 and data[4] == Command.CMD_END: + if data is not None and len(data) == 5 and data[4] == Command.CMD_END: unpack = struct.unpack(" Date: Tue, 2 Dec 2025 00:40:35 +0800 Subject: [PATCH 02/28] [theme-editor] rewrite the window to a class --- theme-editor.py | 433 ++++++++++++++++++++++++------------------------ 1 file changed, 220 insertions(+), 213 deletions(-) diff --git a/theme-editor.py b/theme-editor.py index 3704737c..8d25fe38 100755 --- a/theme-editor.py +++ b/theme-editor.py @@ -23,6 +23,7 @@ # The preview window is refreshed as soon as the theme file is modified from library.pythoncheck import check_python_version + check_python_version() import locale @@ -36,6 +37,7 @@ try: import tkinter from PIL import ImageTk, Image + from tkinter import Tk except: print( "[ERROR] Tkinter dependency not installed. Please follow troubleshooting page: https://github.com/mathoudebine/turing-smart-screen-python/wiki/Troubleshooting#all-os-tkinter-dependency-not-installed") @@ -77,12 +79,8 @@ from library.display import display # Only import display after hardcoded config is set -RGB_LED_MARGIN = 12 # Resize editor if display is too big (e.g. 8.8" displays are 1920x480), can be changed later by zoom buttons -RESIZE_FACTOR = 2 if (display.lcd.get_width() > 1000 or display.lcd.get_height() > 1000) else 1 - -ERROR_IN_THEME = Image.open("res/docs/error-in-theme.png") def refresh_theme(): @@ -129,255 +127,264 @@ def refresh_theme(): stats.Ping.stats() -if __name__ == "__main__": - def on_closing(): - logger.debug("Exit Theme Editor...") - try: - sys.exit(0) - except: - os._exit(0) +class Viewer(Tk): + def __init__(self): + super().__init__() + self.last_edit_time = 0 + self.theme_file = None + self.error_in_theme = False + self.inited = False + self.RESIZE_FACTOR = 2 if (display.lcd.get_width() > 1000 or display.lcd.get_height() > 1000) else 1 + self.init_env() + self.ERROR_IN_THEME = Image.open("res/docs/error-in-theme.png") + self.RGB_LED_MARGIN = 12 + self.x0 = 0 + self.y0 = 0 + self.y1 = 0 + self.x1 = 0 + self.title("Turing SysMon Theme Editor") + self.iconphoto(True, tkinter.PhotoImage(file=config.MAIN_DIRECTORY / "res/icons/monitor-icon-17865/64.png")) + self.display_width, self.display_height = int(display.lcd.get_width() / self.RESIZE_FACTOR), int( + display.lcd.get_height() / self.RESIZE_FACTOR) + self.geometry( + str(self.display_width + 2 * self.RGB_LED_MARGIN) + "x" + str( + self.display_height + 2 * self.RGB_LED_MARGIN + 80)) + self.protocol("WM_DELETE_WINDOW", self.on_closing) + self.call('wm', 'attributes', '.', '-topmost', '1') # Preview window always on top + self.config(cursor="cross") + led_color = config.THEME_DATA['display'].get("DISPLAY_RGB_LED", (255, 255, 255)) + if isinstance(led_color, str): + led_color = tuple(map(int, led_color.split(', '))) + self.configure(bg='#%02x%02x%02x' % led_color) + self.circular_mask = Image.open(config.MAIN_DIRECTORY / "res/backgrounds/circular-mask.png") - x0 = 0 - y0 = 0 + # Display preview in the window + if not self.error_in_theme: + screen_image = display.lcd.screen_image + if config.THEME_DATA["display"].get("DISPLAY_SIZE", '3.5"') == '2.1"': + # This is a circular screen: apply a circle mask over the preview + screen_image.paste(self.circular_mask, mask=self.circular_mask) + self.display_image = ImageTk.PhotoImage( + screen_image.resize( + (int(screen_image.width / self.RESIZE_FACTOR), int(screen_image.height / self.RESIZE_FACTOR)))) + else: + size = self.display_width if self.display_width < self.display_height else self.display_height + self.display_image = ImageTk.PhotoImage(self.ERROR_IN_THEME.resize((size, size))) + self.viewer_picture = tkinter.Label(self, image=self.display_image, borderwidth=0) + self.viewer_picture.place(x=self.RGB_LED_MARGIN, y=self.RGB_LED_MARGIN) + # Allow to click on preview to show coordinates and draw zones + self.viewer_picture.bind("", self.on_button1_press) + self.viewer_picture.bind("", self.on_button1_press_and_drag) + self.viewer_picture.bind("", self.on_button1_release) - def draw_zone(x0, y0, x1, y1): - x = min(x0, x1) - y = min(y0, y1) - width = max(x0, x1) - min(x0, x1) - height = max(y0, y1) - min(y0, y1) + # Allow to resize editor using mouse wheel or buttons + self.bind_all("", self.on_mousewheel) + + zoom_plus_btn = tkinter.Button(self, text="Zoom +", command=lambda: self.on_zoom_plus()) + zoom_plus_btn.place(x=self.RGB_LED_MARGIN, y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, + width=int(self.display_width / 2)) + + zoom_minus_btn = tkinter.Button(self, text="Zoom -", command=lambda: self.on_zoom_minus()) + zoom_minus_btn.place(x=int(self.display_width / 2) + self.RGB_LED_MARGIN, + y=self.display_height + 2 * self.RGB_LED_MARGIN, + height=30, width=int(self.display_width / 2)) + + self.label_coord = tkinter.Label(self, text="Click or draw a zone to show coordinates") + self.label_coord.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 40, + width=self.display_width + 2 * self.RGB_LED_MARGIN) + + label_info = tkinter.Label(self, text="This preview will reload when theme file is updated") + label_info.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 60, + width=self.display_width + 2 * self.RGB_LED_MARGIN) + + self.label_zone = tkinter.Label(self, bg='#%02x%02x%02x' % tuple(map(lambda x: 255 - x, led_color))) + self.label_zone.bind("", self.on_zone_click) + self.update() + + def init_env(self): + if self.inited: + return + # Apply system locale to this program + locale.setlocale(locale.LC_ALL, '') + + logger.debug("Starting Theme Editor...") + + # Get theme file to edit + self.theme_file = config.THEME_DATA['PATH'] + "theme.yaml" + self.last_edit_time = os.path.getmtime(self.theme_file) + logger.debug("Using theme file " + self.theme_file) + + # Open theme in default editor. You can also open the file manually in another program + logger.debug("Opening theme file in your default editor. If it does not work, open it manually in the " + "editor of your choice") + if platform.system() == 'Darwin': # macOS + subprocess.call(('open', config.MAIN_DIRECTORY / self.theme_file)) + elif platform.system() == 'Windows': # Windows + os.startfile(config.MAIN_DIRECTORY / self.theme_file) + else: # linux variants + subprocess.call(('xdg-open', config.MAIN_DIRECTORY / self.theme_file)) + + # Load theme file and generate first preview + try: + refresh_theme() + self.error_in_theme = False + except Exception as e: + logger.error(f"Error in theme: {e}") + self.error_in_theme = True + self.inited = True + + def refresh(self): + if os.path.exists(self.theme_file) and os.path.getmtime(self.theme_file) > self.last_edit_time: + logger.debug("The theme file has been updated, the preview window will refresh") + try: + refresh_theme() + self.error_in_theme = False + except Exception as e: + logger.error(f"Error in theme: {e}") + self.error_in_theme = True + self.last_edit_time = os.path.getmtime(self.theme_file) + + # Update the preview.png that is in the theme folder + display.lcd.screen_image.save(config.THEME_DATA['PATH'] + "preview.png", "PNG") + + # Display new picture + if not self.error_in_theme: + self.screen_image = display.lcd.screen_image + if config.THEME_DATA["display"].get("DISPLAY_SIZE", '3.5"') == '2.1"': + # This is a circular screen: apply a circle mask over the preview + self.screen_image.paste(self.circular_mask, mask=self.circular_mask) + self.display_image = ImageTk.PhotoImage( + self.screen_image.resize( + (int(self.screen_image.width / self.RESIZE_FACTOR), + int(self.screen_image.height / self.RESIZE_FACTOR)))) + else: + size = self.display_width if self.display_width < self.display_height else self.display_height + self.display_image = ImageTk.PhotoImage(self.ERROR_IN_THEME.resize((size, size))) + self.viewer_picture.config(image=self.display_image) + + # Refresh RGB backplate LEDs color + led_color = config.THEME_DATA['display'].get("DISPLAY_RGB_LED", (255, 255, 255)) + if isinstance(led_color, str): + led_color = tuple(map(int, led_color.split(', '))) + self.configure(bg='#%02x%02x%02x' % led_color) + self.label_zone.configure(bg='#%02x%02x%02x' % tuple(map(lambda x: 255 - x, led_color))) + + def draw_zone(self): + x = min(self.x0, self.x1) + y = min(self.y0, self.y1) + width = max(self.x0, self.x1) - min(self.x0, self.x1) + height = max(self.y0, self.y1) - min(self.y0, self.y1) if width > 0 and height > 0: - label_zone.place(x=x + RGB_LED_MARGIN, y=y + RGB_LED_MARGIN, width=width, height=height) + self.label_zone.place(x=x + self.RGB_LED_MARGIN, y=y + self.RGB_LED_MARGIN, width=width, height=height) else: - label_zone.place_forget() - + self.label_zone.place_forget() - def on_button1_press(event): - global x0, y0 - x0, y0 = event.x, event.y - label_zone.place_forget() + def on_button1_press(self, event): + self.x0, self.y0 = event.x, event.y + self.label_zone.place_forget() - - def on_button1_press_and_drag(event): - display_width, display_height = int(display.lcd.get_width() / RESIZE_FACTOR), int( - display.lcd.get_height() / RESIZE_FACTOR) - x1, y1 = event.x, event.y + def on_button1_press_and_drag(self, event): + display_width, display_height = int(display.lcd.get_width() / self.RESIZE_FACTOR), int( + display.lcd.get_height() / self.RESIZE_FACTOR) + self.x1, self.y1 = event.x, event.y # Do not draw zone outside of theme preview - if x1 < 0: - x1 = 0 - elif x1 >= display_width: - x1 = display_width - 1 - if y1 < 0: - y1 = 0 - elif y1 >= display_height: - y1 = display_height - 1 - - label_coord.config(text='Drawing zone from [{:0.0f},{:0.0f}] to [{:0.0f},{:0.0f}]'.format(x0 * RESIZE_FACTOR, - y0 * RESIZE_FACTOR, - x1 * RESIZE_FACTOR, - y1 * RESIZE_FACTOR)) - draw_zone(x0, y0, x1, y1) - - - def on_button1_release(event): - display_width, display_height = int(display.lcd.get_width() / RESIZE_FACTOR), int( - display.lcd.get_height() / RESIZE_FACTOR) - x1, y1 = event.x, event.y - if x1 != x0 or y1 != y0: + if self.x1 < 0: + self.x1 = 0 + elif self.x1 >= display_width: + self.x1 = display_width - 1 + if self.y1 < 0: + self.y1 = 0 + elif self.y1 >= display_height: + self.y1 = display_height - 1 + + self.label_coord.config( + text='Drawing zone from [{:0.0f},{:0.0f}] to [{:0.0f},{:0.0f}]'.format(self.x0 * self.RESIZE_FACTOR, + self.y0 * self.RESIZE_FACTOR, + self.x1 * self.RESIZE_FACTOR, + self.y1 * self.RESIZE_FACTOR)) + self.draw_zone() + + def on_button1_release(self, event): + display_width, display_height = int(display.lcd.get_width() / self.RESIZE_FACTOR), int( + display.lcd.get_height() / self.RESIZE_FACTOR) + self.x1, self.y1 = event.x, event.y + if self.x1 != self.x0 or self.y1 != self.y0: # Do not draw zone outside of theme preview - if x1 < 0: - x1 = 0 - elif x1 >= display_width: - x1 = display_width - 1 - if y1 < 0: - y1 = 0 - elif y1 >= display_height: - y1 = display_height - 1 + if self.x1 < 0: + self.x1 = 0 + elif self.x1 >= display_width: + self.x1 = display_width - 1 + if self.y1 < 0: + self.y1 = 0 + elif self.y1 >= display_height: + self.y1 = display_height - 1 # Display drawn zone and coordinates - draw_zone(x0, y0, x1, y1) + self.draw_zone() # Display relative zone coordinates, to set in theme - x = min(x0, x1) - y = min(y0, y1) - width = max(x0, x1) - min(x0, x1) - height = max(y0, y1) - min(y0, y1) - - label_coord.config(text='Zone: X={:0.0f}, Y={:0.0f}, width={:0.0f} height={:0.0f}'.format(x * RESIZE_FACTOR, - y * RESIZE_FACTOR, - width * RESIZE_FACTOR, - height * RESIZE_FACTOR)) + x = min(self.x0, self.x1) + y = min(self.y0, self.y1) + width = max(self.x0, self.x1) - min(self.x0, self.x1) + height = max(self.y0, self.y1) - min(self.y0, self.y1) + + self.label_coord.config( + text='Zone: X={:0.0f}, Y={:0.0f}, width={:0.0f} height={:0.0f}'.format(x * self.RESIZE_FACTOR, + y * self.RESIZE_FACTOR, + width * self.RESIZE_FACTOR, + height * self.RESIZE_FACTOR)) else: # Display click coordinates - label_coord.config( - text='X={:0.0f}, Y={:0.0f} (click and drag to draw a zone)'.format(x0 * RESIZE_FACTOR, - y0 * RESIZE_FACTOR)) - + self.label_coord.config( + text='X={:0.0f}, Y={:0.0f} (click and drag to draw a zone)'.format(self.x0 * self.RESIZE_FACTOR, + self.y0 * self.RESIZE_FACTOR)) - def on_zone_click(event): - label_zone.place_forget() + def on_zone_click(self, event): + self.label_zone.place_forget() + def on_closing(self): + logger.debug("Exit Theme Editor...") + try: + sys.exit(0) + except: + os._exit(0) - def on_mousewheel(event): - global RESIZE_FACTOR + def on_mousewheel(self, event): if event.delta > 0: - RESIZE_FACTOR = RESIZE_FACTOR - 0.2 + self.RESIZE_FACTOR += 0.2 else: - RESIZE_FACTOR = RESIZE_FACTOR + 0.2 - - - def on_zoom_plus(): - global RESIZE_FACTOR - RESIZE_FACTOR = RESIZE_FACTOR - 0.2 + self.RESIZE_FACTOR -= 0.2 + def on_zoom_plus(self): + self.RESIZE_FACTOR += 0.2 - def on_zoom_minus(): - global RESIZE_FACTOR - RESIZE_FACTOR = RESIZE_FACTOR + 0.2 + def on_zoom_minus(self): + self.RESIZE_FACTOR -= 0.2 - # Apply system locale to this program - locale.setlocale(locale.LC_ALL, '') - - logger.debug("Starting Theme Editor...") - - # Get theme file to edit - theme_file = config.THEME_DATA['PATH'] + "theme.yaml" - last_edit_time = os.path.getmtime(theme_file) - logger.debug("Using theme file " + theme_file) - - # Open theme in default editor. You can also open the file manually in another program - logger.debug("Opening theme file in your default editor. If it does not work, open it manually in the " - "editor of your choice") - if platform.system() == 'Darwin': # macOS - subprocess.call(('open', config.MAIN_DIRECTORY / theme_file)) - elif platform.system() == 'Windows': # Windows - os.startfile(config.MAIN_DIRECTORY / theme_file) - else: # linux variants - subprocess.call(('xdg-open', config.MAIN_DIRECTORY / theme_file)) - - # Load theme file and generate first preview - try: - refresh_theme() - error_in_theme = False - except Exception as e: - logger.error(f"Error in theme: {e}") - error_in_theme = True - +if __name__ == "__main__": while True: - display_width, display_height = int(display.lcd.get_width() / RESIZE_FACTOR), int( - display.lcd.get_height() / RESIZE_FACTOR) - current_resize_factor = RESIZE_FACTOR - # Create preview window logger.debug("Opening theme preview window with static data") - viewer = tkinter.Tk() - viewer.title("Turing SysMon Theme Editor") - viewer.iconphoto(True, tkinter.PhotoImage(file=config.MAIN_DIRECTORY / "res/icons/monitor-icon-17865/64.png")) - viewer.geometry(str(display_width + 2 * RGB_LED_MARGIN) + "x" + str(display_height + 2 * RGB_LED_MARGIN + 80)) - viewer.protocol("WM_DELETE_WINDOW", on_closing) - viewer.call('wm', 'attributes', '.', '-topmost', '1') # Preview window always on top - viewer.config(cursor="cross") - - # Display RGB backplate LEDs color as background color - led_color = config.THEME_DATA['display'].get("DISPLAY_RGB_LED", (255, 255, 255)) - if isinstance(led_color, str): - led_color = tuple(map(int, led_color.split(', '))) - viewer.configure(bg='#%02x%02x%02x' % led_color) - - circular_mask = Image.open(config.MAIN_DIRECTORY / "res/backgrounds/circular-mask.png") - - # Display preview in the window - if not error_in_theme: - screen_image = display.lcd.screen_image - if config.THEME_DATA["display"].get("DISPLAY_SIZE", '3.5"') == '2.1"': - # This is a circular screen: apply a circle mask over the preview - screen_image.paste(circular_mask, mask=circular_mask) - display_image = ImageTk.PhotoImage( - screen_image.resize( - (int(screen_image.width / RESIZE_FACTOR), int(screen_image.height / RESIZE_FACTOR)))) - else: - size = display_width if display_width < display_height else display_height - display_image = ImageTk.PhotoImage(ERROR_IN_THEME.resize((size, size))) - viewer_picture = tkinter.Label(viewer, image=display_image, borderwidth=0) - viewer_picture.place(x=RGB_LED_MARGIN, y=RGB_LED_MARGIN) - - # Allow to click on preview to show coordinates and draw zones - viewer_picture.bind("", on_button1_press) - viewer_picture.bind("", on_button1_press_and_drag) - viewer_picture.bind("", on_button1_release) - - # Allow to resize editor using mouse wheel or buttons - viewer.bind_all("", on_mousewheel) - - zoom_plus_btn = tkinter.Button(viewer, text="Zoom +", command=lambda: on_zoom_plus()) - zoom_plus_btn.place(x=RGB_LED_MARGIN, y=display_height + 2 * RGB_LED_MARGIN, height=30, - width=int(display_width / 2)) - - zoom_minus_btn = tkinter.Button(viewer, text="Zoom -", command=lambda: on_zoom_minus()) - zoom_minus_btn.place(x=int(display_width / 2) + RGB_LED_MARGIN, y=display_height + 2 * RGB_LED_MARGIN, - height=30, width=int(display_width / 2)) - - label_coord = tkinter.Label(viewer, text="Click or draw a zone to show coordinates") - label_coord.place(x=0, y=display_height + 2 * RGB_LED_MARGIN + 40, - width=display_width + 2 * RGB_LED_MARGIN) - - label_info = tkinter.Label(viewer, text="This preview will reload when theme file is updated") - label_info.place(x=0, y=display_height + 2 * RGB_LED_MARGIN + 60, - width=display_width + 2 * RGB_LED_MARGIN) - - label_zone = tkinter.Label(viewer, bg='#%02x%02x%02x' % tuple(map(lambda x: 255 - x, led_color))) - label_zone.bind("", on_zone_click) + viewer = Viewer() viewer.update() + current_resize_factor = viewer.RESIZE_FACTOR logger.debug( "You can now edit the theme file in the editor. When you save your changes, the preview window will " "update automatically") - while current_resize_factor == RESIZE_FACTOR: + while current_resize_factor == viewer.RESIZE_FACTOR: # Every time the theme file is modified: reload preview - if os.path.exists(theme_file) and os.path.getmtime(theme_file) > last_edit_time: - logger.debug("The theme file has been updated, the preview window will refresh") - try: - refresh_theme() - error_in_theme = False - except Exception as e: - logger.error(f"Error in theme: {e}") - error_in_theme = True - last_edit_time = os.path.getmtime(theme_file) - - # Update the preview.png that is in the theme folder - display.lcd.screen_image.save(config.THEME_DATA['PATH'] + "preview.png", "PNG") - - # Display new picture - if not error_in_theme: - screen_image = display.lcd.screen_image - if config.THEME_DATA["display"].get("DISPLAY_SIZE", '3.5"') == '2.1"': - # This is a circular screen: apply a circle mask over the preview - screen_image.paste(circular_mask, mask=circular_mask) - display_image = ImageTk.PhotoImage( - screen_image.resize( - (int(screen_image.width / RESIZE_FACTOR), int(screen_image.height / RESIZE_FACTOR)))) - else: - size = display_width if display_width < display_height else display_height - display_image = ImageTk.PhotoImage(ERROR_IN_THEME.resize((size, size))) - viewer_picture.config(image=display_image) - - # Refresh RGB backplate LEDs color - led_color = config.THEME_DATA['display'].get("DISPLAY_RGB_LED", (255, 255, 255)) - if isinstance(led_color, str): - led_color = tuple(map(int, led_color.split(', '))) - viewer.configure(bg='#%02x%02x%02x' % led_color) - label_zone.configure(bg='#%02x%02x%02x' % tuple(map(lambda x: 255 - x, led_color))) - + viewer.refresh() # Regularly update the viewer window even if content unchanged, or it will appear as "not responding" viewer.update() - time.sleep(0.1) # Zoom level changed, reload editor logger.info( - f"Zoom level changed from {current_resize_factor:.1f} to {RESIZE_FACTOR:.1f}, reloading theme editor") + f"Zoom level changed from {current_resize_factor:.1f} to {viewer.RESIZE_FACTOR:.1f}, reloading theme editor") viewer.destroy() From 5ce30d4ec4e6f99a00ce72e103ce51a67d07dd4a Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Tue, 2 Dec 2025 00:47:07 +0800 Subject: [PATCH 03/28] [turning-theme-extractor] get it more specific --- tools/turing-theme-extractor.py | 68 ++++++++++++++++----------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/tools/turing-theme-extractor.py b/tools/turing-theme-extractor.py index 9d44df3c..63e06d40 100644 --- a/tools/turing-theme-extractor.py +++ b/tools/turing-theme-extractor.py @@ -28,46 +28,46 @@ PNG_SIGNATURE = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A' PNG_IEND = b'\x49\x45\x4E\x44\xAE\x42\x60\x82' +def main(file_path): + found_png = 0 -if len(sys.argv) != 2: - print("Usage :") - print(" turing-theme-extractor.py path/to/theme-file.data") - print("Examples : ") - print(" turing-theme-extractor.py \"Dragon Ball.data\"") - print(" turing-theme-extractor.py \"Pikachu theme.data\"") - print(" turing-theme-extractor.py NZXT_BLUR.data") - try: - sys.exit(0) - except: - os._exit(0) + with open(file_path, "r+b") as theme_file: + mm = mmap.mmap(theme_file.fileno(), 0) -found_png = 0 + # Find PNG signature in binary data + start_pos = 0 + header_found = mm.find(PNG_SIGNATURE, 0) -with open(sys.argv[1], "r+b") as theme_file: - mm = mmap.mmap(theme_file.fileno(), 0) + while header_found != -1: + print("\nFound PNG header at 0x%06x" % header_found) - # Find PNG signature in binary data - start_pos=0 - header_found = mm.find(PNG_SIGNATURE, 0) + # Find PNG IEND chunk (= end of file) + iend_found = mm.find(PNG_IEND, header_found) + print("Found PNG end-of-file at 0x%06x" % iend_found) - while header_found != -1: - print("\nFound PNG header at 0x%06x" % header_found) + # Extract PNG data to a file + theme_file.seek(header_found) + with open(f'theme_res_{str(header_found)}.png', 'wb') as png_file: + png_file.write(theme_file.read(iend_found - header_found + len(PNG_IEND))) - # Find PNG IEND chunk (= end of file) - iend_found = mm.find(PNG_IEND, header_found) - print("Found PNG end-of-file at 0x%06x" % iend_found) + print("PNG extracted to theme_res_%s.png" % str(header_found)) + found_png = found_png + 1 - # Extract PNG data to a file - theme_file.seek(header_found) - png_file = open('theme_res_' + str(header_found) + '.png', 'wb') - png_file.write(theme_file.read(iend_found - header_found + len(PNG_IEND))) - png_file.close() + # Find next PNG signature (if any) + header_found = mm.find(PNG_SIGNATURE, iend_found) - print("PNG extracted to theme_res_%s.png" % str(header_found)) - found_png = found_png + 1 - - # Find next PNG signature (if any) - header_found = mm.find(PNG_SIGNATURE, iend_found) - - print("\n%d PNG files extracted from theme to current directory" % found_png) + print(f"\n{found_png} PNG files extracted from theme to current directory") +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage :") + print(" turing-theme-extractor.py path/to/theme-file.data") + print("Examples : ") + print(" turing-theme-extractor.py \"Dragon Ball.data\"") + print(" turing-theme-extractor.py \"Pikachu theme.data\"") + print(" turing-theme-extractor.py NZXT_BLUR.data") + try: + sys.exit(0) + except: + os._exit(0) + main(sys.argv[1]) \ No newline at end of file From 9217153cbdea51fb1e82b4dab9b02221f43127af Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Tue, 2 Dec 2025 12:23:29 +0800 Subject: [PATCH 04/28] [theme-editor] use ttk Button instead of TkButton --- theme-editor.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/theme-editor.py b/theme-editor.py index 8d25fe38..0c51f1d1 100755 --- a/theme-editor.py +++ b/theme-editor.py @@ -38,6 +38,7 @@ import tkinter from PIL import ImageTk, Image from tkinter import Tk + from tkinter.ttk import Button, Label except: print( "[ERROR] Tkinter dependency not installed. Please follow troubleshooting page: https://github.com/mathoudebine/turing-smart-screen-python/wiki/Troubleshooting#all-os-tkinter-dependency-not-installed") @@ -171,7 +172,7 @@ def __init__(self): else: size = self.display_width if self.display_width < self.display_height else self.display_height self.display_image = ImageTk.PhotoImage(self.ERROR_IN_THEME.resize((size, size))) - self.viewer_picture = tkinter.Label(self, image=self.display_image, borderwidth=0) + self.viewer_picture = Label(self, image=self.display_image, borderwidth=0) self.viewer_picture.place(x=self.RGB_LED_MARGIN, y=self.RGB_LED_MARGIN) # Allow to click on preview to show coordinates and draw zones @@ -182,20 +183,20 @@ def __init__(self): # Allow to resize editor using mouse wheel or buttons self.bind_all("", self.on_mousewheel) - zoom_plus_btn = tkinter.Button(self, text="Zoom +", command=lambda: self.on_zoom_plus()) + zoom_plus_btn = Button(self, text="Zoom +", command=lambda: self.on_zoom_plus()) zoom_plus_btn.place(x=self.RGB_LED_MARGIN, y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, width=int(self.display_width / 2)) - zoom_minus_btn = tkinter.Button(self, text="Zoom -", command=lambda: self.on_zoom_minus()) + zoom_minus_btn = Button(self, text="Zoom -", command=lambda: self.on_zoom_minus()) zoom_minus_btn.place(x=int(self.display_width / 2) + self.RGB_LED_MARGIN, y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, width=int(self.display_width / 2)) - self.label_coord = tkinter.Label(self, text="Click or draw a zone to show coordinates") + self.label_coord = Label(self, text="Click or draw a zone to show coordinates") self.label_coord.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 40, width=self.display_width + 2 * self.RGB_LED_MARGIN) - label_info = tkinter.Label(self, text="This preview will reload when theme file is updated") + label_info = Label(self, text="This preview will reload when theme file is updated") label_info.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 60, width=self.display_width + 2 * self.RGB_LED_MARGIN) From 5bf2e4a354c543f572afb5dd127c6d95e041e749 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Tue, 2 Dec 2025 12:49:13 +0800 Subject: [PATCH 05/28] [Configure] Remove unused argument e --- configure.py | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/configure.py b/configure.py index 83322595..8786f64a 100755 --- a/configure.py +++ b/configure.py @@ -156,8 +156,7 @@ def get_theme_size(name: str) -> str: def get_com_ports(): com_ports_names = ["Automatic detection"] # Add manual entry on top for automatic detection - com_ports = comports() - for com_port in com_ports: + for com_port in comports(): com_ports_names.append(com_port.name) return com_ports_names @@ -175,7 +174,7 @@ def get_fans(): for entry in entries: fan_list.append("%s/%s (%d%% - %d RPM)" % (name, entry.label, entry.percent, entry.current)) if (is_cpu_fan(entry.label) or is_cpu_fan(name)) and auto_detected_cpu_fan == "None": - auto_detected_cpu_fan = "Auto-detected: %s/%s" % (name, entry.label) + auto_detected_cpu_fan = f"Auto-detected: {name}/{entry.label}" fan_list.insert(0, auto_detected_cpu_fan) # Add manual entry on top if auto-detection succeeded return fan_list @@ -188,7 +187,7 @@ def __init__(self): self.geometry("820x580") self.iconphoto(True, PhotoImage(file=MAIN_DIRECTORY + "res/icons/monitor-icon-17865/64.png")) # When window gets focus again, reload theme preview in case it has been updated by theme editor - self.bind("", self.on_theme_change) + self.bind("", lambda *_:self.on_theme_change()) self.after(0, self.on_fan_speed_update) # Subwindow for weather/ping config. @@ -216,7 +215,7 @@ def __init__(self): self.size_label = ttk.Label(self, text='Smart screen size') self.size_label.place(x=370, y=75) self.size_cb = ttk.Combobox(self, values=size_list, state='readonly') - self.size_cb.bind('<>', self.on_size_change) + self.size_cb.bind('<>', lambda *_:self.on_size_change()) self.size_cb.place(x=550, y=70, width=250) self.com_label = ttk.Label(self, text='COM port') @@ -233,7 +232,7 @@ def __init__(self): self.brightness_label = ttk.Label(self, text='Brightness') self.brightness_label.place(x=370, y=195) self.brightness_slider = ttk.Scale(self, from_=0, to=100, orient=HORIZONTAL, - command=self.on_brightness_change) + command=lambda *_:self.on_brightness_change()) self.brightness_slider.place(x=600, y=195, width=180) self.brightness_val_label = ttk.Label(self, textvariable=self.brightness_string) self.brightness_val_label.place(x=550, y=195) @@ -248,7 +247,7 @@ def __init__(self): self.theme_label.place(x=370, y=300) self.theme_cb = ttk.Combobox(self, state='readonly') self.theme_cb.place(x=550, y=295, width=250) - self.theme_cb.bind('<>', self.on_theme_change) + self.theme_cb.bind('<>', lambda *_:self.on_theme_change()) self.hwlib_label = ttk.Label(self, text='Hardware monitoring') self.hwlib_label.place(x=370, y=340) @@ -256,7 +255,7 @@ def __init__(self): del hw_lib_map["LHM"] # LHM is for Windows platforms only self.hwlib_cb = ttk.Combobox(self, values=list(hw_lib_map.values()), state='readonly') self.hwlib_cb.place(x=550, y=335, width=250) - self.hwlib_cb.bind('<>', self.on_hwlib_change) + self.hwlib_cb.bind('<>', lambda *_:self.on_hwlib_change()) self.eth_label = ttk.Label(self, text='Ethernet interface') self.eth_label.place(x=370, y=380) @@ -311,24 +310,24 @@ def load_theme_preview(self): theme_data = get_theme_data(self.theme_cb.get()) try: - theme_preview = Image.open(MAIN_DIRECTORY + "res/themes/" + self.theme_cb.get() + "/preview.png") + theme_preview = Image.open(f"{MAIN_DIRECTORY}res/themes/{self.theme_cb.get()}/preview.png") if theme_data['display'].get("DISPLAY_SIZE", '3.5"') == SIZE_2_1_INCH: # This is a circular screen: apply a circle mask over the preview theme_preview.paste(circular_mask, mask=circular_mask) except: - theme_preview = Image.open(MAIN_DIRECTORY + "res/docs/no-preview.png") + theme_preview = Image.open(f"{MAIN_DIRECTORY}res/docs/no-preview.png") finally: theme_preview.thumbnail((320, 480), Image.Resampling.LANCZOS) self.theme_preview_img = ImageTk.PhotoImage(theme_preview) self.theme_preview.config(image=self.theme_preview_img) author_name = theme_data.get('author', 'unknown') - self.theme_author.config(text="Author: " + author_name) + self.theme_author.config(text=f"Author: {author_name}") if author_name.startswith("@"): self.theme_author.config(foreground="#a3a3ff", cursor="hand2") self.theme_author.bind("", - lambda e: webbrowser.open_new_tab("https://github.com/" + author_name[1:])) + lambda e: webbrowser.open_new_tab(f"https://github.com/{author_name[1:]}")) else: self.theme_author.config(foreground="#a3a3a3", cursor="") self.theme_author.unbind("") @@ -459,7 +458,7 @@ def save_additional_config(self, ping: str, api_key: str, lat: str, long: str, u with open(MAIN_DIRECTORY + "config.yaml", "w", encoding='utf-8') as file: ruamel.yaml.YAML().dump(self.config, file) - def on_theme_change(self, e=None): + def on_theme_change(self): self.load_theme_preview() def on_weatherping_click(self): @@ -487,11 +486,11 @@ def on_saverun_click(self): subprocess.Popen(f'"{MAIN_DIRECTORY}{glob.glob("main.*", root_dir=MAIN_DIRECTORY)[0]}"', shell=True) self.destroy() - def on_brightness_change(self, e=None): + def on_brightness_change(self): self.brightness_string.set(str(int(self.brightness_slider.get())) + "%") self.show_hide_brightness_warning() - def on_model_change(self, e=None): + def on_model_change(self): self.show_hide_brightness_warning() model = self.model_cb.get() if model == SIMULATED_MODEL: @@ -505,7 +504,7 @@ def on_model_change(self, e=None): self.brightness_slider.configure(state="normal") self.brightness_val_label.configure(foreground="#000") - def on_size_change(self, e=None): + def on_size_change(self): size = self.size_cb.get() size = size.replace(SIZE_2_x_INCH, SIZE_2_1_INCH) # For '2.1" / 2.8"' size, keep '2.1"' as size to get themes for themes = get_themes(size) @@ -517,7 +516,7 @@ def on_size_change(self, e=None): self.show_hide_brightness_warning() - def on_hwlib_change(self, e=None): + def on_hwlib_change(self): hwlib = [k for k, v in hw_lib_map.items() if v == self.hwlib_cb.get()][0] if hwlib == "STUB" or hwlib == "STATIC": self.eth_cb.configure(state="disabled", foreground="#C0C0C0") @@ -529,21 +528,21 @@ def on_hwlib_change(self, e=None): if sys.platform == "win32": import ctypes is_admin = ctypes.windll.shell32.IsUserAnAdmin() != 0 - if (hwlib == "LHM" or hwlib == "AUTO") and not is_admin: + if (hwlib in ["LHM", "AUTO"]) and not is_admin: self.lhm_admin_warning.place(x=370, y=460) self.save_run_btn.state(["disabled"]) else: self.lhm_admin_warning.place_forget() self.save_run_btn.state(["!disabled"]) else: - if hwlib == "PYTHON" or hwlib == "AUTO": + if hwlib in ["PYTHON", "AUTO"]: self.cpu_fan_label.place(x=370, y=460) self.cpu_fan_cb.place(x=550, y=455, width=250) else: self.cpu_fan_label.place_forget() self.cpu_fan_cb.place_forget() - def show_hide_brightness_warning(self, e=None): + def show_hide_brightness_warning(self): if int(self.brightness_slider.get()) > 50 and self.model_cb.get() == TURING_MODEL and self.size_cb.get() == SIZE_3_5_INCH: # Show warning for Turing Smart screen 3.5 with high brightness self.brightness_warning_label.place(x=370, y=225) From 0b07b9ce1d5f46849be739b6a50bc31ec91cd070 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Tue, 2 Dec 2025 13:12:04 +0800 Subject: [PATCH 06/28] [Configure] Remove duplicate function defines --- configure.py | 68 ++++++++++++++++++++++------------------------------ 1 file changed, 28 insertions(+), 40 deletions(-) diff --git a/configure.py b/configure.py index 8786f64a..dc0841d2 100755 --- a/configure.py +++ b/configure.py @@ -22,6 +22,7 @@ # This file is the system monitor configuration GUI from library.pythoncheck import check_python_version + check_python_version() import glob @@ -126,6 +127,7 @@ circular_mask = Image.open(MAIN_DIRECTORY + "res/backgrounds/circular-mask.png") + def get_theme_data(name: str): folder = os.path.join(THEMES_DIR, name) # checking if it is a directory @@ -187,7 +189,7 @@ def __init__(self): self.geometry("820x580") self.iconphoto(True, PhotoImage(file=MAIN_DIRECTORY + "res/icons/monitor-icon-17865/64.png")) # When window gets focus again, reload theme preview in case it has been updated by theme editor - self.bind("", lambda *_:self.on_theme_change()) + self.bind("", lambda *_: self.load_theme_preview()) self.after(0, self.on_fan_speed_update) # Subwindow for weather/ping config. @@ -209,13 +211,13 @@ def __init__(self): self.model_label.place(x=370, y=35) self.model_cb = ttk.Combobox(self, values=list(dict.fromkeys((revision_and_size_to_model_map.values()))), state='readonly') - self.model_cb.bind('<>', self.on_model_change) + self.model_cb.bind('<>', lambda *_: self.on_model_change()) self.model_cb.place(x=550, y=30, width=250) self.size_label = ttk.Label(self, text='Smart screen size') self.size_label.place(x=370, y=75) self.size_cb = ttk.Combobox(self, values=size_list, state='readonly') - self.size_cb.bind('<>', lambda *_:self.on_size_change()) + self.size_cb.bind('<>', lambda *_: self.on_size_change()) self.size_cb.place(x=550, y=70, width=250) self.com_label = ttk.Label(self, text='COM port') @@ -232,7 +234,7 @@ def __init__(self): self.brightness_label = ttk.Label(self, text='Brightness') self.brightness_label.place(x=370, y=195) self.brightness_slider = ttk.Scale(self, from_=0, to=100, orient=HORIZONTAL, - command=lambda *_:self.on_brightness_change()) + command=lambda *_: self.on_brightness_change()) self.brightness_slider.place(x=600, y=195, width=180) self.brightness_val_label = ttk.Label(self, textvariable=self.brightness_string) self.brightness_val_label.place(x=550, y=195) @@ -247,7 +249,7 @@ def __init__(self): self.theme_label.place(x=370, y=300) self.theme_cb = ttk.Combobox(self, state='readonly') self.theme_cb.place(x=550, y=295, width=250) - self.theme_cb.bind('<>', lambda *_:self.on_theme_change()) + self.theme_cb.bind('<>', lambda *_: self.load_theme_preview()) self.hwlib_label = ttk.Label(self, text='Hardware monitoring') self.hwlib_label.place(x=370, y=340) @@ -255,7 +257,7 @@ def __init__(self): del hw_lib_map["LHM"] # LHM is for Windows platforms only self.hwlib_cb = ttk.Combobox(self, values=list(hw_lib_map.values()), state='readonly') self.hwlib_cb.place(x=550, y=335, width=250) - self.hwlib_cb.bind('<>', lambda *_:self.on_hwlib_change()) + self.hwlib_cb.bind('<>', lambda *_: self.on_hwlib_change()) self.eth_label = ttk.Label(self, text='Ethernet interface') self.eth_label.place(x=370, y=380) @@ -283,18 +285,17 @@ def __init__(self): "and run 'sudo sensors-detect' command, then reboot.") self.weather_ping_btn = ttk.Button(self, text="Weather & ping", - command=lambda: self.on_weatherping_click()) + command=lambda: self.more_config_window.deiconify()) self.weather_ping_btn.place(x=80, y=520, height=50, width=130) - self.open_theme_folder_btn = ttk.Button(self, text="Open themes\nfolder", - command=lambda: self.on_open_theme_folder_click()) + command=lambda: self.on_open_theme_folder_click()) self.open_theme_folder_btn.place(x=220, y=520, height=50, width=130) self.edit_theme_btn = ttk.Button(self, text="Edit theme", command=lambda: self.on_theme_editor_click()) self.edit_theme_btn.place(x=360, y=520, height=50, width=130) - self.save_btn = ttk.Button(self, text="Save settings", command=lambda: self.on_save_click()) + self.save_btn = ttk.Button(self, text="Save settings", command=lambda: self.save_config_values()) self.save_btn.place(x=500, y=520, height=50, width=130) self.save_run_btn = ttk.Button(self, text="Save and run", command=lambda: self.on_saverun_click()) @@ -380,7 +381,7 @@ def load_config_values(self): # Guess display size from theme in the configuration size = get_theme_size(self.config['config']['THEME']) - size = size.replace(SIZE_2_1_INCH, SIZE_2_x_INCH) # If a theme is for 2.1" then it also is for 2.8" + size = size.replace(SIZE_2_1_INCH, SIZE_2_x_INCH) # If a theme is for 2.1" then it also is for 2.8" try: self.size_cb.set(size) except: @@ -414,7 +415,7 @@ def load_config_values(self): # Reload content on screen self.on_model_change() self.on_size_change() - self.on_theme_change() + self.load_theme_preview() self.on_brightness_change() self.on_hwlib_change() @@ -458,12 +459,6 @@ def save_additional_config(self, ping: str, api_key: str, lat: str, long: str, u with open(MAIN_DIRECTORY + "config.yaml", "w", encoding='utf-8') as file: ruamel.yaml.YAML().dump(self.config, file) - def on_theme_change(self): - self.load_theme_preview() - - def on_weatherping_click(self): - self.more_config_window.show() - def on_open_theme_folder_click(self): path = f'"{MAIN_DIRECTORY}res/themes"' if platform.system() == "Windows": @@ -478,9 +473,6 @@ def on_theme_editor_click(self): f'"{MAIN_DIRECTORY}{glob.glob("theme-editor.*", root_dir=MAIN_DIRECTORY)[0]}" "{self.theme_cb.get()}"', shell=True) - def on_save_click(self): - self.save_config_values() - def on_saverun_click(self): self.save_config_values() subprocess.Popen(f'"{MAIN_DIRECTORY}{glob.glob("main.*", root_dir=MAIN_DIRECTORY)[0]}"', shell=True) @@ -506,7 +498,8 @@ def on_model_change(self): def on_size_change(self): size = self.size_cb.get() - size = size.replace(SIZE_2_x_INCH, SIZE_2_1_INCH) # For '2.1" / 2.8"' size, keep '2.1"' as size to get themes for + size = size.replace(SIZE_2_x_INCH, + SIZE_2_1_INCH) # For '2.1" / 2.8"' size, keep '2.1"' as size to get themes for themes = get_themes(size) self.theme_cb.config(values=themes) @@ -518,7 +511,7 @@ def on_size_change(self): def on_hwlib_change(self): hwlib = [k for k, v in hw_lib_map.items() if v == self.hwlib_cb.get()][0] - if hwlib == "STUB" or hwlib == "STATIC": + if hwlib in ["STUB", "STATIC"]: self.eth_cb.configure(state="disabled", foreground="#C0C0C0") self.wl_cb.configure(state="disabled", foreground="#C0C0C0") else: @@ -626,9 +619,10 @@ def __init__(self, main_window: TuringConfigWindow): self.citysearch1_label = ttk.Label(self, text='Location search', font='bold') self.citysearch1_label.place(x=80, y=370) - self.citysearch2_label = ttk.Label(self, text="Enter location to automatically get coordinates (latitude/longitude).\n" - "For example \"Berlin\" \"London, GB\", \"London, Quebec\".\n" - "Remember to set valid API key and pick language first!") + self.citysearch2_label = ttk.Label(self, + text="Enter location to automatically get coordinates (latitude/longitude).\n" + "For example \"Berlin\" \"London, GB\", \"London, Quebec\".\n" + "Remember to set valid API key and pick language first!") self.citysearch2_label.place(x=10, y=396) self.citysearch3_label = ttk.Label(self, text="Enter location") @@ -652,7 +646,7 @@ def __init__(self, main_window: TuringConfigWindow): self.save_btn = ttk.Button(self, text="Save settings", command=lambda: self.on_save_click()) self.save_btn.place(x=590, y=620, height=50, width=130) - self.protocol("WM_DELETE_WINDOW", self.on_closing) + self.protocol("WM_DELETE_WINDOW", self.withdraw) self._city_entries = [] @@ -665,12 +659,6 @@ def validateCoord(self, coord: str): return False return True - def show(self): - self.deiconify() - - def on_closing(self): - self.withdraw() - def load_config_values(self, config): self.config = config @@ -703,10 +691,10 @@ def load_config_values(self, config): self.lang_cb.set(weather_lang_map[self.config['config']['WEATHER_LANGUAGE']]) except: self.lang_cb.set(weather_lang_map["en"]) - + def citysearch_show_warning(self, warning): self.citysearch_warn_label.config(text=warning) - + def on_search_click(self): OPENWEATHER_GEOAPI_URL = "http://api.openweathermap.org/geo/1.0/direct" api_key = self.api_entry.get() @@ -718,8 +706,8 @@ def on_search_click(self): return try: - request = requests.get(OPENWEATHER_GEOAPI_URL, timeout=5, params={"appid": api_key, "lang": lang, - "q": city, "limit": 10}) + request = requests.get(OPENWEATHER_GEOAPI_URL, timeout=5, params={"appid": api_key, "lang": lang, + "q": city, "limit": 10}) except: self.citysearch_show_warning("Error fetching OpenWeatherMap Geo API") return @@ -730,7 +718,7 @@ def on_search_click(self): elif request.status_code != 200: self.citysearch_show_warning(f"Error #{request.status_code} fetching OpenWeatherMap Geo API.") return - + self._city_entries = [] cb_entries = [] for entry in request.json(): @@ -747,7 +735,7 @@ def on_search_click(self): self._city_entries.append({"full_name": full_name, "lat": str(lat), "long": str(long)}) cb_entries.append(full_name) - self.citysearch_cb.config(values = cb_entries) + self.citysearch_cb.config(values=cb_entries) if len(cb_entries) == 0: self.citysearch_show_warning("No given city found.") else: @@ -767,7 +755,7 @@ def on_filllatlong_click(self): def on_save_click(self): self.save_config_values() - self.on_closing() + self.withdraw() def save_config_values(self): ping = self.ping_entry.get() From 388c3aa02e79322a0d9f2308d519e1dc3e979c9f Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Tue, 2 Dec 2025 13:19:52 +0800 Subject: [PATCH 07/28] [display] use dict instead of judge --- library/display.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/library/display.py b/library/display.py index d4f21009..06cfe89d 100644 --- a/library/display.py +++ b/library/display.py @@ -55,20 +55,17 @@ def _get_theme_orientation() -> Orientation: def _get_theme_size() -> tuple[int, int]: - if config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '0.96"': - return 80, 160 - if config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '2.1"': - return 480, 480 - elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '3.5"': - return 320, 480 - elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '5"': - return 480, 800 - elif config.THEME_DATA["display"].get("DISPLAY_SIZE", '') == '8.8"': - return 480, 1920 - else: + sizes = { + '0.96"':(80, 160), + '2.1"':(480, 480), + '3.5"':(320, 480), + '5"':(480, 800), + '8.8"':(480, 1920), + } + if config.THEME_DATA["display"].get("DISPLAY_SIZE", '') not in sizes.keys(): logger.warning( - f'Cannot find valid DISPLAY_SIZE property in selected theme {config.CONFIG_DATA["config"]["THEME"]}, defaulting to 3.5"') - return 320, 480 + f'Cannot find valid DISPLAY_SIZE property in selected theme {config.CONFIG_DATA["config"]["THEME"]}, defaulting to 3.5"') + return sizes.get(config.THEME_DATA["display"].get("DISPLAY_SIZE", ''), (320, 480)) class Display: From ccd726c70577ec8e15971f6bef696b73c8a9ae96 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Tue, 2 Dec 2025 13:20:25 +0800 Subject: [PATCH 08/28] [config] use isinstance instead of type --- library/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/config.py b/library/config.py index c8920032..0415876b 100644 --- a/library/config.py +++ b/library/config.py @@ -48,7 +48,7 @@ def copy_default(default, theme): for k, v in default.items(): if k not in theme: theme[k] = v - if type(v) == type({}): + if isinstance(v, dict): copy_default(default[k], theme[k]) From b1402f77e08a18d2f6acdac2cecd500c76f094cb Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Tue, 2 Dec 2025 13:26:16 +0800 Subject: [PATCH 09/28] [lcd_simulated] use f-string instead of + --- library/lcd/lcd_simulated.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/library/lcd/lcd_simulated.py b/library/lcd/lcd_simulated.py index fab873e1..d25abd46 100644 --- a/library/lcd/lcd_simulated.py +++ b/library/lcd/lcd_simulated.py @@ -38,11 +38,11 @@ def do_GET(self): self.send_response(200) self.send_header("Content-type", "text/html") self.end_headers() - self.wfile.write(bytes("", "utf-8")) + self.wfile.write(bytes(f"", "utf-8")) self.wfile.write(bytes("", "utf-8")) elif self.path.startswith("/" + SCREENSHOT_FILE): @@ -67,7 +67,7 @@ def __init__(self, com_port: str = "AUTO", display_width: int = 320, display_hei try: self.webServer = HTTPServer(("localhost", WEBSERVER_PORT), SimulatedLcdWebServer) - logger.debug("To see your simulated screen, open http://%s:%d in a browser" % ("localhost", WEBSERVER_PORT)) + logger.debug(f"To see your simulated screen, open http://localhost:{WEBSERVER_PORT} in a browser") threading.Thread(target=self.webServer.serve_forever).start() except OSError: logger.error("Error starting webserver! An instance might already be running on port %d." % WEBSERVER_PORT) From 005c64ef547945a738eb8925d22e5ea2a222cb38 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Tue, 2 Dec 2025 13:32:00 +0800 Subject: [PATCH 10/28] [lcd_comm_rev_c] use dict instead of judge --- library/lcd/lcd_comm_rev_c.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/library/lcd/lcd_comm_rev_c.py b/library/lcd/lcd_comm_rev_c.py index 75fe73a2..8d93318d 100644 --- a/library/lcd/lcd_comm_rev_c.py +++ b/library/lcd/lcd_comm_rev_c.py @@ -220,7 +220,7 @@ def _hello(self): response = ''.join( filter(lambda x: x in set(string.printable), str(self.serial_read(23).decode(errors="ignore")))) self.serial_flush_input() - logger.debug("Display ID returned: %s" % response) + logger.debug(f"Display ID returned: {response}") while not response.startswith("chs_"): logger.warning("Display returned invalid or unsupported ID, try again in 1 second") time.sleep(1) @@ -232,13 +232,13 @@ def _hello(self): # Note: ID returned by display are not reliable for some models e.g. 2.1" displays return "chs_5inch" # Rely on width/height for sub-revision detection - if self.display_width == 480 and self.display_height == 480: - self.sub_revision = SubRevision.REV_2INCH - elif self.display_width == 480 and self.display_height == 800: - self.sub_revision = SubRevision.REV_5INCH - elif self.display_width == 480 and self.display_height == 1920: - self.sub_revision = SubRevision.REV_8INCH - else: + sub_revisions = { + (480, 480):SubRevision.REV_2INCH, + (480,800):SubRevision.REV_5INCH, + (480,1920):SubRevision.REV_8INCH + } + self.sub_revision = sub_revisions.get((self.display_width, self.display_height), SubRevision.UNKNOWN) + if self.sub_revision == SubRevision.UNKNOWN: logger.error(f"Unsupported resolution {self.display_width}x{self.display_height} for revision C") # Detect ROM version From c4bd5f29c434a8f378ffef5612356367c24024ad Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Tue, 2 Dec 2025 18:30:46 +0800 Subject: [PATCH 11/28] [test_librehardwaremonitor] add os detect --- external/LibreHardwareMonitor/test_librehardwaremonitor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/external/LibreHardwareMonitor/test_librehardwaremonitor.py b/external/LibreHardwareMonitor/test_librehardwaremonitor.py index 71a71ebc..d7a32724 100644 --- a/external/LibreHardwareMonitor/test_librehardwaremonitor.py +++ b/external/LibreHardwareMonitor/test_librehardwaremonitor.py @@ -6,7 +6,8 @@ import os import sys from pathlib import Path - +if os.name != 'nt': + raise Exception("This script is only for Windows") import clr # Clr is from pythonnet package. Do not install clr package from win32api import * From 3df591f9fe0e3709dd8e6c7197fb425c518031f1 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Tue, 2 Dec 2025 18:39:25 +0800 Subject: [PATCH 12/28] [config] feat: add app theme switcher --- configure.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/configure.py b/configure.py index dc0841d2..ba314bef 100755 --- a/configure.py +++ b/configure.py @@ -196,7 +196,8 @@ def __init__(self): self.more_config_window = MoreConfigWindow(self) # Make TK look better with Sun Valley ttk theme - sv_ttk.set_theme("light") + self.app_theme = StringVar(value='light') + sv_ttk.set_theme(self.app_theme.get()) self.theme_preview_img = None self.theme_preview = ttk.Label(self) @@ -300,6 +301,8 @@ def __init__(self): self.save_run_btn = ttk.Button(self, text="Save and run", command=lambda: self.on_saverun_click()) self.save_run_btn.place(x=640, y=520, height=50, width=130) + self.change_app_theme = ttk.Button(self, textvariable=self.app_theme, command=lambda: self.on_change_theme()) + self.change_app_theme.place(x=5, y=520, height=50, width=70) self.config = None self.load_config_values() @@ -473,6 +476,13 @@ def on_theme_editor_click(self): f'"{MAIN_DIRECTORY}{glob.glob("theme-editor.*", root_dir=MAIN_DIRECTORY)[0]}" "{self.theme_cb.get()}"', shell=True) + def on_change_theme(self): + if self.app_theme.get() == 'light': + self.app_theme.set('dark') + else: + self.app_theme.set('light') + sv_ttk.set_theme(self.app_theme.get()) + def on_saverun_click(self): self.save_config_values() subprocess.Popen(f'"{MAIN_DIRECTORY}{glob.glob("main.*", root_dir=MAIN_DIRECTORY)[0]}"', shell=True) @@ -559,9 +569,9 @@ def __init__(self, main_window: TuringConfigWindow): self.geometry("750x680") self.main_window = main_window - + self.app_theme = StringVar(value='light') # Make TK look better with Sun Valley ttk theme - sv_ttk.set_theme("light") + sv_ttk.set_theme(self.app_theme.get()) self.ping_label = ttk.Label(self, text='Hostname / IP to ping') self.ping_label.place(x=10, y=10) From 6f0bda566f1cc1274262fb22cad278c57c0d5177 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Tue, 2 Dec 2025 23:16:34 +0800 Subject: [PATCH 13/28] [MAIN] rewrite it to classes --- configure.py | 5 +- main.py | 309 +++++++++++++++++++++++++++------------------------ 2 files changed, 163 insertions(+), 151 deletions(-) diff --git a/configure.py b/configure.py index ba314bef..1a33d8ce 100755 --- a/configure.py +++ b/configure.py @@ -489,7 +489,7 @@ def on_saverun_click(self): self.destroy() def on_brightness_change(self): - self.brightness_string.set(str(int(self.brightness_slider.get())) + "%") + self.brightness_string.set(f"{int(self.brightness_slider.get())} %") self.show_hide_brightness_warning() def on_model_change(self): @@ -569,9 +569,6 @@ def __init__(self, main_window: TuringConfigWindow): self.geometry("750x680") self.main_window = main_window - self.app_theme = StringVar(value='light') - # Make TK look better with Sun Valley ttk theme - sv_ttk.set_theme(self.app_theme.get()) self.ping_label = ttk.Label(self, text='Hostname / IP to ping') self.ping_label.place(x=10, y=10) diff --git a/main.py b/main.py index 38676496..eae5307c 100755 --- a/main.py +++ b/main.py @@ -27,23 +27,24 @@ # This file is the system monitor main program to display HW sensors on your screen using themes (see README) from library.pythoncheck import check_python_version + check_python_version() import glob import os import sys +import platform try: import atexit import locale - import platform import signal import subprocess import time from pathlib import Path from PIL import Image - if platform.system() == 'Windows': + if os.name == 'nt': import win32api import win32con import win32gui @@ -70,26 +71,26 @@ MAIN_DIRECTORY = str(Path(__file__).parent.resolve()) + "/" -if __name__ == "__main__": - - # Apply system locale to this program - locale.setlocale(locale.LC_ALL, '') - logger.debug("Using Python %s" % sys.version) +class Main: + def __init__(self): + # Apply system locale to this program + locale.setlocale(locale.LC_ALL, '') + logger.debug("Using Python %s" % sys.version) - def wait_for_empty_queue(timeout: int = 5): + def wait_for_empty_queue(self, timeout: int = 5): # Waiting for all pending request to be sent to display logger.info("Waiting for all pending request to be sent to display (%ds max)..." % timeout) wait_time = 0 while not scheduler.is_queue_empty() and wait_time < timeout: time.sleep(0.1) - wait_time = wait_time + 0.1 + wait_time += 0.1 logger.debug("(Waited %.1fs)" % wait_time) - def clean_stop(tray_icon=None): + def clean_stop(self, tray_icon=None): # Turn screen and LEDs off before stopping display.turn_off() @@ -98,7 +99,7 @@ def clean_stop(tray_icon=None): scheduler.STOPPING = True # Waiting for all pending request to be sent to display - wait_for_empty_queue(5) + self.wait_for_empty_queue(5) # Remove tray icon just before exit if tray_icon: @@ -110,38 +111,32 @@ def clean_stop(tray_icon=None): except: os._exit(0) + def on_signal_caught(self, signum, frame=None): + logger.info(f"Caught signal {signum}, exiting") + self.clean_stop() - def on_signal_caught(signum, frame=None): - logger.info("Caught signal %d, exiting" % signum) - clean_stop() - - - def on_configure_tray(tray_icon, item): + def on_configure_tray(self, tray_icon, item): logger.info("Configure from tray icon") subprocess.Popen(f'"{MAIN_DIRECTORY}{glob.glob("configure.*", root_dir=MAIN_DIRECTORY)[0]}"', shell=True) - clean_stop(tray_icon) + self.clean_stop(tray_icon) - - def on_exit_tray(tray_icon, item): + def on_exit_tray(self, tray_icon, item): logger.info("Exit from tray icon") - clean_stop(tray_icon) - + self.clean_stop(tray_icon) - def on_clean_exit(*args): + def on_clean_exit(self, *args): logger.info("Program will now exit") - clean_stop() + self.clean_stop() - - if platform.system() == "Windows": - def on_win32_ctrl_event(event): + if os.name == 'nt': + def on_win32_ctrl_event(self, event): """Handle Windows console control events (like Ctrl-C).""" if event in (win32con.CTRL_C_EVENT, win32con.CTRL_BREAK_EVENT, win32con.CTRL_CLOSE_EVENT): logger.debug("Caught Windows control event %s, exiting" % event) - clean_stop() + self.clean_stop() return 0 - - def on_win32_wm_event(hWnd, msg, wParam, lParam): + def on_win32_wm_event(self, hWnd, msg, wParam, lParam): """Handle Windows window message events (like ENDSESSION, CLOSE, DESTROY).""" logger.debug("Caught Windows window message event %s" % msg) if msg == win32con.WM_POWERBROADCAST: @@ -158,124 +153,144 @@ def on_win32_wm_event(hWnd, msg, wParam, lParam): else: # For any other events, the program will stop logger.info("Program will now exit") - clean_stop() + self.clean_stop() - # Create a tray icon for the program, with an Exit entry in menu - try: - tray_icon = pystray.Icon( - name='Turing System Monitor', - title='Turing System Monitor', - icon=Image.open(MAIN_DIRECTORY + "res/icons/monitor-icon-17865/64.png"), - menu=pystray.Menu( - pystray.MenuItem( - text='Configure', - action=on_configure_tray), - pystray.Menu.SEPARATOR, - pystray.MenuItem( - text='Exit', - action=on_exit_tray) + def main(self): + + # Create a tray icon for the program, with an Exit entry in menu + try: + tray_icon = pystray.Icon( + name='Turing System Monitor', + title='Turing System Monitor', + icon=Image.open(MAIN_DIRECTORY + "res/icons/monitor-icon-17865/64.png"), + menu=pystray.Menu( + pystray.MenuItem( + text='Configure', + action=self.on_configure_tray), + pystray.Menu.SEPARATOR, + pystray.MenuItem( + text='Exit', + action=self.on_exit_tray) + ) ) - ) - # For platforms != macOS, display the tray icon now with non-blocking function - if platform.system() != "Darwin": - tray_icon.run_detached() - logger.info("Tray icon has been displayed") - except: - tray_icon = None - logger.warning("Tray icon is not supported on your platform") - - # Set the different stopping event handlers, to send a complete frame to the LCD before exit - atexit.register(on_clean_exit) - signal.signal(signal.SIGINT, on_signal_caught) - signal.signal(signal.SIGTERM, on_signal_caught) - is_posix = os.name == 'posix' - if is_posix: - signal.signal(signal.SIGQUIT, on_signal_caught) - if platform.system() == "Windows": - win32api.SetConsoleCtrlHandler(on_win32_ctrl_event, True) - - # Initialize the display - logger.info("Initialize display") - display.initialize_display() - - # Start serial queue handler - scheduler.QueueHandler() - - # Create all static images - display.display_static_images() - - # Create all static texts - display.display_static_text() - - # Wait for static images/text to be displayed before starting monitoring (to avoid filling the queue while waiting) - wait_for_empty_queue(10) - - # Start sensor scheduled reading. Avoid starting them all at the same time to optimize load - logger.info("Starting system monitoring") - import library.stats as stats - - scheduler.CPUPercentage(); time.sleep(0.25) - scheduler.CPUFrequency(); time.sleep(0.25) - scheduler.CPULoad(); time.sleep(0.25) - scheduler.CPUTemperature(); time.sleep(0.25) - scheduler.CPUFanSpeed(); time.sleep(0.25) - if stats.Gpu.is_available(): - scheduler.GpuStats(); time.sleep(0.25) - scheduler.MemoryStats(); time.sleep(0.25) - scheduler.DiskStats(); time.sleep(0.25) - scheduler.NetStats(); time.sleep(0.25) - scheduler.DateStats(); time.sleep(0.25) - scheduler.SystemUptimeStats(); time.sleep(0.25) - scheduler.CustomStats(); time.sleep(0.25) - scheduler.WeatherStats(); time.sleep(0.25) - scheduler.PingStats(); time.sleep(0.25) - - # OS-specific tasks - if tray_icon and platform.system() == "Darwin": # macOS-specific - from AppKit import NSBundle, NSApp, NSApplicationActivationPolicyProhibited - - # Hide Python Launcher icon from macOS dock - info = NSBundle.mainBundle().infoDictionary() - info["LSUIElement"] = "1" - NSApp.setActivationPolicy_(NSApplicationActivationPolicyProhibited) - - # For macOS: display the tray icon now with blocking function - tray_icon.run() - - elif platform.system() == "Windows": # Windows-specific - # Create a hidden window just to be able to receive window message events (for shutdown/logoff clean stop) - hinst = win32api.GetModuleHandle(None) - wndclass = win32gui.WNDCLASS() - wndclass.hInstance = hinst - wndclass.lpszClassName = "turingEventWndClass" - messageMap = {win32con.WM_QUERYENDSESSION: on_win32_wm_event, - win32con.WM_ENDSESSION: on_win32_wm_event, - win32con.WM_QUIT: on_win32_wm_event, - win32con.WM_DESTROY: on_win32_wm_event, - win32con.WM_CLOSE: on_win32_wm_event, - win32con.WM_POWERBROADCAST: on_win32_wm_event} - - wndclass.lpfnWndProc = messageMap + # For platforms != macOS, display the tray icon now with non-blocking function + if platform.system() != "Darwin": + tray_icon.run_detached() + logger.info("Tray icon has been displayed") + except: + tray_icon = None + logger.warning("Tray icon is not supported on your platform") + + # Set the different stopping event handlers, to send a complete frame to the LCD before exit + atexit.register(self.on_clean_exit) + signal.signal(signal.SIGINT, self.on_signal_caught) + signal.signal(signal.SIGTERM, self.on_signal_caught) + if os.name == 'posix': + signal.signal(signal.SIGQUIT, self.on_signal_caught) + if os.name == 'nt': + win32api.SetConsoleCtrlHandler(self.on_win32_ctrl_event, True) + + # Initialize the display + logger.info("Initialize display") + display.initialize_display() + + # Start serial queue handler + scheduler.QueueHandler() + + # Create all static images + display.display_static_images() + + # Create all static texts + display.display_static_text() + + # Wait for static images/text to be displayed before starting monitoring (to avoid filling the queue while waiting) + self.wait_for_empty_queue(10) + + # Start sensor scheduled reading. Avoid starting them all at the same time to optimize load + logger.info("Starting system monitoring") + import library.stats as stats + + scheduler.CPUPercentage() + time.sleep(0.25) + scheduler.CPUFrequency() + time.sleep(0.25) + scheduler.CPULoad() + time.sleep(0.25) + scheduler.CPUTemperature() + time.sleep(0.25) + scheduler.CPUFanSpeed() + time.sleep(0.25) + if stats.Gpu.is_available(): + scheduler.GpuStats() + time.sleep(0.25) + scheduler.MemoryStats() + time.sleep(0.25) + scheduler.DiskStats() + time.sleep(0.25) + scheduler.NetStats() + time.sleep(0.25) + scheduler.DateStats() + time.sleep(0.25) + scheduler.SystemUptimeStats() + time.sleep(0.25) + scheduler.CustomStats() + time.sleep(0.25) + scheduler.WeatherStats() + time.sleep(0.25) + scheduler.PingStats() + time.sleep(0.25) + + # OS-specific tasks + if tray_icon and platform.system() == "Darwin": # macOS-specific + from AppKit import NSBundle, NSApp, NSApplicationActivationPolicyProhibited + + # Hide Python Launcher icon from macOS dock + info = NSBundle.mainBundle().infoDictionary() + info["LSUIElement"] = "1" + NSApp.setActivationPolicy_(NSApplicationActivationPolicyProhibited) + + # For macOS: display the tray icon now with blocking function + tray_icon.run() + + elif os.name == "nt": # Windows-specific + # Create a hidden window just to be able to receive window message events (for shutdown/logoff clean stop) + hinst = win32api.GetModuleHandle(None) + wndclass = win32gui.WNDCLASS() + wndclass.hInstance = hinst + wndclass.lpszClassName = "turingEventWndClass" + messageMap = {win32con.WM_QUERYENDSESSION: self.on_win32_wm_event, + win32con.WM_ENDSESSION: self.on_win32_wm_event, + win32con.WM_QUIT: self.on_win32_wm_event, + win32con.WM_DESTROY: self.on_win32_wm_event, + win32con.WM_CLOSE: self.on_win32_wm_event, + win32con.WM_POWERBROADCAST: self.on_win32_wm_event} + + wndclass.lpfnWndProc = messageMap + + try: + myWindowClass = win32gui.RegisterClass(wndclass) + hwnd = win32gui.CreateWindowEx(win32con.WS_EX_LEFT, + myWindowClass, + "turingEventWnd", + 0, + 0, + 0, + win32con.CW_USEDEFAULT, + win32con.CW_USEDEFAULT, + 0, + 0, + hinst, + None) + while True: + # Receive and dispatch window messages + win32gui.PumpWaitingMessages() + time.sleep(0.5) + + except Exception as e: + logger.error("Exception while creating event window: %s" % str(e)) - try: - myWindowClass = win32gui.RegisterClass(wndclass) - hwnd = win32gui.CreateWindowEx(win32con.WS_EX_LEFT, - myWindowClass, - "turingEventWnd", - 0, - 0, - 0, - win32con.CW_USEDEFAULT, - win32con.CW_USEDEFAULT, - 0, - 0, - hinst, - None) - while True: - # Receive and dispatch window messages - win32gui.PumpWaitingMessages() - time.sleep(0.5) - - except Exception as e: - logger.error("Exception while creating event window: %s" % str(e)) + +if __name__ == "__main__": + app = Main() + app.main() From b066a31519182901032b2b0ca88151ba6182f209 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Tue, 2 Dec 2025 23:31:47 +0800 Subject: [PATCH 14/28] [config] THEME_DATA is a dict --- library/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/config.py b/library/config.py index 0415876b..034e615f 100644 --- a/library/config.py +++ b/library/config.py @@ -40,7 +40,7 @@ def load_yaml(configfile): FONTS_DIR = str(MAIN_DIRECTORY / "res" / "fonts") + "/" CONFIG_DATA = load_yaml(MAIN_DIRECTORY / "config.yaml") THEME_DEFAULT = load_yaml(MAIN_DIRECTORY / "res/themes/default.yaml") -THEME_DATA = None +THEME_DATA: dict = {} def copy_default(default, theme): From f631eebbb40e789d0e9cc15286eb01fb32b73b9f Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Thu, 4 Dec 2025 23:27:17 +0800 Subject: [PATCH 15/28] [lcd] clean code --- library/lcd/color.py | 7 ++++--- library/lcd/lcd_comm_rev_b.py | 4 ++-- library/lcd/lcd_comm_rev_c.py | 6 +++--- library/lcd/lcd_comm_rev_d.py | 2 +- library/lcd/lcd_comm_weact_a.py | 5 +++-- library/lcd/lcd_comm_weact_b.py | 2 +- library/lcd/lcd_simulated.py | 16 ++++++++-------- library/lcd/serialize.py | 8 ++++---- simple-program.py | 19 +++++++++---------- tools/turing-theme-extractor.py | 1 - 10 files changed, 35 insertions(+), 35 deletions(-) diff --git a/library/lcd/color.py b/library/lcd/color.py index 10ca7efe..baef766b 100644 --- a/library/lcd/color.py +++ b/library/lcd/color.py @@ -17,6 +17,7 @@ # - "hsl(0, 100%, 50%)" Color = Union[str, RGBColor] + def parse_color(color: Color) -> RGBColor: # even if undocumented, let's be nice and accept a list in lieu of a tuple if isinstance(color, tuple) or isinstance(color, list): @@ -42,7 +43,7 @@ def parse_color(color: Color) -> RGBColor: # fallback as a PIL color rgbcolor = ImageColor.getrgb(color) - if len(rgbcolor) == 4: + if len(rgbcolor) >= 4: return rgbcolor[0], rgbcolor[1], rgbcolor[2] - return rgbcolor - + else: + return rgbcolor diff --git a/library/lcd/lcd_comm_rev_b.py b/library/lcd/lcd_comm_rev_b.py index 15ca7c55..5abe9607 100644 --- a/library/lcd/lcd_comm_rev_b.py +++ b/library/lcd/lcd_comm_rev_b.py @@ -79,7 +79,7 @@ def auto_detect_com_port() -> Optional[str]: return None - def SendCommand(self, cmd: Command, payload=None, bypass_queue: bool = False): + def SendCommand(self, cmd: Command, payload: list = None, bypass_queue: bool = False): # New protocol (10 byte packets, framed with the command, 8 data bytes inside) if payload is None: payload = [0] * 8 @@ -253,4 +253,4 @@ def DisplayPILImage( if self.update_queue: self.update_queue.put((time.sleep, [0.05])) else: - time.sleep(0.05) \ No newline at end of file + time.sleep(0.05) diff --git a/library/lcd/lcd_comm_rev_c.py b/library/lcd/lcd_comm_rev_c.py index 8d93318d..53987023 100644 --- a/library/lcd/lcd_comm_rev_c.py +++ b/library/lcd/lcd_comm_rev_c.py @@ -233,9 +233,9 @@ def _hello(self): # Note: ID returned by display are not reliable for some models e.g. 2.1" displays return "chs_5inch" # Rely on width/height for sub-revision detection sub_revisions = { - (480, 480):SubRevision.REV_2INCH, - (480,800):SubRevision.REV_5INCH, - (480,1920):SubRevision.REV_8INCH + (480, 480): SubRevision.REV_2INCH, + (480, 800): SubRevision.REV_5INCH, + (480, 1920): SubRevision.REV_8INCH } self.sub_revision = sub_revisions.get((self.display_width, self.display_height), SubRevision.UNKNOWN) if self.sub_revision == SubRevision.UNKNOWN: diff --git a/library/lcd/lcd_comm_rev_d.py b/library/lcd/lcd_comm_rev_d.py index 850a7726..d99ffb7a 100644 --- a/library/lcd/lcd_comm_rev_d.py +++ b/library/lcd/lcd_comm_rev_d.py @@ -120,7 +120,7 @@ def SetOrientation(self, orientation: Orientation = Orientation.PORTRAIT): # Basic orientations (portrait / landscape) are software-managed because screen commands only support portrait self.orientation = orientation - if self.orientation == Orientation.REVERSE_LANDSCAPE or self.orientation == Orientation.REVERSE_PORTRAIT: + if self.orientation in [Orientation.REVERSE_LANDSCAPE, Orientation.REVERSE_PORTRAIT]: self.SendCommand(cmd=Command.SET180) else: self.SendCommand(cmd=Command.SETORG) diff --git a/library/lcd/lcd_comm_weact_a.py b/library/lcd/lcd_comm_weact_a.py index fa7c6f39..60b7826b 100644 --- a/library/lcd/lcd_comm_weact_a.py +++ b/library/lcd/lcd_comm_weact_a.py @@ -51,7 +51,7 @@ def auto_detect_com_port(): for com_port in com_ports: if com_port.vid == 0x1a86 and com_port.pid == 0xfe0c: return com_port.device - if type(com_port.serial_number) == str: + if isinstance(com_port.serial_number, str): if com_port.serial_number.startswith("AB"): return com_port.device @@ -178,7 +178,7 @@ def SetOrientation(self, orientation: Orientation = Orientation.PORTRAIT): byteBuffer[2] = Command.CMD_END self.SendCommand(byteBuffer) - def SetSensorReportTime(self, time_ms: int): + def SetSensorReportTime(self, time_ms: int) -> bool: if time_ms > 0xFFFF or (time_ms < 500 and time_ms != 0): return False byteBuffer = bytearray(4) @@ -187,6 +187,7 @@ def SetSensorReportTime(self, time_ms: int): byteBuffer[2] = time_ms >> 8 & 0xFF byteBuffer[3] = Command.CMD_END self.SendCommand(byteBuffer) + return True def Free(self): byteBuffer = bytearray(2) diff --git a/library/lcd/lcd_comm_weact_b.py b/library/lcd/lcd_comm_weact_b.py index 3a25fa95..60c96f73 100644 --- a/library/lcd/lcd_comm_weact_b.py +++ b/library/lcd/lcd_comm_weact_b.py @@ -48,7 +48,7 @@ def auto_detect_com_port(): for com_port in com_ports: if com_port.vid == 0x1a86 and com_port.pid == 0xfe0c: return com_port.device - if type(com_port.serial_number) == str: + if isinstance(com_port.serial_number, str): if com_port.serial_number.startswith("AD"): return com_port.device diff --git a/library/lcd/lcd_simulated.py b/library/lcd/lcd_simulated.py index d25abd46..89adbc34 100644 --- a/library/lcd/lcd_simulated.py +++ b/library/lcd/lcd_simulated.py @@ -45,14 +45,14 @@ def do_GET(self): self.wfile.write(bytes(f" myImageElement.src = '{SCREENSHOT_FILE}?rand=' + Math.random();", "utf-8")) self.wfile.write(bytes("}, 250);", "utf-8")) self.wfile.write(bytes("", "utf-8")) - elif self.path.startswith("/" + SCREENSHOT_FILE): - imgfile = open(SCREENSHOT_FILE, 'rb').read() - mimetype = mimetypes.MimeTypes().guess_type(SCREENSHOT_FILE)[0] - self.send_response(200) - if mimetype is not None: - self.send_header('Content-type', mimetype) - self.end_headers() - self.wfile.write(imgfile) + elif self.path.startswith(f"/{SCREENSHOT_FILE}"): + with open(SCREENSHOT_FILE, 'rb') as imgfile: + mimetype = mimetypes.MimeTypes().guess_type(SCREENSHOT_FILE)[0] + self.send_response(200) + if mimetype is not None: + self.send_header('Content-type', mimetype) + self.end_headers() + self.wfile.write(imgfile.read()) # Simulated display: write on a file instead of serial port diff --git a/library/lcd/serialize.py b/library/lcd/serialize.py index e0c5d662..a5433a07 100644 --- a/library/lcd/serialize.py +++ b/library/lcd/serialize.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: GPL-3.0-or-later -from typing import Iterator, Literal +from typing import Iterator, Literal, Tuple import numpy as np from PIL import Image @@ -40,7 +40,7 @@ def image_to_RGB565(image: Image.Image, endianness: Literal["big", "little"]) -> return rgb565.astype(typ).tobytes() -def image_to_BGR(image: Image.Image) -> (bytes, int): +def image_to_BGR(image: Image.Image) -> Tuple[bytes, int]: if image.mode not in ["RGB", "RGBA"]: # we need the first 3 channels to be R, G and B image = image.convert("RGB") @@ -50,7 +50,7 @@ def image_to_BGR(image: Image.Image) -> (bytes, int): return bgr.tobytes(), 3 -def image_to_BGRA(image: Image.Image) -> (bytes, int): +def image_to_BGRA(image: Image.Image) -> Tuple[bytes, int]: if image.mode != "RGBA": image = image.convert("RGBA") rgba = np.asarray(image) @@ -60,7 +60,7 @@ def image_to_BGRA(image: Image.Image) -> (bytes, int): # FIXME: to optimize like other functions above -def image_to_compressed_BGRA(image: Image.Image) -> (bytes, int): +def image_to_compressed_BGRA(image: Image.Image) -> Tuple[bytes, int]: compressed_bgra = bytearray() image_data = image.convert("RGBA").load() for h in range(image.height): diff --git a/simple-program.py b/simple-program.py index 287c92d1..54e2474d 100755 --- a/simple-program.py +++ b/simple-program.py @@ -22,6 +22,7 @@ # This file is a simple Python test program using the library code to display custom content on screen (see README) from library.pythoncheck import check_python_version + check_python_version() import os @@ -64,24 +65,19 @@ assert WIDTH <= HEIGHT, "Indicate display width/height for PORTRAIT orientation: width <= height" -stop = False - -if __name__ == "__main__": +def main(): + stop = False + # Set the signal handlers, to send a complete frame to the LCD before exit def sighandler(signum, frame): - global stop + nonlocal stop stop = True - - - # Set the signal handlers, to send a complete frame to the LCD before exit signal.signal(signal.SIGINT, sighandler) signal.signal(signal.SIGTERM, sighandler) - is_posix = os.name == 'posix' - if is_posix: + if os.name == 'posix': signal.signal(signal.SIGQUIT, sighandler) # Build your LcdComm object based on the HW revision - lcd_comm = None if REVISION == "A": logger.info("Selected Hardware Revision A (Turing Smart Screen 3.5\" & UsbPCMonitor 3.5\"/5\")") # NOTE: If you have UsbPCMonitor 5" you need to change the width/height to 480x800 below @@ -207,3 +203,6 @@ def sighandler(signum, frame): # Close serial connection at exit lcd_comm.closeSerial() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/turing-theme-extractor.py b/tools/turing-theme-extractor.py index 63e06d40..a8a53e07 100644 --- a/tools/turing-theme-extractor.py +++ b/tools/turing-theme-extractor.py @@ -35,7 +35,6 @@ def main(file_path): mm = mmap.mmap(theme_file.fileno(), 0) # Find PNG signature in binary data - start_pos = 0 header_found = mm.find(PNG_SIGNATURE, 0) while header_found != -1: From 7d863e631ec001f5b824eead2fbafc3dd9b97ea0 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Fri, 5 Dec 2025 00:04:13 +0800 Subject: [PATCH 16/28] [theme-preview-generator] clean code --- library/stats.py | 15 +++------------ tools/theme-preview-generator.py | 25 +++++++++---------------- 2 files changed, 12 insertions(+), 28 deletions(-) diff --git a/library/stats.py b/library/stats.py index 9af85060..bb461987 100644 --- a/library/stats.py +++ b/library/stats.py @@ -82,12 +82,7 @@ import library.sensors.sensors_custom as sensors_custom - -def get_theme_file_path(name): - if name: - return os.path.join(config.THEME_DATA['PATH'], name) - else: - return None +get_theme_file_path = lambda name: os.path.join(config.THEME_DATA['PATH'], name) if name else None def display_themed_value(theme_data, value, min_size=0, unit=''): @@ -157,7 +152,7 @@ def display_themed_progress_bar(theme_data, value): ) -def display_themed_radial_bar(theme_data, value, min_size=0, unit='', custom_text=None): +def display_themed_radial_bar(theme_data, value, min_size=0, unit='', custom_text:str=None): if not theme_data.get("SHOW", False): return @@ -340,11 +335,7 @@ def temperature(cls): @classmethod def fan_speed(cls): - if CPU_FAN != "AUTO": - fan_percent = sensors.Cpu.fan_percent(CPU_FAN) - else: - fan_percent = sensors.Cpu.fan_percent() - + fan_percent = sensors.Cpu.fan_percent(CPU_FAN if CPU_FAN != "AUTO" else None) save_last_value(fan_percent, cls.last_values_cpu_fan_speed, config.THEME_DATA['STATS']['CPU']['FAN_SPEED']['LINE_GRAPH'].get("HISTORY_SIZE", DEFAULT_HISTORY_SIZE)) diff --git a/tools/theme-preview-generator.py b/tools/theme-preview-generator.py index 374dea1b..4d5d775a 100644 --- a/tools/theme-preview-generator.py +++ b/tools/theme-preview-generator.py @@ -27,15 +27,15 @@ import yaml -def get_themes(display_size: str): +def get_themes(display_size: str) -> list: themes = [] directory = 'res/themes/' for filename in os.listdir('res/themes'): - dir = os.path.join(directory, filename) + folder = os.path.join(directory, filename) # checking if it is a directory - if os.path.isdir(dir): + if os.path.isdir(folder): # Check if a theme.yaml file exists - theme = os.path.join(dir, 'theme.yaml') + theme = os.path.join(folder, 'theme.yaml') if os.path.isfile(theme): # Get display size from theme.yaml with open(theme, "rt", encoding='utf8') as stream: @@ -45,25 +45,21 @@ def get_themes(display_size: str): return sorted(themes, key=str.casefold) -def write_theme_previews_to_file(themes, file, size): +def write_theme_previews_to_file(themes: list, file, size: str): file.write(f"\n## {size} themes\n") file.write("") i = 0 for theme in themes: file.write( f"") - i = i + 1 - if i >= 5: + i += 1 + if not i % 5: file.write("
{theme}
") i = 0 file.write("
\n") if __name__ == "__main__": - themes21inch = get_themes('2.1"') - themes3inch = get_themes('3.5"') - themes5inch = get_themes('5"') - themes88inch = get_themes('8.8"') with open("res/themes/themes.md", "w", encoding='utf-8') as file: file.write("\n") @@ -75,8 +71,5 @@ def write_theme_previews_to_file(themes, file, size): file.write("[3.5\" themes](#35-themes)\n\n") file.write("[5\" themes](#5-themes)\n\n") file.write("[8.8\" themes](#88-themes)\n") - - write_theme_previews_to_file(themes21inch, file, "2.1\"") - write_theme_previews_to_file(themes3inch, file, "3.5\"") - write_theme_previews_to_file(themes5inch, file, "5\"") - write_theme_previews_to_file(themes88inch, file, "8.8\"") + for i in ["2.1\"", "3.5\"", "5\"", "8.8\""]: + write_theme_previews_to_file(get_themes(i), file, i) From e2ab00b4762ec66055802ade9bf6e99a9f199c02 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Fri, 5 Dec 2025 00:08:35 +0800 Subject: [PATCH 17/28] [turing-theme-extrcator] clean code --- tools/turing-theme-extractor.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tools/turing-theme-extractor.py b/tools/turing-theme-extractor.py index a8a53e07..91cd26e0 100644 --- a/tools/turing-theme-extractor.py +++ b/tools/turing-theme-extractor.py @@ -22,17 +22,19 @@ # turing-theme-extractor.py: Extract resources from a Turing Smart Screen theme (.data files) made for Windows app # This program will search and extract PNGs from the theme data and extract theme in the current directory # The PNG can then be re-used to create a theme for System Monitor python program (see Wiki for theme creation) -import mmap +from mmap import mmap import os import sys PNG_SIGNATURE = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A' PNG_IEND = b'\x49\x45\x4E\x44\xAE\x42\x60\x82' + + def main(file_path): found_png = 0 with open(file_path, "r+b") as theme_file: - mm = mmap.mmap(theme_file.fileno(), 0) + mm = mmap(theme_file.fileno(), 0) # Find PNG signature in binary data header_found = mm.find(PNG_SIGNATURE, 0) @@ -50,13 +52,14 @@ def main(file_path): png_file.write(theme_file.read(iend_found - header_found + len(PNG_IEND))) print("PNG extracted to theme_res_%s.png" % str(header_found)) - found_png = found_png + 1 + found_png += 1 # Find next PNG signature (if any) header_found = mm.find(PNG_SIGNATURE, iend_found) print(f"\n{found_png} PNG files extracted from theme to current directory") + if __name__ == "__main__": if len(sys.argv) != 2: print("Usage :") @@ -69,4 +72,4 @@ def main(file_path): sys.exit(0) except: os._exit(0) - main(sys.argv[1]) \ No newline at end of file + main(sys.argv[1]) From a823911d62fcf5f72a476d968e90953f6d4d0c25 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Fri, 5 Dec 2025 00:24:15 +0800 Subject: [PATCH 18/28] [theme-editor] make sure RESIZE_FACTOR > 0 --- theme-editor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/theme-editor.py b/theme-editor.py index 0c51f1d1..d783ce59 100755 --- a/theme-editor.py +++ b/theme-editor.py @@ -363,7 +363,10 @@ def on_zoom_plus(self): self.RESIZE_FACTOR += 0.2 def on_zoom_minus(self): - self.RESIZE_FACTOR -= 0.2 + if self.RESIZE_FACTOR >= 0.2: + self.RESIZE_FACTOR -= 0.2 + else: + self.RESIZE_FACTOR = 0.2 if __name__ == "__main__": From 6f9938e46178827b0d3c75dd9f8099e4610e0e1c Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Fri, 5 Dec 2025 00:43:30 +0800 Subject: [PATCH 19/28] [theme-editor] allow change zoom level while running and refresh auto --- theme-editor.py | 71 ++++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/theme-editor.py b/theme-editor.py index d783ce59..77d9ede6 100755 --- a/theme-editor.py +++ b/theme-editor.py @@ -183,12 +183,12 @@ def __init__(self): # Allow to resize editor using mouse wheel or buttons self.bind_all("", self.on_mousewheel) - zoom_plus_btn = Button(self, text="Zoom +", command=lambda: self.on_zoom_plus()) - zoom_plus_btn.place(x=self.RGB_LED_MARGIN, y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, + self.zoom_plus_btn = Button(self, text="Zoom +", command=lambda: self.on_zoom_plus()) + self.zoom_plus_btn.place(x=self.RGB_LED_MARGIN, y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, width=int(self.display_width / 2)) - zoom_minus_btn = Button(self, text="Zoom -", command=lambda: self.on_zoom_minus()) - zoom_minus_btn.place(x=int(self.display_width / 2) + self.RGB_LED_MARGIN, + self.zoom_minus_btn = Button(self, text="Zoom -", command=lambda: self.on_zoom_minus()) + self.zoom_minus_btn.place(x=int(self.display_width / 2) + self.RGB_LED_MARGIN, y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, width=int(self.display_width / 2)) @@ -196,8 +196,8 @@ def __init__(self): self.label_coord.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 40, width=self.display_width + 2 * self.RGB_LED_MARGIN) - label_info = Label(self, text="This preview will reload when theme file is updated") - label_info.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 60, + self.label_info = Label(self, text="This preview will reload when theme file is updated") + self.label_info.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 60, width=self.display_width + 2 * self.RGB_LED_MARGIN) self.label_zone = tkinter.Label(self, bg='#%02x%02x%02x' % tuple(map(lambda x: 255 - x, led_color))) @@ -236,8 +236,25 @@ def init_env(self): self.error_in_theme = True self.inited = True - def refresh(self): - if os.path.exists(self.theme_file) and os.path.getmtime(self.theme_file) > self.last_edit_time: + def refresh_window(self): + self.display_width, self.display_height = int(display.lcd.get_width() / self.RESIZE_FACTOR), int( + display.lcd.get_height() / self.RESIZE_FACTOR) + self.geometry( + str(self.display_width + 2 * self.RGB_LED_MARGIN) + "x" + str( + self.display_height + 2 * self.RGB_LED_MARGIN + 80)) + self.zoom_minus_btn.place(x=self.RGB_LED_MARGIN + int(self.display_width / 2), y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, + width=int(self.display_width / 2)) + self.zoom_plus_btn.place(x=self.RGB_LED_MARGIN, y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, + width=int(self.display_width / 2)) + self.label_info.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 60, + width=self.display_width + 2 * self.RGB_LED_MARGIN) + self.label_coord.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 40, + width=self.display_width + 2 * self.RGB_LED_MARGIN) + + + def refresh(self, force_fresh: bool = False): + if os.path.exists(self.theme_file) and os.path.getmtime( + self.theme_file) > self.last_edit_time or force_fresh: logger.debug("The theme file has been updated, the preview window will refresh") try: refresh_theme() @@ -370,25 +387,23 @@ def on_zoom_minus(self): if __name__ == "__main__": + # Create preview window + logger.debug("Opening theme preview window with static data") + viewer = Viewer() + current_resize_factor = viewer.RESIZE_FACTOR + logger.debug( + "You can now edit the theme file in the editor. When you save your changes, the preview window will " + "update automatically") + while True: - # Create preview window - logger.debug("Opening theme preview window with static data") - viewer = Viewer() + if current_resize_factor != viewer.RESIZE_FACTOR: + logger.info( + f"Zoom level changed from {current_resize_factor:.1f} to {viewer.RESIZE_FACTOR:.1f}, reloading theme editor") + viewer.refresh(True) + viewer.refresh_window() + current_resize_factor = viewer.RESIZE_FACTOR + # Every time the theme file is modified: reload preview + viewer.refresh() + # Regularly update the viewer window even if content unchanged, or it will appear as "not responding" viewer.update() - - current_resize_factor = viewer.RESIZE_FACTOR - logger.debug( - "You can now edit the theme file in the editor. When you save your changes, the preview window will " - "update automatically") - - while current_resize_factor == viewer.RESIZE_FACTOR: - # Every time the theme file is modified: reload preview - viewer.refresh() - # Regularly update the viewer window even if content unchanged, or it will appear as "not responding" - viewer.update() - time.sleep(0.1) - - # Zoom level changed, reload editor - logger.info( - f"Zoom level changed from {current_resize_factor:.1f} to {viewer.RESIZE_FACTOR:.1f}, reloading theme editor") - viewer.destroy() + time.sleep(0.1) From 66aaa8f499183b33486c52f222a7235fca83169b Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Fri, 5 Dec 2025 01:22:37 +0800 Subject: [PATCH 20/28] [theme-editor] change zoom+- buttons to scale --- theme-editor.py | 52 +++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/theme-editor.py b/theme-editor.py index 77d9ede6..543c544e 100755 --- a/theme-editor.py +++ b/theme-editor.py @@ -37,8 +37,8 @@ try: import tkinter from PIL import ImageTk, Image - from tkinter import Tk - from tkinter.ttk import Button, Label + from tkinter import Tk, DoubleVar + from tkinter.ttk import Button, Label, Scale except: print( "[ERROR] Tkinter dependency not installed. Please follow troubleshooting page: https://github.com/mathoudebine/turing-smart-screen-python/wiki/Troubleshooting#all-os-tkinter-dependency-not-installed") @@ -182,15 +182,18 @@ def __init__(self): # Allow to resize editor using mouse wheel or buttons self.bind_all("", self.on_mousewheel) - - self.zoom_plus_btn = Button(self, text="Zoom +", command=lambda: self.on_zoom_plus()) - self.zoom_plus_btn.place(x=self.RGB_LED_MARGIN, y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, - width=int(self.display_width / 2)) - - self.zoom_minus_btn = Button(self, text="Zoom -", command=lambda: self.on_zoom_minus()) - self.zoom_minus_btn.place(x=int(self.display_width / 2) + self.RGB_LED_MARGIN, - y=self.display_height + 2 * self.RGB_LED_MARGIN, - height=30, width=int(self.display_width / 2)) + self.zoom_level = DoubleVar(value=self.RESIZE_FACTOR) + self.zoom_scale = Scale(self, from_=0.6, to=1.6, variable=self.zoom_level, orient="horizontal", + command=self.on_zoom_level_change) + self.zoom_scale.place(x=self.RGB_LED_MARGIN, y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, + width=int(self.display_width / 2)) + self.zoom_label = Label(self, text=f"Zoom Level:{self.RESIZE_FACTOR}") + self.zoom_label.place(x=self.RGB_LED_MARGIN, y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, + width=int(self.display_width / 2)) + + self.zoom_scale.place(x=int(self.display_width / 2) + self.RGB_LED_MARGIN, + y=self.display_height + 2 * self.RGB_LED_MARGIN, + height=30, width=int(self.display_width / 2)) self.label_coord = Label(self, text="Click or draw a zone to show coordinates") self.label_coord.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 40, @@ -198,7 +201,7 @@ def __init__(self): self.label_info = Label(self, text="This preview will reload when theme file is updated") self.label_info.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 60, - width=self.display_width + 2 * self.RGB_LED_MARGIN) + width=self.display_width + 2 * self.RGB_LED_MARGIN) self.label_zone = tkinter.Label(self, bg='#%02x%02x%02x' % tuple(map(lambda x: 255 - x, led_color))) self.label_zone.bind("", self.on_zone_click) @@ -242,16 +245,16 @@ def refresh_window(self): self.geometry( str(self.display_width + 2 * self.RGB_LED_MARGIN) + "x" + str( self.display_height + 2 * self.RGB_LED_MARGIN + 80)) - self.zoom_minus_btn.place(x=self.RGB_LED_MARGIN + int(self.display_width / 2), y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, - width=int(self.display_width / 2)) - self.zoom_plus_btn.place(x=self.RGB_LED_MARGIN, y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, + self.zoom_scale.place(x=self.RGB_LED_MARGIN + int(self.display_width / 2), + y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, + width=int(self.display_width / 2)) + self.zoom_label.place(x=self.RGB_LED_MARGIN, y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, width=int(self.display_width / 2)) self.label_info.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 60, width=self.display_width + 2 * self.RGB_LED_MARGIN) self.label_coord.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 40, width=self.display_width + 2 * self.RGB_LED_MARGIN) - def refresh(self, force_fresh: bool = False): if os.path.exists(self.theme_file) and os.path.getmtime( self.theme_file) > self.last_edit_time or force_fresh: @@ -370,20 +373,19 @@ def on_closing(self): except: os._exit(0) + def on_zoom_level_change(self, value): + level = self.zoom_level.get() + if not level % 0.2: + self.zoom_level.set(level - (level % 0.2)) + self.zoom_label.config(text=f"Zoom Level:{self.zoom_level.get():.1f}") + self.RESIZE_FACTOR = round(self.zoom_level.get(), 1) + def on_mousewheel(self, event): if event.delta > 0: self.RESIZE_FACTOR += 0.2 else: self.RESIZE_FACTOR -= 0.2 - - def on_zoom_plus(self): - self.RESIZE_FACTOR += 0.2 - - def on_zoom_minus(self): - if self.RESIZE_FACTOR >= 0.2: - self.RESIZE_FACTOR -= 0.2 - else: - self.RESIZE_FACTOR = 0.2 + self.zoom_scale.set(self.RESIZE_FACTOR) if __name__ == "__main__": From 7b46ce8d0c7ef5739c2ddff66ff28230a39815a4 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Fri, 5 Dec 2025 12:27:03 +0800 Subject: [PATCH 21/28] [theme-editor] change zoom range --- theme-editor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/theme-editor.py b/theme-editor.py index 543c544e..9d060d1b 100755 --- a/theme-editor.py +++ b/theme-editor.py @@ -183,7 +183,7 @@ def __init__(self): # Allow to resize editor using mouse wheel or buttons self.bind_all("", self.on_mousewheel) self.zoom_level = DoubleVar(value=self.RESIZE_FACTOR) - self.zoom_scale = Scale(self, from_=0.6, to=1.6, variable=self.zoom_level, orient="horizontal", + self.zoom_scale = Scale(self, from_=0.2, to=2, variable=self.zoom_level, orient="horizontal", command=self.on_zoom_level_change) self.zoom_scale.place(x=self.RGB_LED_MARGIN, y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, width=int(self.display_width / 2)) From d5fc2d7122448e798193197ca9de0d47f09de8f9 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Fri, 5 Dec 2025 18:19:02 +0800 Subject: [PATCH 22/28] [config] rewrite it to a class --- library/config.py | 104 ++++++++++++++++++++----------------------- library/display.py | 2 +- library/scheduler.py | 2 +- library/stats.py | 2 +- theme-editor.py | 2 +- 5 files changed, 53 insertions(+), 59 deletions(-) diff --git a/library/config.py b/library/config.py index 034e615f..c57cf7ee 100644 --- a/library/config.py +++ b/library/config.py @@ -28,60 +28,54 @@ from library.log import logger - -def load_yaml(configfile): - with open(configfile, "rt", encoding='utf8') as stream: - yamlconfig = yaml.safe_load(stream) - return yamlconfig - - -PATH = sys.path[0] -MAIN_DIRECTORY = Path(__file__).parent.parent.resolve() -FONTS_DIR = str(MAIN_DIRECTORY / "res" / "fonts") + "/" -CONFIG_DATA = load_yaml(MAIN_DIRECTORY / "config.yaml") -THEME_DEFAULT = load_yaml(MAIN_DIRECTORY / "res/themes/default.yaml") -THEME_DATA: dict = {} - - -def copy_default(default, theme): - """recursively supply default values into a dict of dicts of dicts ....""" - for k, v in default.items(): - if k not in theme: - theme[k] = v - if isinstance(v, dict): - copy_default(default[k], theme[k]) - - -def load_theme(): - global THEME_DATA - try: - theme_path = Path("res/themes/" + CONFIG_DATA['config']['THEME']) - logger.info("Loading theme %s from %s" % (CONFIG_DATA['config']['THEME'], theme_path / "theme.yaml")) - THEME_DATA = load_yaml(MAIN_DIRECTORY / theme_path / "theme.yaml") - THEME_DATA['PATH'] = str(MAIN_DIRECTORY / theme_path) + "/" - except: - logger.error("Theme not found or contains errors!") +class Config: + def __init__(self): + self.MAIN_DIRECTORY = Path(__file__).parent.parent.resolve() + self.FONTS_DIR = str(self.MAIN_DIRECTORY / "res" / "fonts") + "/" + self.CONFIG_DATA = self.load_yaml(self.MAIN_DIRECTORY / "config.yaml") + self.THEME_DEFAULT = self.load_yaml(self.MAIN_DIRECTORY / "res/themes/default.yaml") + self.THEME_DATA: dict = {} + # Load theme on import + self.load_theme() + # Queue containing the serial requests to send to the screen + self.update_queue = queue.Queue() + + def load_yaml(self, configfile: str | Path): + with open(configfile, "rt", encoding='utf8') as stream: + yamlconfig = yaml.safe_load(stream) + return yamlconfig + + def copy_default(self, default: dict, theme: dict): + """recursively supply default values into a dict of dicts of dicts ....""" + for k, v in default.items(): + if k not in theme: + theme[k] = v + if isinstance(v, dict): + self.copy_default(default[k], theme[k]) + + def load_theme(self): try: - sys.exit(0) + theme_path = Path("res/themes/" + self.CONFIG_DATA['config']['THEME']) + logger.info("Loading theme %s from %s" % (self.CONFIG_DATA['config']['THEME'], theme_path / "theme.yaml")) + self.THEME_DATA = self.load_yaml(self.MAIN_DIRECTORY / theme_path / "theme.yaml") + self.THEME_DATA['PATH'] = str(self.MAIN_DIRECTORY / theme_path) + "/" except: - os._exit(0) - - copy_default(THEME_DEFAULT, THEME_DATA) - - -def check_theme_compatible(display_size: str): - # Check if theme is compatible with hardware revision - if display_size != THEME_DATA['display'].get("DISPLAY_SIZE", '3.5"'): - logger.error("The selected theme " + CONFIG_DATA['config'][ - 'THEME'] + " is not compatible with your display revision " + CONFIG_DATA["display"]["REVISION"]) - try: - sys.exit(0) - except: - os._exit(0) - - -# Load theme on import -load_theme() - -# Queue containing the serial requests to send to the screen -update_queue = queue.Queue() + logger.error("Theme not found or contains errors!") + try: + sys.exit(0) + except: + os._exit(0) + + self.copy_default(self.THEME_DEFAULT, self.THEME_DATA) + + def check_theme_compatible(self, display_size: str): + # Check if theme is compatible with hardware revision + if display_size != self.THEME_DATA['display'].get("DISPLAY_SIZE", '3.5"'): + logger.error("The selected theme " + self.CONFIG_DATA['config'][ + 'THEME'] + " is not compatible with your display revision " + self.CONFIG_DATA["display"]["REVISION"]) + try: + sys.exit(0) + except: + os._exit(0) + +config = Config() \ No newline at end of file diff --git a/library/display.py b/library/display.py index 06cfe89d..dfd27f05 100644 --- a/library/display.py +++ b/library/display.py @@ -18,7 +18,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from library import config +from library.config import config from library.lcd.lcd_comm import Orientation from library.lcd.lcd_comm_rev_a import LcdCommRevA from library.lcd.lcd_comm_rev_b import LcdCommRevB diff --git a/library/scheduler.py b/library/scheduler.py index d6a1edd5..ca7e9d16 100644 --- a/library/scheduler.py +++ b/library/scheduler.py @@ -26,7 +26,7 @@ from datetime import timedelta from functools import wraps -import library.config as config +from library.config import config import library.stats as stats STOPPING = False diff --git a/library/stats.py b/library/stats.py index bb461987..168c6635 100644 --- a/library/stats.py +++ b/library/stats.py @@ -36,7 +36,7 @@ from psutil._common import bytes2human from uptime import uptime -import library.config as config +from library.config import config from library.display import display from library.log import logger diff --git a/theme-editor.py b/theme-editor.py index 9d060d1b..c7601336 100755 --- a/theme-editor.py +++ b/theme-editor.py @@ -68,7 +68,7 @@ logger.setLevel(logging.DEBUG) # Hardcode specific configuration for theme editor -from library import config +from library.config import config config.CONFIG_DATA["config"]["HW_SENSORS"] = "STATIC" # For theme editor always use stub data config.CONFIG_DATA["config"]["THEME"] = sys.argv[1] # Theme is given as argument From f13597c3133db3413229bc16fed5cc4df05905d2 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Fri, 5 Dec 2025 18:28:17 +0800 Subject: [PATCH 23/28] [config] catch exception to log --- library/config.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/library/config.py b/library/config.py index c57cf7ee..8985c8b9 100644 --- a/library/config.py +++ b/library/config.py @@ -6,6 +6,7 @@ # Copyright (C) 2021 Matthieu Houdebine (mathoudebine) # Copyright (C) 2022 Rollbacke # Copyright (C) 2022 Ebag333 +# Copyright (C) 2025 ColdWindScholar # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -21,13 +22,13 @@ # along with this program. If not, see . import os -import queue +from queue import Queue import sys from pathlib import Path -import yaml - +from yaml import safe_load from library.log import logger + class Config: def __init__(self): self.MAIN_DIRECTORY = Path(__file__).parent.parent.resolve() @@ -38,12 +39,12 @@ def __init__(self): # Load theme on import self.load_theme() # Queue containing the serial requests to send to the screen - self.update_queue = queue.Queue() + self.update_queue = Queue() - def load_yaml(self, configfile: str | Path): + @staticmethod + def load_yaml(configfile: str | Path): with open(configfile, "rt", encoding='utf8') as stream: - yamlconfig = yaml.safe_load(stream) - return yamlconfig + return safe_load(stream) def copy_default(self, default: dict, theme: dict): """recursively supply default values into a dict of dicts of dicts ....""" @@ -55,12 +56,13 @@ def copy_default(self, default: dict, theme: dict): def load_theme(self): try: - theme_path = Path("res/themes/" + self.CONFIG_DATA['config']['THEME']) - logger.info("Loading theme %s from %s" % (self.CONFIG_DATA['config']['THEME'], theme_path / "theme.yaml")) + theme_path = Path(f"res/themes/{self.CONFIG_DATA['config']['THEME']}") + logger.info(f"Loading theme {self.CONFIG_DATA['config']['THEME']} from {theme_path / 'theme.yaml'}") self.THEME_DATA = self.load_yaml(self.MAIN_DIRECTORY / theme_path / "theme.yaml") self.THEME_DATA['PATH'] = str(self.MAIN_DIRECTORY / theme_path) + "/" except: logger.error("Theme not found or contains errors!") + logger.exception('load_theme') try: sys.exit(0) except: @@ -71,11 +73,12 @@ def load_theme(self): def check_theme_compatible(self, display_size: str): # Check if theme is compatible with hardware revision if display_size != self.THEME_DATA['display'].get("DISPLAY_SIZE", '3.5"'): - logger.error("The selected theme " + self.CONFIG_DATA['config'][ - 'THEME'] + " is not compatible with your display revision " + self.CONFIG_DATA["display"]["REVISION"]) + logger.error( + f"The selected theme {self.CONFIG_DATA['config']['THEME']} is not compatible with your display revision {self.CONFIG_DATA["display"]["REVISION"]}") try: sys.exit(0) except: os._exit(0) -config = Config() \ No newline at end of file + +config = Config() From ee397fe8cc8c928f94209354d70255d0180254d2 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Fri, 5 Dec 2025 18:40:08 +0800 Subject: [PATCH 24/28] [scheduler] Remove duplicate function defines --- library/scheduler.py | 4 ---- main.py | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/library/scheduler.py b/library/scheduler.py index ca7e9d16..25116228 100644 --- a/library/scheduler.py +++ b/library/scheduler.py @@ -199,7 +199,3 @@ def QueueHandler(): f, args = config.update_queue.get() if f: f(*args) - - -def is_queue_empty() -> bool: - return config.update_queue.empty() diff --git a/main.py b/main.py index eae5307c..abc48edd 100755 --- a/main.py +++ b/main.py @@ -51,6 +51,7 @@ from library.log import logger import library.scheduler as scheduler + from library.config import config from library.display import display except Exception as e: @@ -84,7 +85,7 @@ def wait_for_empty_queue(self, timeout: int = 5): logger.info("Waiting for all pending request to be sent to display (%ds max)..." % timeout) wait_time = 0 - while not scheduler.is_queue_empty() and wait_time < timeout: + while not config.update_queue.empty() and wait_time < timeout: time.sleep(0.1) wait_time += 0.1 From 63a013c8ca1187f65a91f57444f22abcaceed232 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Fri, 5 Dec 2025 23:26:20 +0800 Subject: [PATCH 25/28] [theme-editor] allow to be imported in other modules --- library/display.py | 19 ++++++------ tests/library/lcd/serial_mock.py | 9 ++---- theme-editor.py | 52 ++++++++++++++------------------ 3 files changed, 35 insertions(+), 45 deletions(-) diff --git a/library/display.py b/library/display.py index dfd27f05..b5054639 100644 --- a/library/display.py +++ b/library/display.py @@ -33,8 +33,7 @@ def _get_full_path(path, name): if name: return path + name - else: - return None + return None def _get_theme_orientation() -> Orientation: @@ -56,15 +55,15 @@ def _get_theme_orientation() -> Orientation: def _get_theme_size() -> tuple[int, int]: sizes = { - '0.96"':(80, 160), - '2.1"':(480, 480), - '3.5"':(320, 480), - '5"':(480, 800), - '8.8"':(480, 1920), + '0.96"': (80, 160), + '2.1"': (480, 480), + '3.5"': (320, 480), + '5"': (480, 800), + '8.8"': (480, 1920), } if config.THEME_DATA["display"].get("DISPLAY_SIZE", '') not in sizes.keys(): logger.warning( - f'Cannot find valid DISPLAY_SIZE property in selected theme {config.CONFIG_DATA["config"]["THEME"]}, defaulting to 3.5"') + f'Cannot find valid DISPLAY_SIZE property in selected theme {config.CONFIG_DATA["config"]["THEME"]}, defaulting to 3.5"') return sizes.get(config.THEME_DATA["display"].get("DISPLAY_SIZE", ''), (320, 480)) @@ -87,10 +86,10 @@ def __init__(self): update_queue=config.update_queue) elif config.CONFIG_DATA["display"]["REVISION"] == "WEACT_A": self.lcd = LcdCommWeActA(com_port=config.CONFIG_DATA['config']['COM_PORT'], - update_queue=config.update_queue) + update_queue=config.update_queue) elif config.CONFIG_DATA["display"]["REVISION"] == "WEACT_B": self.lcd = LcdCommWeActB(com_port=config.CONFIG_DATA['config']['COM_PORT'], - update_queue=config.update_queue) + update_queue=config.update_queue) elif config.CONFIG_DATA["display"]["REVISION"] == "SIMU": # Simulated display: always set width/height from theme self.lcd = LcdSimulated(display_width=width, display_height=height) diff --git a/tests/library/lcd/serial_mock.py b/tests/library/lcd/serial_mock.py index 95e57e58..0c14591e 100644 --- a/tests/library/lcd/serial_mock.py +++ b/tests/library/lcd/serial_mock.py @@ -12,6 +12,7 @@ # mostly representative of the serialization time. BENCHMARK = bool(os.getenv("BENCHMARK")) + class MockSerial(Mock): def expect_golden(self, tc: unittest.TestCase, fn: str): golden_dir = os.path.join(os.path.dirname(__file__), "golden") @@ -27,11 +28,9 @@ def expect_golden(self, tc: unittest.TestCase, fn: str): assert len(args) == 1 f.write(f"read {args[0]}\n") # don't record the other methods - else: with open(full_path, "r", encoding="ascii") as f: expected = list(filter(lambda l: l.strip() != "", f.readlines())) - mock_calls = [] for method, args, _ in self.mock_calls: if method not in ["write", "read"]: @@ -48,6 +47,7 @@ def expect_golden(self, tc: unittest.TestCase, fn: str): elif call_name == "read": tc.assertEqual(call_args, (int(exp_arg),)) + class NoopSerial: def write(self, data): pass @@ -66,7 +66,4 @@ def expect_golden(self, tc: unittest.TestCase, fn: str): def new_testing_serial(): - if BENCHMARK: - return NoopSerial() - else: - return MockSerial() + return NoopSerial() if BENCHMARK else MockSerial() diff --git a/theme-editor.py b/theme-editor.py index c7601336..8278e5b8 100755 --- a/theme-editor.py +++ b/theme-editor.py @@ -21,11 +21,9 @@ # theme-editor.py: Allow to easily edit themes for System Monitor (main.py) in a preview window on the computer # The preview window is refreshed as soon as the theme file is modified - from library.pythoncheck import check_python_version check_python_version() - import locale import logging import os @@ -46,44 +44,23 @@ sys.exit(0) except: os._exit(0) - -if len(sys.argv) != 2: - print("Usage :") - print(" theme-editor.py theme-name") - print("Examples : ") - print(" theme-editor.py 3.5inchTheme2") - print(" theme-editor.py Landscape6Grid") - print(" theme-editor.py Cyberpunk") - try: - sys.exit(0) - except: - os._exit(0) - import library.log library.log.logger.setLevel(logging.NOTSET) # Disable system monitor logging for the editor - # Create a logger for the editor logger = logging.getLogger('turing-editor') logger.setLevel(logging.DEBUG) - # Hardcode specific configuration for theme editor from library.config import config config.CONFIG_DATA["config"]["HW_SENSORS"] = "STATIC" # For theme editor always use stub data -config.CONFIG_DATA["config"]["THEME"] = sys.argv[1] # Theme is given as argument - config.load_theme() - # For theme editor, always use simulated LCD config.CONFIG_DATA["display"]["REVISION"] = "SIMU" - from library.display import display # Only import display after hardcoded config is set # Resize editor if display is too big (e.g. 8.8" displays are 1920x480), can be changed later by zoom buttons - - def refresh_theme(): config.load_theme() @@ -129,8 +106,10 @@ def refresh_theme(): class Viewer(Tk): - def __init__(self): + def __init__(self, theme: str = None): super().__init__() + if theme: + config.CONFIG_DATA["config"]["THEME"] = theme # Theme is given as argument self.last_edit_time = 0 self.theme_file = None self.error_in_theme = False @@ -246,10 +225,10 @@ def refresh_window(self): str(self.display_width + 2 * self.RGB_LED_MARGIN) + "x" + str( self.display_height + 2 * self.RGB_LED_MARGIN + 80)) self.zoom_scale.place(x=self.RGB_LED_MARGIN + int(self.display_width / 2), - y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, - width=int(self.display_width / 2)) + y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, + width=int(self.display_width / 2)) self.zoom_label.place(x=self.RGB_LED_MARGIN, y=self.display_height + 2 * self.RGB_LED_MARGIN, height=30, - width=int(self.display_width / 2)) + width=int(self.display_width / 2)) self.label_info.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 60, width=self.display_width + 2 * self.RGB_LED_MARGIN) self.label_coord.place(x=0, y=self.display_height + 2 * self.RGB_LED_MARGIN + 40, @@ -388,10 +367,10 @@ def on_mousewheel(self, event): self.zoom_scale.set(self.RESIZE_FACTOR) -if __name__ == "__main__": +def main(theme: str = None): # Create preview window logger.debug("Opening theme preview window with static data") - viewer = Viewer() + viewer = Viewer(theme) current_resize_factor = viewer.RESIZE_FACTOR logger.debug( "You can now edit the theme file in the editor. When you save your changes, the preview window will " @@ -409,3 +388,18 @@ def on_mousewheel(self, event): # Regularly update the viewer window even if content unchanged, or it will appear as "not responding" viewer.update() time.sleep(0.1) + + +if __name__ == '__main__': + if len(sys.argv) != 2: + print("Usage :") + print(" theme-editor.py theme-name") + print("Examples : ") + print(" theme-editor.py 3.5inchTheme2") + print(" theme-editor.py Landscape6Grid") + print(" theme-editor.py Cyberpunk") + try: + sys.exit(1) + except: + os._exit(1) + main(sys.argv[1]) From f04e897943239b445d13f922eee7940403612cb5 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Fri, 5 Dec 2025 23:36:45 +0800 Subject: [PATCH 26/28] [configure] import theme_editor.py insteadof popen --- configure.py | 5 ++--- theme-editor.py => theme_editor.py | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) rename theme-editor.py => theme_editor.py (96%) mode change 100755 => 100644 diff --git a/configure.py b/configure.py index 1a33d8ce..c4f95b5f 100755 --- a/configure.py +++ b/configure.py @@ -472,9 +472,8 @@ def on_open_theme_folder_click(self): subprocess.Popen(["xdg-open", path]) def on_theme_editor_click(self): - subprocess.Popen( - f'"{MAIN_DIRECTORY}{glob.glob("theme-editor.*", root_dir=MAIN_DIRECTORY)[0]}" "{self.theme_cb.get()}"', - shell=True) + from theme_editor import main + main(self.theme_cb.get()) def on_change_theme(self): if self.app_theme.get() == 'light': diff --git a/theme-editor.py b/theme_editor.py old mode 100755 new mode 100644 similarity index 96% rename from theme-editor.py rename to theme_editor.py index 8278e5b8..1f9d6409 --- a/theme-editor.py +++ b/theme_editor.py @@ -19,7 +19,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# theme-editor.py: Allow to easily edit themes for System Monitor (main.py) in a preview window on the computer +# theme_editor.py: Allow to easily edit themes for System Monitor (main.py) in a preview window on the computer # The preview window is refreshed as soon as the theme file is modified from library.pythoncheck import check_python_version @@ -35,7 +35,7 @@ try: import tkinter from PIL import ImageTk, Image - from tkinter import Tk, DoubleVar + from tkinter import Tk, DoubleVar, Toplevel from tkinter.ttk import Button, Label, Scale except: print( @@ -105,7 +105,7 @@ def refresh_theme(): stats.Ping.stats() -class Viewer(Tk): +class Viewer(Tk if __name__ == '__main__' else Toplevel): def __init__(self, theme: str = None): super().__init__() if theme: @@ -129,8 +129,9 @@ def __init__(self, theme: str = None): self.geometry( str(self.display_width + 2 * self.RGB_LED_MARGIN) + "x" + str( self.display_height + 2 * self.RGB_LED_MARGIN + 80)) - self.protocol("WM_DELETE_WINDOW", self.on_closing) - self.call('wm', 'attributes', '.', '-topmost', '1') # Preview window always on top + if hasattr(self, 'call'): + self.protocol("WM_DELETE_WINDOW", self.on_closing) + self.call('wm', 'attributes', '.', '-topmost', '1') # Preview window always on top self.config(cursor="cross") led_color = config.THEME_DATA['display'].get("DISPLAY_RGB_LED", (255, 255, 255)) if isinstance(led_color, str): @@ -393,11 +394,11 @@ def main(theme: str = None): if __name__ == '__main__': if len(sys.argv) != 2: print("Usage :") - print(" theme-editor.py theme-name") + print(" theme_editor.py theme-name") print("Examples : ") - print(" theme-editor.py 3.5inchTheme2") - print(" theme-editor.py Landscape6Grid") - print(" theme-editor.py Cyberpunk") + print(" theme_editor.py 3.5inchTheme2") + print(" theme_editor.py Landscape6Grid") + print(" theme_editor.py Cyberpunk") try: sys.exit(1) except: From 40d5c3195ca15ee5c3b91108381b626c9c09d7c8 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Sat, 6 Dec 2025 08:34:42 +0800 Subject: [PATCH 27/28] [configure] rewrite loop processing to a def --- theme_editor.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/theme_editor.py b/theme_editor.py index 1f9d6409..0e21ad08 100644 --- a/theme_editor.py +++ b/theme_editor.py @@ -126,11 +126,12 @@ def __init__(self, theme: str = None): self.iconphoto(True, tkinter.PhotoImage(file=config.MAIN_DIRECTORY / "res/icons/monitor-icon-17865/64.png")) self.display_width, self.display_height = int(display.lcd.get_width() / self.RESIZE_FACTOR), int( display.lcd.get_height() / self.RESIZE_FACTOR) + self.current_resize_factor = self.RESIZE_FACTOR self.geometry( str(self.display_width + 2 * self.RGB_LED_MARGIN) + "x" + str( self.display_height + 2 * self.RGB_LED_MARGIN + 80)) + self.protocol("WM_DELETE_WINDOW", self.on_closing) if hasattr(self, 'call'): - self.protocol("WM_DELETE_WINDOW", self.on_closing) self.call('wm', 'attributes', '.', '-topmost', '1') # Preview window always on top self.config(cursor="cross") led_color = config.THEME_DATA['display'].get("DISPLAY_RGB_LED", (255, 255, 255)) @@ -367,29 +368,28 @@ def on_mousewheel(self, event): self.RESIZE_FACTOR -= 0.2 self.zoom_scale.set(self.RESIZE_FACTOR) + def loop(self): + if self.current_resize_factor != self.RESIZE_FACTOR: + logger.info( + f"Zoom level changed from {self.current_resize_factor:.1f} to {self.RESIZE_FACTOR:.1f}, reloading theme editor") + self.refresh(True) + self.refresh_window() + self.current_resize_factor = self.RESIZE_FACTOR + # Every time the theme file is modified: reload preview + self.refresh() + # Regularly update the viewer window even if content unchanged, or it will appear as "not responding" + self.update() + def main(theme: str = None): # Create preview window logger.debug("Opening theme preview window with static data") viewer = Viewer(theme) - current_resize_factor = viewer.RESIZE_FACTOR logger.debug( "You can now edit the theme file in the editor. When you save your changes, the preview window will " "update automatically") - while True: - if current_resize_factor != viewer.RESIZE_FACTOR: - logger.info( - f"Zoom level changed from {current_resize_factor:.1f} to {viewer.RESIZE_FACTOR:.1f}, reloading theme editor") - viewer.refresh(True) - viewer.refresh_window() - current_resize_factor = viewer.RESIZE_FACTOR - # Every time the theme file is modified: reload preview - viewer.refresh() - # Regularly update the viewer window even if content unchanged, or it will appear as "not responding" - viewer.update() - time.sleep(0.1) - + viewer.loop() if __name__ == '__main__': if len(sys.argv) != 2: From 31040200eb4e99d7c5a96a3c4d03f9ad6732b2c8 Mon Sep 17 00:00:00 2001 From: ColdWindScholar <3590361911@qq.com> Date: Sat, 6 Dec 2025 08:35:49 +0800 Subject: [PATCH 28/28] [configure] 2rewrite loop processing to a def --- theme_editor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/theme_editor.py b/theme_editor.py index 0e21ad08..c845bc58 100644 --- a/theme_editor.py +++ b/theme_editor.py @@ -18,6 +18,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import time # theme_editor.py: Allow to easily edit themes for System Monitor (main.py) in a preview window on the computer # The preview window is refreshed as soon as the theme file is modified @@ -30,7 +31,6 @@ import platform import subprocess import sys -import time try: import tkinter @@ -379,6 +379,7 @@ def loop(self): self.refresh() # Regularly update the viewer window even if content unchanged, or it will appear as "not responding" self.update() + time.sleep(0.1) def main(theme: str = None):