518 lines
20 KiB
Python
Executable File
518 lines
20 KiB
Python
Executable File
#!/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("<Return>", 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("<Button-3>", self.show_context_menu)
|
|
self._entry.bind("<Button-3>", 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()
|