first commit of shared_libs coarse build

This commit is contained in:
2025-06-04 00:40:07 +02:00
parent 25355baf37
commit d1521ac9f5
8 changed files with 1452 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
debug.log
.venv
.venv.bak
.idea
.vscode
__pycache__

33
Changelog Normal file
View File

@ -0,0 +1,33 @@
Changelog for shared_libs
## [Unreleased]
- add Info Window for user in delete logfile
bevore delete logfile.
### Added
03-06-2025
- add method for logfile Button.
### Added
02-06-2025
- add Button for another logfiles.
- eception handling for logfile when modul is not found.
### Added
31-05-2025
- Add menu for logviewer.
- Add KontextMenu for textfield.
- Resizeable logviewer with minsize.
### Added
30-05-2025
- Create shared_libs for better structure in projects by git.ilunix.de
moduls from shared_libs can be used in other projects.

3
__init__.py Normal file
View File

@ -0,0 +1,3 @@
#!/usr/bin/python3

573
common_tools.py Executable file
View File

@ -0,0 +1,573 @@
""" Classes Method and Functions for lx Apps """
import logging
import signal
import base64
from subprocess import CompletedProcess, run
import re
import sys
import shutil
import tkinter as tk
from typing import Optional, Dict, Any, NoReturn
from pathlib import Path
from tkinter import ttk, Toplevel
class CryptoUtil:
"""
This class is for the creation of the folders and files
required by Wire-Py, as well as for decryption
the tunnel from the user's home directory
"""
@staticmethod
def decrypt(user, path) -> None:
"""
Starts SSL dencrypt
"""
if len([file.stem for file in path.glob("*.dat")]) == 0:
pass
else:
process: CompletedProcess[str] = run(
["pkexec", "/usr/local/bin/ssl_decrypt.py", "--user", user],
capture_output=True,
text=True,
check=False,
)
# Output from Openssl Error
if process.stderr:
logging.error(process.stderr, exc_info=True)
if process.returncode == 0:
logging.info("Files successfully decrypted...", exc_info=True)
else:
logging.error(
f"Error process decrypt: Code {process.returncode}", exc_info=True
)
@staticmethod
def encrypt(user) -> None:
"""
Starts SSL encryption
"""
process: CompletedProcess[str] = run(
["pkexec", "/usr/local/bin/ssl_encrypt.py", "--user", user],
capture_output=True,
text=True,
check=False,
)
# Output from Openssl Error
if process.stderr:
logging.error(process.stderr, exc_info=True)
if process.returncode == 0:
logging.info("Files successfully encrypted...", exc_info=True)
else:
logging.error(
f"Error process encrypt: Code {process.returncode}", exc_info=True
)
@staticmethod
def find_key(key: str = "") -> bool:
"""
Checks if the private key already exists in the system using an external script.
Returns True only if the full key is found exactly (no partial match).
"""
process: CompletedProcess[str] = run(
["pkexec", "/usr/local/bin/match_found.py", key],
capture_output=True,
text=True,
check=False,
)
if "True" in process.stdout:
return True
elif "False" in process.stdout:
return False
logging.error(
f"Unexpected output from the external script:\nSTDOUT: {process.stdout}\nSTDERR: {process.stderr}",
exc_info=True,
)
return False
@staticmethod
def is_valid_base64(key: str) -> bool:
"""
Validates if the input is a valid Base64 string (WireGuard private key format).
Returns True only for non-empty strings that match the expected length.
"""
# Check for empty string
if not key or key.strip() == "":
return False
# Regex pattern to validate Base64: [A-Za-z0-9+/]+={0,2}
base64_pattern = r"^[A-Za-z0-9+/]+={0,2}$"
if not re.match(base64_pattern, key):
return False
try:
# Decode and check length (WireGuard private keys are 32 bytes long)
decoded = base64.b64decode(key)
if len(decoded) != 32: # 32 bytes = 256 bits
return False
except Exception as e:
logging.error(f"Error on decode Base64: {e}", exc_info=True)
return False
return True
class LxTools:
"""
Class LinuxTools methods that can also be used for other apps
"""
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
@staticmethod
def center_window_cross_platform(window, width, height):
"""
Centers a window on the primary monitor in a way that works on both X11 and Wayland
Args:
window: The tkinter window to center
width: Window width
height: Window height
"""
# Calculate the position before showing the window
# First attempt: Try to use GDK if available (works on both X11 and Wayland)
try:
import gi
gi.require_version("Gdk", "3.0")
from gi.repository import Gdk
display = Gdk.Display.get_default()
monitor = display.get_primary_monitor() or display.get_monitor(0)
geometry = monitor.get_geometry()
scale_factor = monitor.get_scale_factor()
# Calculate center position on the primary monitor
x = geometry.x + (geometry.width - width // scale_factor) // 2
y = geometry.y + (geometry.height - height // scale_factor) // 2
# Set window geometry
window.geometry(f"{width}x{height}+{x}+{y}")
return
except (ImportError, AttributeError):
pass
# Second attempt: Try xrandr for X11
try:
import subprocess
output = subprocess.check_output(
["xrandr", "--query"], universal_newlines=True
)
# Parse the output to find the primary monitor
primary_info = None
for line in output.splitlines():
if "primary" in line:
parts = line.split()
for part in parts:
if "x" in part and "+" in part:
primary_info = part
break
break
if primary_info:
# Parse the geometry: WIDTH x HEIGHT+X+Y
geometry = primary_info.split("+")
dimensions = geometry[0].split("x")
primary_width = int(dimensions[0])
primary_height = int(dimensions[1])
primary_x = int(geometry[1])
primary_y = int(geometry[2])
# Calculate center position on the primary monitor
x = primary_x + (primary_width - width) // 2
y = primary_y + (primary_height - height) // 2
# Set window geometry
window.geometry(f"{width}x{height}+{x}+{y}")
return
except (ImportError, IndexError, ValueError):
pass
# Final fallback: Use standard Tkinter method
screen_width = window.winfo_screenwidth()
screen_height = window.winfo_screenheight()
# Try to make an educated guess for multi-monitor setups
# If screen width is much larger than height, assume multiple monitors side by side
if (
screen_width > screen_height * 1.8
): # Heuristic for detecting multiple monitors
# Assume the primary monitor is on the left half
screen_width = screen_width // 2
x = (screen_width - width) // 2
y = (screen_height - height) // 2
window.geometry(f"{width}x{height}+{x}+{y}")
@staticmethod
def clean_files(tmp_dir: Path = None, file: Path = None) -> None:
"""
Deletes temporary files and directories for cleanup when exiting the application.
This method safely removes an optional directory defined by `AppConfig.TEMP_DIR`
and a single file to free up resources at the end of the program's execution.
All operations are performed securely, and errors such as `FileNotFoundError`
are ignored if the target files or directories do not exist.
:param tmp_dir: (Path, optional): Path to the temporary directory that should be deleted.
If `None`, the value of `AppConfig.TEMP_DIR` is used.
:param file: (Path, optional): Path to the file that should be deleted.
If `None`, no additional file will be deleted.
Returns:
None: The method does not return any value.
"""
if tmp_dir is not None:
shutil.rmtree(tmp_dir, ignore_errors=True)
try:
if file is not None:
Path.unlink(file)
except FileNotFoundError:
pass
@staticmethod
def msg_window(
image_path: Path,
image_path2: Path,
w_title: str,
w_txt: str,
txt2: Optional[str] = None,
com: Optional[str] = None,
) -> None:
"""
Creates message windows
:param image_path2:
:param image_path:
AppConfig.IMAGE_PATHS["icon_info"] = Image for TK window which is displayed to the left of the text
AppConfig.IMAGE_PATHS["icon_vpn"] = Image for Task Icon
:argument w_title = Windows Title
:argument w_txt = Text for Tk Window
:argument txt2 = Text for Button two
:argument com = function for Button command
"""
msg: tk.Toplevel = tk.Toplevel()
msg.resizable(width=False, height=False)
msg.title(w_title)
msg.configure(pady=15, padx=15)
# load first image for a window
try:
msg.img = tk.PhotoImage(file=image_path)
msg.i_window = tk.Label(msg, image=msg.img)
except Exception as e:
logging.error(f"Error on load Window Image: {e}", exc_info=True)
msg.i_window = tk.Label(msg, text="Image not found")
label: tk.Label = tk.Label(msg, text=w_txt)
label.grid(column=1, row=0)
if txt2 is not None and com is not None:
label.config(font=("Ubuntu", 11), padx=15, justify="left")
msg.i_window.grid(column=0, row=0, sticky="nw")
button2: ttk.Button = ttk.Button(
msg, text=f"{txt2}", command=com, padding=4
)
button2.grid(column=0, row=1, sticky="e", columnspan=2)
button: ttk.Button = ttk.Button(
msg, text="OK", command=msg.destroy, padding=4
)
button.grid(column=0, row=1, sticky="w", columnspan=2)
else:
label.config(font=("Ubuntu", 11), padx=15)
msg.i_window.grid(column=0, row=0)
button: ttk.Button = ttk.Button(
msg, text="OK", command=msg.destroy, padding=4
)
button.grid(column=0, columnspan=2, row=1)
try:
icon = tk.PhotoImage(file=image_path2)
msg.iconphoto(True, icon)
except Exception as e:
logging.error(f"Error loading the window icon: {e}", exc_info=True)
msg.columnconfigure(0, weight=1)
msg.rowconfigure(0, weight=1)
msg.winfo_toplevel()
@staticmethod
def sigi(file_path: Optional[Path] = None, file: Optional[Path] = None) -> None:
"""
Function for cleanup after a program interruption
:param file: Optional - File to be deleted
:param file_path: Optional - Directory to be deleted
"""
def signal_handler(signum: int, frame: Any) -> NoReturn:
"""
Determines clear text names for signal numbers and handles signals
Args:
signum: The signal number
frame: The current stack frame
Returns:
NoReturn since the function either exits the program or continues execution
"""
signals_to_names_dict: Dict[int, str] = dict(
(getattr(signal, n), n)
for n in dir(signal)
if n.startswith("SIG") and "_" not in n
)
signal_name: str = signals_to_names_dict.get(
signum, f"Unnamed signal: {signum}"
)
# End program for certain signals, report to others only reception
if signum in (signal.SIGINT, signal.SIGTERM):
exit_code: int = 1
logging.error(
f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}.",
exc_info=True,
)
LxTools.clean_files(file_path, file)
logging.info("Breakdown by user...")
sys.exit(exit_code)
else:
logging.info(f"Signal {signum} received and ignored.")
LxTools.clean_files(file_path, file)
logging.error("Process unexpectedly ended...")
# Register signal handlers for various signals
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGHUP, signal_handler)
# ConfigManager with caching
class ConfigManager:
"""
Universal class for managing configuration files with caching support.
This class provides a general solution to load, save, and manage configuration
files across different projects. It uses a caching system to optimize access efficiency.
The `init()` method initializes the configuration file path, while `load()` and `save()`
synchronize data between the file and internal memory structures.
Key Features:
- Caching to minimize I/O operations.
- Default values for missing or corrupted configuration files.
- Reusability across different projects and use cases.
The class is designed for central application configuration management, working closely
with `ThemeManager` to dynamically manage themes or other settings.
"""
_config = None
_config_file = None
@classmethod
def init(cls, config_file):
"""Initial the Configmanager with the given config file"""
cls._config_file = config_file
cls._config = None # Reset the cache
@classmethod
def load(cls):
"""Load the config file and return the config as dict"""
if not cls._config:
try:
lines = Path(cls._config_file).read_text(encoding="utf-8").splitlines()
cls._config = {
"updates": lines[1].strip(),
"theme": lines[3].strip(),
"tooltips": lines[5].strip()
== "True", # is converted here to boolean!!!
"autostart": lines[7].strip() if len(lines) > 7 else "off",
"logfile": lines[9].strip(),
}
except (IndexError, FileNotFoundError):
# DeDefault values in case of error
cls._config = {
"updates": "on",
"theme": "light",
"tooltips": "True", # Default Value as string!
"autostart": "off",
"logfile": LOG_FILE_PATH,
}
return cls._config
@classmethod
def save(cls):
"""Save the config to the config file"""
if cls._config:
lines = [
"# Configuration\n",
f"{cls._config['updates']}\n",
"# Theme\n",
f"{cls._config['theme']}\n",
"# Tooltips\n",
f"{str(cls._config['tooltips'])}\n",
"# Autostart\n",
f"{cls._config['autostart']}\n",
"# Logfile\n",
f"{cls._config['logfile']}\n",
]
Path(cls._config_file).write_text("".join(lines), encoding="utf-8")
@classmethod
def set(cls, key, value):
"""Sets a configuration value and saves the change"""
cls.load()
cls._config[key] = value
cls.save()
@classmethod
def get(cls, key, default=None):
"""Returns a configuration value"""
config = cls.load()
return config.get(key, default)
class ThemeManager:
"""
Class for central theme management and UI customization.
This static class allows dynamic adjustment of the application's appearance.
The method `change_theme()` updates the current theme and saves
the selection in the configuration file via `ConfigManager`.
It ensures a consistent visual design across the entire project.
Key Features:
- Central control over themes.
- Automatic saving of theme settings to the configuration file.
- Tight integration with `ConfigManager` for persistent storage of preferences.
The class is designed to apply themes consistently throughout the application,
ensuring that changes are traceable and uniform across all parts of the project.
"""
@staticmethod
def change_theme(root, theme_in_use, theme_name=None):
"""Change application theme centrally"""
root.tk.call("set_theme", theme_in_use)
if theme_in_use == theme_name:
ConfigManager.set("theme", theme_in_use)
class Tooltip:
"""Class for Tooltip
from common_tools.py import Tooltip
example: Tooltip(label, "Show tooltip on label")
example: Tooltip(button, "Show tooltip on button")
example: Tooltip(widget, "Text", state_var=tk.BooleanVar())
example: Tooltip(widget, "Text", state_var=tk.BooleanVar(), x_offset=20, y_offset=10)
info: label and button are parent widgets.
NOTE: When using with state_var, pass the tk.BooleanVar object directly,
NOT its value. For example: use state_var=my_bool_var, NOT state_var=my_bool_var.get()
"""
def __init__(
self,
widget: Any,
text: str,
state_var: Optional[tk.BooleanVar] = None,
x_offset: int = 65,
y_offset: int = 40,
) -> None:
"""Tooltip Class"""
self.widget: Any = widget
self.text: str = text
self.tooltip_window: Optional[Toplevel] = None
self.state_var = state_var
self.x_offset = x_offset
self.y_offset = y_offset
# Initial binding based on the current state
self.update_bindings()
# Add trace to the state_var if provided
if self.state_var is not None:
self.state_var.trace_add("write", self.update_bindings)
def update_bindings(self, *args) -> None:
"""Updates the bindings based on the current state"""
# Remove existing bindings first
self.widget.unbind("<Enter>")
self.widget.unbind("<Leave>")
# Add new bindings if tooltips are enabled
if self.state_var is None or self.state_var.get():
self.widget.bind("<Enter>", self.show_tooltip)
self.widget.bind("<Leave>", self.hide_tooltip)
def show_tooltip(self, event: Optional[Any] = None) -> None:
"""Shows the tooltip"""
if self.tooltip_window or not self.text:
return
x: int
y: int
cx: int
cy: int
x, y, cx, cy = self.widget.bbox("insert")
x += self.widget.winfo_rootx() + self.x_offset
y += self.widget.winfo_rooty() + self.y_offset
self.tooltip_window = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
tw.wm_geometry(f"+{x}+{y}")
label: tk.Label = tk.Label(
tw,
text=self.text,
background="lightgreen",
foreground="black",
relief="solid",
borderwidth=1,
padx=5,
pady=5,
)
label.grid()
self.tooltip_window.after(2200, lambda: tw.destroy())
def hide_tooltip(self, event: Optional[Any] = None) -> None:
"""Hides the tooltip"""
if self.tooltip_window:
self.tooltip_window.destroy()
self.tooltip_window = None
class LogConfig:
@staticmethod
def logger(file_path) -> None:
file_handler = logging.FileHandler(
filename=f"{file_path}",
mode="a",
encoding="utf-8",
)
formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
file_handler.setFormatter(formatter)
file_handler.setLevel(logging.DEBUG)
logger = logging.getLogger()
logger.addHandler(file_handler)

22
file_and_dir_ensure.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/python3
"""Utility functions for setting up the application."""
from logview_app_config import AppConfig
from pathlib import Path
# Logging
LOG_DIR = Path.home() / ".local/share/lxlogs"
Path(LOG_DIR).mkdir(parents=True, exist_ok=True)
LOG_FILE_PATH = LOG_DIR / "logviewer.log"
def prepare_app_environment() -> None:
"""Ensures that all required files and directories exist."""
AppConfig.ensure_directories()
AppConfig.create_default_settings()
AppConfig.ensure_log()
if __name__ == "__main__":
prepare_app_environment()

143
gitea.py Normal file
View File

@ -0,0 +1,143 @@
#!/usr/bin/python3
import gettext
import locale
import requests
from pathlib import Path
import subprocess
import shutil
from shared_libs.common_tools import LxTools
class GiteaUpdate:
"""
Calling download requests the download URL of the running script,
the taskbar image for the “Download OK” window, the taskbar image for the
“Download error” window, and the variable res
"""
@staticmethod
def api_down(update_api_url: str, version: str, update_setting: str = None) -> str:
"""
Checks for updates via API
Args:
update_api_url: Update API URL
version: Current version
update_setting: Update setting from ConfigManager (on/off)
Returns:
New version or status message
"""
# If updates are disabled, return immediately
if update_setting != "on":
return "False"
try:
response: requests.Response = requests.get(update_api_url, timeout=10)
response.raise_for_status() # Raise exception for HTTP errors
response_data = response.json()
if not response_data:
return "No Updates"
latest_version = response_data[0].get("tag_name")
if not latest_version:
return "Invalid API Response"
# Compare versions (strip 'v. ' prefix if present)
current_version = version[3:] if version.startswith("v. ") else version
if current_version != latest_version:
return latest_version
else:
return "No Updates"
except requests.exceptions.RequestException:
return "No Internet Connection!"
except (ValueError, KeyError, IndexError):
return "Invalid API Response"
@staticmethod
def download(urld: str, res: str) -> None:
"""
Downloads new version of application
:param urld: Download URL
:param res: Result filename
"""
try:
to_down: str = f"wget -qP {Path.home()} {" "} {urld}"
result: int = subprocess.call(to_down, shell=True)
if result == 0:
shutil.chown(f"{Path.home()}/{res}.zip", 1000, 1000)
LxTools.msg_window(
AppConfig.IMAGE_PATHS["icon_info"],
AppConfig.IMAGE_PATHS["icon_download"],
Msg.STR["title"],
Msg.STR["ok_message"],
)
else:
LxTools.msg_window(
AppConfig.IMAGE_PATHS["icon_error"],
AppConfig.IMAGE_PATHS["icon_download_error"],
Msg.STR["error_title"],
Msg.STR["error_massage"],
)
except subprocess.CalledProcessError:
LxTools.msg_window(
AppConfig.IMAGE_PATHS["icon_error"],
AppConfig.IMAGE_PATHS["icon_msg"],
Msg.STR["error_title"],
Msg.STR["error_no_internet"],
)
class AppConfig:
# Localization
APP_NAME: str = "gitea"
LOCALE_DIR: Path = Path("/usr/share/locale/")
@staticmethod
def setup_translations() -> gettext.gettext:
"""
Initialize translations and set the translation function
Special method for translating strings in this file
Returns:
The gettext translation function
"""
locale.bindtextdomain(AppConfig.APP_NAME, AppConfig.LOCALE_DIR)
gettext.bindtextdomain(AppConfig.APP_NAME, AppConfig.LOCALE_DIR)
gettext.textdomain(AppConfig.APP_NAME)
return gettext.gettext
# Images and icons paths
IMAGE_PATHS: dict[str, Path] = {
"icon_info": "/usr/share/icons/lx-icons/64/info.png",
"icon_error": "/usr/share/icons/lx-icons/64/error.png",
"icon_download": "/usr/share/icons/lx-icons/48/download.png",
"icon_download_error": "/usr/share/icons/lx-icons/48/download_error.png",
}
# here is initializing the class for translation strings
_ = AppConfig.setup_translations()
class Msg:
STR: dict[str, str] = {
# Strings for messages
"title": _("Download Successful"),
"ok_message": _("Your zip file is in home directory"),
"error_title": _("Download error"),
"error_message": _("Download failed! Please try again"),
"error_no_internet": _("Download failed! No internet connection!"),
}

146
logview_app_config.py Normal file
View File

@ -0,0 +1,146 @@
"""Configuration for the LogViewer application."""
import gettext
import locale
from pathlib import Path
from typing import Dict, Any
class AppConfig:
"""Central configuration and system setup manager for the LogViewer application.
This class serves as a singleton-like container for all global configuration data,
including paths, UI settings, localization, versioning, and system-specific resources.
It ensures that required directories, files, and services are created and configured
before the application starts. Additionally, it provides tools for managing translations,
default settings, and autostart functionality to maintain a consistent user experience.
Key Responsibilities:
- Centralizes all configuration values (paths, UI preferences, localization).
- Ensures required directories and files exist on startup.
- Handles translation setup via `gettext` for multilingual support.
- Manages default settings file generation.
- Configures autostart services using systemd for user-specific launch behavior.
This class is used globally across the application to access configuration data
consistently and perform system-level setup tasks.
"""
# Logging
LOG_DIR = Path.home() / ".local/share/lxlogs"
Path(LOG_DIR).mkdir(parents=True, exist_ok=True)
LOG_FILE_PATH = LOG_DIR / "logviewer.log"
# Localization
APP_NAME: str = "logviewer"
LOCALE_DIR: Path = Path("/usr/share/locale/")
# Base paths
BASE_DIR: Path = Path.home()
CONFIG_DIR: Path = BASE_DIR / ".config/logviewer"
# Configuration files
SETTINGS_FILE: Path = CONFIG_DIR / "settings"
DEFAULT_SETTINGS: Dict[str, str] = {
"# Configuration": "on",
"# Theme": "light",
"# Tooltips": True,
"# Autostart": "off",
"# Logfile": LOG_FILE_PATH,
}
# Updates
# 1 = 1. Year, 09 = Month of the Year, 2924 = Day and Year of the Year
VERSION: str = "v. 1.06.3125"
UPDATE_URL: str = "https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases"
DOWNLOAD_URL: str = "https://git.ilunix.de/punix/Wire-Py/archive"
# UI configuration
UI_CONFIG: Dict[str, Any] = {
"window_title2": "LogViewer",
"window_size": (600, 383),
"font_family": "Ubuntu",
"font_size": 11,
"resizable_window": (True, True),
}
# Images and icons paths
IMAGE_PATHS: Dict[str, Path] = {
"icon_info": "/usr/share/icons/lx-icons/64/info.png",
"icon_error": "/usr/share/icons/lx-icons/64/error.png",
"icon_log": "/usr/share/icons/lx-icons/48/log.png",
}
# System-dependent paths
SYSTEM_PATHS: Dict[str, Path] = {
"tcl_path": "/usr/share/TK-Themes",
}
@staticmethod
def setup_translations() -> gettext.gettext:
"""
Initialize translations and set the translation function
Special method for translating strings in this file
Returns:
The gettext translation function
"""
locale.bindtextdomain(AppConfig.APP_NAME, AppConfig.LOCALE_DIR)
gettext.bindtextdomain(AppConfig.APP_NAME, AppConfig.LOCALE_DIR)
gettext.textdomain(AppConfig.APP_NAME)
return gettext.gettext
@classmethod
def create_default_settings(cls) -> None:
"""Creates default settings if they don't exist"""
if not cls.SETTINGS_FILE.exists():
content = "\n".join(
f"[{k.upper()}]\n{v}" for k, v in cls.DEFAULT_SETTINGS.items()
)
cls.SETTINGS_FILE.write_text(content)
@classmethod
def ensure_directories(cls) -> None:
"""Ensures that all required directories exist"""
if not cls.CONFIG_DIR.exists():
cls.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
@classmethod
def ensure_log(cls) -> None:
"""Ensures that the log file exists"""
if not cls.LOG_FILE_PATH.exists():
cls.LOG_FILE_PATH.touch()
# here is initializing the class for translation strings
_ = AppConfig.setup_translations()
class Msg:
"""
A utility class that provides centralized access to translated message strings.
This class contains a dictionary of message strings used throughout the Wire-Py application.
All strings are prepared for translation using gettext. The short key names make the code
more concise while maintaining readability.
Attributes:
STR (dict): A dictionary mapping short keys to translated message strings.
Keys are abbreviated for brevity but remain descriptive.
Usage:
Import this class and access messages using the dictionary:
`Msg.STR["sel_tl"]` returns the translated "Select tunnel" message.
Note:
Ensure that gettext translation is properly initialized before
accessing these strings to ensure correct localization.
"""
STR: Dict[str, str] = {
# Strings for messages
}
TTIP: Dict[str, str] = {
# Strings for Tooltips
"settings": _("Click for Settings"),
}

526
logviewer.py Executable file
View File

@ -0,0 +1,526 @@
#!/usr/bin/python3
import argparse
import logging
import tkinter as tk
from tkinter import TclError, filedialog, ttk
from pathlib import Path
from shared_libs.gitea import GiteaUpdate
from shared_libs.common_tools import (
LogConfig,
ConfigManager,
ThemeManager,
LxTools,
Tooltip,
)
import sys
from file_and_dir_ensure import prepare_app_environment
import webbrowser
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
"""
def link_btn() -> None:
webbrowser.open("https://git.ilunix.de/punix/shared_libs")
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"
)
LxTools.msg_window(
modul_name.AppConfig.IMAGE_PATHS["icon_log"],
modul_name.AppConfig.IMAGE_PATHS["icon_log"],
_("Info"),
msg_t,
_("Go to shared_libs git"),
link_btn,
)
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)}"))
LxTools.msg_window(
modul_name.AppConfig.IMAGE_PATHS["icon_error"],
modul_name.AppConfig.IMAGE_PATHS["icon_log"],
"LogViewer",
_(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}"))
LxTools.msg_window(
modul_name.AppConfig.IMAGE_PATHS["icon_error"],
modul_name.AppConfig.IMAGE_PATHS["icon_log"],
"LogViewer",
_(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()