#!/usr/bin/python3 import argparse import logging import tkinter as tk from tkinter import TclError, filedialog, ttk from pathlib import Path import webbrowser 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(_) self.load_file(_, modul_name=modul_name) self.log_icon = tk.PhotoImage(file=modul_name.AppConfig.IMAGE_PATHS["icon_log"]) 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() # 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) update_text = f"Update {res} {_('available!')}" # Clear the label text since we'll show the button instead self.update_label.set("") # Create the update button self.update_btn = ttk.Menubutton(self.menu_frame, text=update_text) self.update_btn.grid(column=5, row=0, padx=0) Tooltip( self.update_btn, _("Click to download new version"), self.tooltip_state ) self.download = tk.Menu(self, relief="flat") self.update_btn.configure(menu=self.download, style="Toolbutton") self.download.add_command( label=_("Download"), command=lambda: GiteaUpdate.download( f"{modul_name.AppConfig.DOWNLOAD_URL}/{res}.zip", res ), ) @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, _): 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()