#!/usr/bin/python3 import argparse import logging import tkinter as tk from tkinter import TclError, filedialog, ttk from pathlib import Path import os import webbrowser import subprocess from functools import partial from shared_libs.gitea import GiteaUpdate from shared_libs.message import MessageDialog from shared_libs.common_tools import ( LogConfig, ConfigManager, ThemeManager, LxTools, Tooltip, ) import sys from file_and_dir_ensure import prepare_app_environment class LogViewer(tk.Tk): def __init__(self, modul_name): super().__init__() self.my_tool_tip = None self.modul_name = modul_name # Save the module name # from here the calls must be made with the module name _ = modul_name.AppConfig.setup_translations() self.x_width = modul_name.AppConfig.UI_CONFIG["window_size"][0] self.y_height = modul_name.AppConfig.UI_CONFIG["window_size"][1] # Set the window size self.geometry(f"{self.x_width}x{self.y_height}") self.minsize( modul_name.AppConfig.UI_CONFIG["window_size"][0], modul_name.AppConfig.UI_CONFIG["window_size"][1], ) self.title(modul_name.AppConfig.UI_CONFIG["window_title2"]) self.tk.call( "source", f"{modul_name.AppConfig.SYSTEM_PATHS['tcl_path']}/water.tcl" ) ConfigManager.init(modul_name.AppConfig.SETTINGS_FILE) theme = ConfigManager.get("theme") ThemeManager.change_theme(self, theme) LxTools.center_window_cross_platform(self, self.x_width, self.y_height) self.createWidgets(modul_name, _) self.load_file(_, modul_name=modul_name) self.log_icon = tk.PhotoImage(file="/usr/share/icons/lx-icons/48/log.png") self.update_icon = tk.PhotoImage( file="/usr/share/icons/lx-icons/16/settings.png" ) self.iconphoto(True, self.log_icon) self.grid_rowconfigure(0, weight=1) self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(0, weight=1) # StringVar-Variables initialization self.tooltip_state = tk.BooleanVar() # Get value from configuration state = ConfigManager.get("tooltips") # NOTE: ConfigManager.get("tooltips") can return either a boolean value or a string, # depending on whether the value was loaded from the file (bool) or the default value is used (string). # The expression 'lines[5].strip() == "True"' in ConfigManager.load() converts the string to a boolean. # Convert to boolean and set if isinstance(state, bool): # If it's already a boolean, use directly self.tooltip_state.set(state) else: # If it's a string or something else self.tooltip_state.set(str(state) == "True") self.tooltip_label = ( tk.StringVar() ) # StringVar-Variable for tooltip label for view Disabled/Enabled self.tooltip_update_label(modul_name, _) self.update_label = tk.StringVar() # StringVar-Variable for update label self.update_tooltip = ( tk.StringVar() ) # StringVar-Variable for update tooltip please not remove! self.update_foreground = tk.StringVar(value="red") # Frame for Menu self.menu_frame = ttk.Frame(self) self.menu_frame.configure(relief="flat") if "'logview_app_config'" in f"{modul_name}".split(): self.menu_frame.grid(column=0, row=0, columnspan=4, sticky=tk.NSEW) # App Menu self.version_lb = ttk.Label(self.menu_frame, text=modul_name.AppConfig.VERSION) self.version_lb.config(font=("Ubuntu", 11), foreground="#00c4ff") self.version_lb.grid(column=0, row=0, rowspan=4, padx=10, pady=10) Tooltip( self.version_lb, f"Version: {modul_name.AppConfig.VERSION[2:]}", self.tooltip_state, ) self.load_button = ttk.Button( self.menu_frame, text=_("Load Log"), style="Toolbutton", command=lambda: self.directory_load(modul_name, _), ) self.load_button.grid(column=1, row=0) self.options_btn = ttk.Menubutton(self.menu_frame, text=_("Options")) self.options_btn.grid(column=2, row=0) Tooltip(self.options_btn, modul_name.Msg.TTIP["settings"], self.tooltip_state) self.set_update = tk.IntVar() self.settings = tk.Menu(self, relief="flat") self.options_btn.configure(menu=self.settings, style="Toolbutton") self.settings.add_checkbutton( label=_("Disable Updates"), command=lambda: self.update_setting(self.set_update.get(), modul_name, _), variable=self.set_update, ) self.updates_lb = ttk.Label(self.menu_frame, textvariable=self.update_label) self.updates_lb.grid(column=5, row=0, padx=10) self.updates_lb.grid_remove() self.update_label.trace_add("write", self.update_label_display) self.update_foreground.trace_add("write", self.update_label_display) res = GiteaUpdate.api_down( modul_name.AppConfig.UPDATE_URL, modul_name.AppConfig.VERSION, ConfigManager.get("updates"), ) self.update_ui_for_update(res, modul_name, _) # Tooltip Menu self.settings.add_command( label=self.tooltip_label.get(), command=lambda: self.tooltips_toggle(modul_name, _), ) # Label show dark or light self.theme_label = tk.StringVar() self.update_theme_label(modul_name, _) self.settings.add_command( label=self.theme_label.get(), command=lambda: self.on_theme_toggle(modul_name, _), ) # About BTN Menu / Label self.about_btn = ttk.Button( self.menu_frame, text=_("About"), style="Toolbutton", command=lambda: self.about(modul_name, _), ) self.about_btn.grid(column=3, row=0) self.readme = tk.Menu(self) # self.grid_rowconfigure(0, weight=) self.grid_rowconfigure(1, weight=25) self.grid_columnconfigure(0, weight=1) # Method that is called when the variable changes def update_label_display(self, *args): # Set the foreground color self.updates_lb.configure(foreground=self.update_foreground.get()) # Show or hide the label based on whether it contains text if self.update_label.get(): # Make sure the label is in the correct position every time it's shown self.updates_lb.grid(column=5, row=0, padx=10) else: self.updates_lb.grid_remove() def updater(self): """Start the lxtools_installer""" tmp_dir = Path("/tmp/lxtools") Path.mkdir(tmp_dir, exist_ok=True) os.chdir(tmp_dir) result = subprocess.run(["/usr/local/bin/lxtools_installer"], check=False) if result.returncode != 0: MessageDialog("error", result.stderr) # Update the labels based on the result def update_ui_for_update(self, res, modul_name, _): """Update UI elements based on an update check result""" # First, remove the update button if it exists to avoid conflicts if hasattr(self, "update_btn"): self.update_btn.grid_forget() delattr(self, "update_btn") if res == "False": self.set_update.set(value=1) self.update_label.set(_("Update search off")) self.update_tooltip.set(_("Updates you have disabled")) # Clear the foreground color as requested self.update_foreground.set("") # Set the tooltip for the label Tooltip(self.updates_lb, self.update_tooltip.get(), self.tooltip_state) elif res == "No Internet Connection!": self.update_label.set(_("No Server Connection!")) self.update_foreground.set("red") # Set the tooltip for "No Server Connection" Tooltip( self.updates_lb, _("Could not connect to update server"), self.tooltip_state, ) elif res == "No Updates": self.update_label.set(_("No Updates")) self.update_tooltip.set(_("Congratulations! Wire-Py is up to date")) self.update_foreground.set("") # Set the tooltip for the label Tooltip(self.updates_lb, self.update_tooltip.get(), self.tooltip_state) else: self.set_update.set(value=0) # Clear the label text since we'll show the button instead self.update_label.set("") # Create the update button self.update_btn = ttk.Button( self.menu_frame, image=self.update_icon, style="Toolbutton", command=self.updater, ) self.update_btn.grid(column=5, row=0, padx=0) Tooltip( self.update_btn, _("Click to install new version"), self.tooltip_state ) @staticmethod def about(modul_name, _) -> None: """ a tk.Toplevel window """ msg_t = _( "Logviewer a simple Gui for View Logfiles.\n\n" "Logviewer is open source software written in Python.\n\n" "Email: polunga40@unity-mail.de also likes for donation.\n\n" "Use without warranty!\n" ) MessageDialog( "info", text=msg_t, buttons=["OK", "Go to Logviewer"], commands=[ None, # Default on "OK" partial(webbrowser.open, "https://git.ilunix.de/punix/shared_libs"), ], icon=modul_name.AppConfig.IMAGE_PATHS["icon_log"], title="Logviewer", ) def update_setting(self, update_res, modul_name, _) -> None: """write off or on in file Args: update_res (int): argument that is passed contains 0 or 1 """ if update_res == 1: # Disable updates ConfigManager.set("updates", "off") # When updates are disabled, we know the result should be "False" self.update_ui_for_update("False", modul_name, _) else: # Enable updates ConfigManager.set("updates", "on") # When enabling updates, we need to actually check for updates try: # Force a fresh check by passing "on" as the update setting res = GiteaUpdate.api_down( modul_name.AppConfig.UPDATE_URL, modul_name.AppConfig.VERSION, "on" ) # Make sure the UI is updated regardless of the previous state if hasattr(self, "update_btn"): self.update_btn.grid_forget() if hasattr(self, "updates_lb"): self.updates_lb.grid_forget() # Now update the UI with the fresh result self.update_ui_for_update(res, modul_name, _) except Exception as e: logging.error(f"Error checking for updates: {e}") # Fallback to a default message if there's an error self.update_ui_for_update("No Internet Connection!", modul_name, _) def tooltip_update_label(self, modul_name, _) -> None: """Updates the tooltip menu label based on the current tooltip status""" # Set the menu text based on the current status if self.tooltip_state.get(): # If tooltips are enabled, the menu option should be to disable them self.tooltip_label.set(_("Disable Tooltips")) else: # If tooltips are disabled, the menu option should be to enable them self.tooltip_label.set(_("Enable Tooltips")) def tooltips_toggle(self, modul_name, _): """ Toggles the visibility of tooltips (on/off) and updates the corresponding menu label. Inverts the current tooltip state (`self.tooltip_state`), saves the new value in the configuration, and applies the change immediately. Updates the menu entry's label to reflect the new tooltip status (e.g., "Tooltips: On" or "Tooltips: Off"). """ # Toggle the boolean state new_bool_state = not self.tooltip_state.get() # Save the converted value in the configuration ConfigManager.set("tooltips", str(new_bool_state)) # Update the tooltip_state variable for immediate effect self.tooltip_state.set(new_bool_state) # Update the menu label self.tooltip_update_label(modul_name, _) # Update the menu entry - find the correct index # This assumes it's the third item (index 2) in your menu self.settings.entryconfigure(1, label=self.tooltip_label.get()) def update_theme_label(self, modul_name, _) -> None: """Update the theme label based on the current theme""" current_theme = ConfigManager.get("theme") if current_theme == "light": self.theme_label.set(_("Dark")) else: self.theme_label.set(_("Light")) def on_theme_toggle(self, modul_name, _) -> None: """Toggle between light and dark theme""" current_theme = ConfigManager.get("theme") new_theme = "dark" if current_theme == "light" else "light" ThemeManager.change_theme(self, new_theme, new_theme) self.update_theme_label(modul_name, _) # Update the theme label # Update Menulfield self.settings.entryconfigure(2, label=self.theme_label.get()) def createWidgets(self, modul_name, _): text_frame = ttk.Frame(self) text_frame.grid(row=1, column=0, padx=5, pady=5, sticky=tk.NSEW) text_frame.rowconfigure(0, weight=3) text_frame.columnconfigure(0, weight=1) next_frame = ttk.Frame(self) next_frame.grid(row=2, column=0, sticky=tk.NSEW) next_frame.rowconfigure(2, weight=1) next_frame.columnconfigure(1, weight=1) # Create a Text widget for displaying the log file self.text_area = tk.Text( text_frame, wrap=tk.WORD, padx=5, pady=5, relief="flat" ) self.text_area.grid(row=0, column=0, sticky=tk.NSEW) self.text_area.tag_configure( "found-tag", foreground="yellow", background="green" ) # Create a vertical scrollbar for the Text widget v_scrollbar = ttk.Scrollbar( text_frame, orient="vertical", command=self.text_area.yview ) v_scrollbar.grid(row=0, column=1, sticky=tk.NS) self.text_area.configure(yscrollcommand=v_scrollbar.set) self._entry = ttk.Entry(next_frame) self._entry.bind("", lambda e: self._onFind()) self._entry.grid(row=0, column=1, padx=5, sticky=tk.EW) # Add a context menu to the Text widget self.context_menu = tk.Menu(self, tearoff=0) self.context_menu.add_command(label=_("Copy"), command=self.copy_text) self.context_menu.add_command(label=_("Paste"), command=self.paste_into_entry) self.text_area.bind("", self.show_context_menu) self._entry.bind("", self.show_context_menu) search_button = ttk.Button(next_frame, text=_("Search"), command=self._onFind) search_button.grid(row=0, column=0, padx=5, pady=5, sticky=tk.EW) delete_button = ttk.Button( next_frame, text=_("Delete_Log"), command=self.delete_file ) delete_button.grid(row=0, column=2, padx=5, pady=5, sticky=tk.EW) def show_text_menu(self, event): try: self.configure.tk_popup(event.x_root, event.y_root) finally: self.context_menu.grab_release() def copy_text(self): try: selected_text = self.text_area.selection_get() self.clipboard_clear() self.clipboard_append(selected_text) except tk.TclError: # No Text selected pass def show_context_menu(self, event): try: self.context_menu.tk_popup(event.x_root, event.y_root) finally: self.context_menu.grab_release() def paste_into_entry(self): try: text = self.clipboard_get() self._entry.delete(0, tk.END) self._entry.insert(tk.END, text) except tk.TclError: # No Text on Clipboard pass def _onFind(self): searchText = self._entry.get() if len(searchText) == 0: return # Set the search start position to the last found position (initial value: "1.0") start_pos = self.last_search_pos if hasattr(self, "last_search_pos") else "1.0" var = tk.IntVar() foundIndex = self.text_area.search( searchText, start_pos, stopindex=tk.END, nocase=tk.YES, count=var, regexp=tk.YES, ) if not foundIndex: # No further entry found, reset to the beginning self.last_search_pos = "1.0" return count = var.get() lastIndex = self.text_area.index(f"{foundIndex} + {count}c") # Remove and reapply highlighting self.text_area.tag_remove("found-tag", "1.0", tk.END) self.text_area.tag_add("found-tag", foundIndex, lastIndex) # Update the start position for the next search self.last_search_pos = lastIndex self.text_area.see(foundIndex) def delete_file(self, modul_name): Path.unlink(modul_name.AppConfig.LOG_FILE_PATH) modul_name.AppConfig.ensure_log() def load_file(self, _, modul_name): try: if not modul_name.AppConfig.LOG_FILE_PATH: return with open( modul_name.AppConfig.LOG_FILE_PATH, "r", encoding="utf-8" ) as file: self.text_area.delete(1.0, tk.END) self.text_area.insert(tk.END, file.read()) except Exception as e: logging.error(_(f"A mistake occurred: {str(e)}")) MessageDialog("error", _(f"A mistake occurred:\n{str(e)}\n")) def directory_load(self, modul_name, _): filepath = filedialog.askopenfilename( initialdir=f"{Path.home() / ".local/share/lxlogs/"}", title="Select a Logfile File", filetypes=[("Logfiles", "*.log")], ) try: with open(filepath, "r", encoding="utf-8") as file: self.text_area.delete(1.0, tk.END) self.text_area.insert(tk.END, file.read()) except (IsADirectoryError, TypeError, FileNotFoundError): print("File load: abort by user...") except Exception as e: logging.error(_(f"A mistake occurred: {e}")) MessageDialog("error", _(f"A mistake occurred:\n{e}\n")) def main(): # Create an ArgumentParser object parser = argparse.ArgumentParser( description="LogViewer with optional module loading." ) parser.add_argument( "--modul", type=str, default="logview_app_config", help="Give the name of the module to load.", ) args = parser.parse_args() import importlib try: modul = importlib.import_module(args.modul) except ModuleNotFoundError: print(f"Modul '{args.modul}' not found") print("For help use logviewer -h") sys.exit(1) except Exception as e: print(f"Error load Modul: {str(e)}") sys.exit(1) prepare_app_environment() app = LogViewer(modul) LogConfig.logger(ConfigManager.get("logfile")) """ the hidden files are hidden in Filedialog """ try: app.tk.call("tk_getOpenFile", "-foobarbaz") except TclError: pass app.tk.call("set", "::tk::dialog::file::showHiddenBtn", "1") app.tk.call("set", "::tk::dialog::file::showHiddenVar", "0") app.mainloop() if __name__ == "__main__": main()