first commit of shared_libs coarse build
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
debug.log
|
||||||
|
.venv
|
||||||
|
.venv.bak
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
__pycache__
|
33
Changelog
Normal file
33
Changelog
Normal 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
3
__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
|
573
common_tools.py
Executable file
573
common_tools.py
Executable 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
22
file_and_dir_ensure.py
Normal 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
143
gitea.py
Normal 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
146
logview_app_config.py
Normal 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
526
logviewer.py
Executable 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()
|
Reference in New Issue
Block a user