blindwindow 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ include src/blindwindow/data/*
2
+ #recursive-include src/blindwindow/data/icons *
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: blindwindow
3
+ Version: 0.1.0
4
+ Summary: Route console stderr and stdout to blindwindow, to either duplicate or display what is otherwise hidden. Namely for MSIX deployed CLI's
5
+ Author-email: George Clayton Bennett <george.bennett@memphistn.gov>
6
+ Maintainer-email: George Clayton Bennett <george.bennett@memphistn.gov>
7
+ License-Expression: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Typing :: Typed
18
+ Classifier: Development Status :: 3 - Alpha
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: dworshak-config>=0.2.8
22
+ Requires-Dist: pyhabitat>=1.3.9
23
+ Requires-Dist: typer>=0.27.0
24
+ Requires-Dist: typer-helptree>=0.2.12
25
+ Requires-Dist: maxson-gui-utils>=0.1.5
26
+ Provides-Extra: gui
27
+ Requires-Dist: maxson_gui_utils>=0.1.4; extra == "gui"
28
+
29
+ # Blindwindow
30
+
31
+ Render console stderr and stdout to a TKinter based REPL widget masquerading as a log pange.
32
+
33
+ Use Case: When calling a CLI packaged inside of a MSIX that has been delivered via the Microsoft Store. This use case typically does not allow prints.
34
+ Example: `pdflinkcheck.exe serve --help`
35
+
36
+
37
+
38
+ ## Helptree
39
+
40
+ See the `blindwindow` Typer CLI structure.
41
+
42
+ ```bash
43
+ blindwindow helptree
44
+ ```
@@ -0,0 +1,16 @@
1
+ # Blindwindow
2
+
3
+ Render console stderr and stdout to a TKinter based REPL widget masquerading as a log pange.
4
+
5
+ Use Case: When calling a CLI packaged inside of a MSIX that has been delivered via the Microsoft Store. This use case typically does not allow prints.
6
+ Example: `pdflinkcheck.exe serve --help`
7
+
8
+
9
+
10
+ ## Helptree
11
+
12
+ See the `blindwindow` Typer CLI structure.
13
+
14
+ ```bash
15
+ blindwindow helptree
16
+ ```
@@ -0,0 +1,114 @@
1
+ [project]
2
+ name = "blindwindow"
3
+ dynamic = ["version"]
4
+ description = "Route console stderr and stdout to blindwindow, to either duplicate or display what is otherwise hidden. Namely for MSIX deployed CLI's"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+
8
+ dependencies = [
9
+ "dworshak-config>=0.2.8",
10
+ "pyhabitat>=1.3.9",
11
+ "typer>=0.27.0",
12
+ "typer-helptree>=0.2.12",
13
+ "maxson-gui-utils>=0.1.5",
14
+ ]
15
+
16
+ license = "MIT"
17
+ license-files = ["LICENSE"]
18
+
19
+ authors = [
20
+ { name = "George Clayton Bennett", email = "george.bennett@memphistn.gov" }
21
+ ]
22
+
23
+ maintainers = [
24
+ { name = "George Clayton Bennett", email = "george.bennett@memphistn.gov" }
25
+ ]
26
+
27
+ classifiers = [
28
+ "Programming Language :: Python :: 3",
29
+ "Programming Language :: Python :: 3 :: Only",
30
+ "Programming Language :: Python :: 3.10",
31
+ "Programming Language :: Python :: 3.11",
32
+ "Programming Language :: Python :: 3.12",
33
+ "Programming Language :: Python :: 3.13",
34
+ "Programming Language :: Python :: 3.14",
35
+ "Operating System :: OS Independent",
36
+ "Intended Audience :: Developers",
37
+ "Typing :: Typed",
38
+ "Development Status :: 3 - Alpha",
39
+ #"Development Status :: 4 - Beta",
40
+ #"Development Status :: 5 - Production/Stable",
41
+ ]
42
+
43
+ keywords = [
44
+
45
+ ]
46
+
47
+ #[project.urls]
48
+ #Homepage = ""
49
+ #Repository = ""
50
+ #Issues = ""
51
+ #Changelog = ""
52
+
53
+ [project.scripts]
54
+ "blindwindow" = "blindwindow.__main__:app"
55
+
56
+ [dependency-groups]
57
+ test = [
58
+ "pytest>=8.0.0",
59
+ "pytest-cov>=4.1.0",
60
+ ]
61
+ dev = [
62
+ { include-group = "test" },
63
+ "build>=1.3.0",
64
+ "pyinstaller>=6.17.0 ; platform_system == 'Linux' and platform_machine != 'aarch64'",
65
+ "shiv>=1.0.8",
66
+ "ruff>=0.7.0 ; platform_system == 'Linux' and platform_machine != 'aarch64'",
67
+ "maxson-build-utils>=0.1.33",
68
+ ]
69
+
70
+ [project.optional-dependencies]
71
+ gui = [
72
+ "maxson_gui_utils>=0.1.4",
73
+ ]
74
+
75
+
76
+ [build-system]
77
+ requires = ["setuptools>=64", "wheel"]
78
+ build-backend = "setuptools.build_meta"
79
+
80
+ [tool.uv.sources]
81
+ "blindwindow" = { path = "src/blindwindow" }
82
+
83
+ #[tool.setuptools]
84
+ #packages = [""blindwindow""]
85
+
86
+ [tool.setuptools.packages.find]
87
+ where = ["src"]
88
+
89
+ [tool.setuptools.dynamic]
90
+ version = { file = "src/blindwindow/VERSION" }
91
+
92
+ [tool.setuptools.package-data]
93
+ "blindwindow" = [
94
+ "data/*",
95
+ "data/icons/*",
96
+ "data/icons/**/*",
97
+ "data/themes/*",
98
+ "data/themes/**/*",
99
+ ]
100
+
101
+ [tool.maxson-build-utils.names]
102
+ import = "blindwindow"
103
+ pretty = "Blindwindow"
104
+
105
+ [tool.maxson-build-utils.pyinstaller]
106
+ mode = "onedir"
107
+ windowed = false
108
+ collect_data_pkgs = ["blindwindow"]
109
+ collect_binary_pkgs = []
110
+
111
+ [tool.maxson-build-utils.packaging.msix]
112
+ publisher = "CN=Development"
113
+ publisher_display_name = "George Clayton Bennett"
114
+ identity_name = "GeorgeClaytonBennett.Blindwindow"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ 0.1.0
2
+
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env python3
2
+ # src/blindwindow/__init__.py
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from ._version import __version__
8
+
9
+ # 1. Clean public-facing mapping
10
+ __all__ = [
11
+ "__version__",
12
+
13
+ "__gui_easteregg_enabled__",
14
+ ]
15
+
16
+
17
+ def _check_easteregg_env() -> bool:
18
+ """Helper to dynamically read environment state at call-time."""
19
+ env_flag = os.environ.get("BLINDWINDOW_GUI_EASTEREGG", "").strip().lower()
20
+ return env_flag in ("true", "1", "yes", "on")
21
+
22
+
23
+ # 2. Fully dynamic attribute routing
24
+ def __getattr__(name: str):
25
+
26
+ # Dynamic boolean evaluation for the breadcrumb attribute
27
+ if name == "__gui_easteregg_enabled__":
28
+ return _check_easteregg_env()
29
+
30
+ # Dynamic lookups for the GUI function invocation
31
+ if name == "start_gui":
32
+
33
+ def _missing_gui():
34
+ raise RuntimeError(
35
+ "start_gui requires pyhabitat and a Tkinter-capable environment"
36
+ )
37
+
38
+ _missing_gui.__name__ = "start_gui"
39
+ _missing_gui.__doc__ = (
40
+ "GUI support is unavailable in this environment."
41
+ )
42
+
43
+ if _check_easteregg_env():
44
+ try:
45
+ import pyhabitat
46
+
47
+ if pyhabitat.tkinter_is_available():
48
+ from .gui import start_gui
49
+
50
+ return start_gui
51
+ except ImportError:
52
+ pass
53
+
54
+ return _missing_gui
55
+
56
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
57
+
58
+
59
+ # 3. Dynamic introspection reflecting runtime changes
60
+ def __dir__():
61
+ exported = list(__all__)
62
+ if _check_easteregg_env():
63
+ exported.append("start_gui")
64
+
65
+ return sorted(
66
+ exported
67
+ + [
68
+ "__builtins__",
69
+ "__cached__",
70
+ "__doc__",
71
+ "__file__",
72
+ "__getattr__",
73
+ "__dir__",
74
+ "__loader__",
75
+ "__name__",
76
+ "__package__",
77
+ "__path__",
78
+ "__spec__",
79
+ ]
80
+ )
@@ -0,0 +1,7 @@
1
+ # src/blindwindow/__main__.py
2
+ from __future__ import annotations
3
+
4
+ from blindwindow.cli import app
5
+
6
+ if __name__ == "__main__":
7
+ app()
@@ -0,0 +1,23 @@
1
+
2
+ # src/blindwindow/_version.py
3
+ from pathlib import Path
4
+ from .context import APP_NAME
5
+ def get_version() -> str:
6
+ try:
7
+ version_file = Path(__file__).parent / "VERSION"
8
+ if version_file.exists():
9
+ return version_file.read_text(encoding="utf-8").strip()
10
+ except Exception:
11
+ pass
12
+
13
+ # Try metadata (Installed)
14
+ try:
15
+ from importlib.metadata import version, PackageNotFoundError
16
+ return version(APP_NAME)
17
+ except (ImportError, PackageNotFoundError):
18
+ pass
19
+
20
+
21
+ return "0.0.0-unknown"
22
+
23
+ __version__ = get_version()
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env python3
2
+ # src/blindwindow/cli.py
3
+
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+ import pyhabitat
11
+ import typer
12
+ from typer.models import OptionInfo
13
+ from rich.console import Console
14
+ from typer_helptree import add_typer_helptree
15
+
16
+ from ._version import __version__
17
+ from .context import APP_NAME, DESCRIPTION_STR
18
+ from .logging_setup import (
19
+ configure_logging_all_debug,
20
+ configure_logging_for_application,
21
+ )
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ console_stderr = Console(stderr=True)
26
+ console_stdout = Console()
27
+
28
+ # Force Rich to always enable colors, even when running from a .pyz bundle.
29
+ os.environ["FORCE_COLOR"] = "1"
30
+
31
+ # Optional but helpful for full terminal feature detection.
32
+ os.environ["TERM"] = "xterm-256color"
33
+
34
+ app = typer.Typer(
35
+ name=APP_NAME,
36
+ help=f"{DESCRIPTION_STR} (v{__version__})",
37
+ add_completion=False,
38
+ invoke_without_command=True,
39
+ no_args_is_help=True,
40
+ context_settings={
41
+ "ignore_unknown_options": True,
42
+ "allow_extra_args": True,
43
+ "help_option_names": ["-h", "--help"],
44
+ },
45
+ )
46
+
47
+
48
+ @app.callback(invoke_without_command=True)
49
+ def main(
50
+ ctx: typer.Context,
51
+ version: bool = typer.Option(
52
+ False,
53
+ "--version",
54
+ "-V",
55
+ help="Show application version and exit.",
56
+ ),
57
+ debug: bool = typer.Option(
58
+ False,
59
+ "--debug",
60
+ "-d",
61
+ help="Enable debug level logs for app.",
62
+ ),
63
+ all_debug: bool = typer.Option(
64
+ False,
65
+ "--all-debug",
66
+ help="Enable debug logs for app AND dependencies.",
67
+ ),
68
+ verbose: bool = typer.Option(
69
+ False,
70
+ "--verbose",
71
+ "-v",
72
+ help="Enable verbose info level logs.",
73
+ ),
74
+ log_file: Path | None = typer.Option(
75
+ None,
76
+ "--log-file",
77
+ help="Custom path to output log file.",
78
+ ),
79
+ ):
80
+ if version:
81
+ typer.echo(__version__)
82
+ raise typer.Exit()
83
+
84
+ if all_debug:
85
+ configure_logging_all_debug()
86
+ else:
87
+ configure_logging_for_application(
88
+ debug=debug,
89
+ verbose=verbose,
90
+ log_to_file=log_file is not None,
91
+ )
92
+
93
+ logger.debug("Executing command: %s", " ".join(sys.argv))
94
+
95
+ if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
96
+ typer.echo(ctx.get_help())
97
+ raise typer.Exit()
98
+
99
+
100
+ add_typer_helptree(
101
+ app=app,
102
+ console=console_stderr,
103
+ version=__version__,
104
+ hidden=False,
105
+ )
106
+
107
+ @app.command(name="gui")
108
+ def gui_command(
109
+ auto_close: int = typer.Option(0,
110
+ "--auto-close", "-c",
111
+ help = "Delay in milliseconds after which the GUI window will close (for automated testing). Use 0 to disable auto-closing.",
112
+ min=0)
113
+ )->None:
114
+ """
115
+ Launch tkinter-based GUI.
116
+ """
117
+ assured_auto_close_value = 0
118
+
119
+ # --- Helper, consistent gui failure message. ---
120
+ def _gui_failure_msg():
121
+ console_stderr.print("[bold red]GUI failed to launch[/bold red]")
122
+ console_stderr.print("Use CLI instead.")
123
+ console_stderr.print(f"pyhabitat.tkinter_is_available() = {pyhabitat.tkinter_is_available()}")
124
+ console_stderr.print(f"pyhabitat.on_termux() = {pyhabitat.on_termux()}")
125
+
126
+ if isinstance(auto_close, OptionInfo):
127
+ # Case 1: Called implicitly from main() (with no args)
128
+ # We received the metadata object, so use the function's default value (0).
129
+ # We don't need to do anything here since final_auto_close_value is already 0.
130
+ pass
131
+ else:
132
+ # Case 2: Called explicitly by Typer (gui -c 3000)
133
+ # Typer has successfully converted the command line argument, and auto_close is an int.
134
+ assured_auto_close_value = int(auto_close)
135
+
136
+ if not pyhabitat.tkinter_is_available():
137
+ _gui_failure_msg()
138
+ return
139
+
140
+ from .gui import start_gui
141
+ start_gui(time_auto_close = assured_auto_close_value)
142
+
143
+ @app.command(name="placeholder")
144
+ def placeholder(
145
+ path: Path = Path("path"),
146
+ ):
147
+ """Placeholder."""
148
+ console_stderr.print(f"{path=}")
149
+
150
+
151
+ if __name__ == "__main__":
152
+ app()
@@ -0,0 +1,9 @@
1
+ # src/blindwindow/config.py
2
+ from __future__ import annotations
3
+
4
+
5
+ def get_config_mngr():
6
+ from .context import APP_DIR
7
+ from dworshak_config import DworshakConfig
8
+ return DworshakConfig(path=APP_DIR / "config.json")
9
+
@@ -0,0 +1,20 @@
1
+ # src/blindwindow/context.py
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ PACKAGE_DIR = Path(__file__).resolve().parent
7
+ SRC_DIR = PACKAGE_DIR.parent
8
+ PROJECT_ROOT = SRC_DIR.parent
9
+
10
+ APP_NAME = "blindwindow"
11
+ APP_NAME_PRETTY = "Blindwindow"
12
+ IMPORT_NAME = "blindwindow"
13
+ SRC_FOLDER_NAME = "blindwindow"
14
+ DESCRIPTION_STR = "A Python application."
15
+ APP_DIR = Path.home() / ".blindwindow"
16
+ LOG_FILE_PATH = APP_DIR / "blindwindow.log"
17
+ SERVICE = APP_NAME
18
+ CONFIG_PATH = APP_DIR / "config.json"
19
+ SECRET_PATH = APP_DIR / "vault.db"
20
+ ENV_PATH = PROJECT_ROOT / ".env"
@@ -0,0 +1,3 @@
1
+ # src/blindwindow/core.py
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,215 @@
1
+
2
+ #!/usr/bin/env python3
3
+ # src/blindwindow/gui.py
4
+
5
+ from __future__ import annotations
6
+
7
+ import ctypes
8
+ import logging
9
+ import sys
10
+ import tkinter as tk
11
+ from importlib.resources import files
12
+ from tkinter import messagebox, ttk
13
+ from maxson_gui_utils.tk_utils import center_window_on_primary
14
+ from maxson_gui_utils.external_web_launch import launch_configured_website
15
+
16
+ import pyhabitat
17
+
18
+ from ._version import __version__
19
+ from .context import APP_NAME, IMPORT_NAME, CONFIG_PATH
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ APP_WIDTH = 800
24
+ APP_HEIGHT = 600
25
+
26
+
27
+ class GuiApp:
28
+ """Main application window."""
29
+
30
+ def __init__(self, root: tk.Tk):
31
+ self.root = root
32
+
33
+ #self._initialize_theme()
34
+ self._configure_window()
35
+ self._create_menubar()
36
+ self._create_widgets()
37
+
38
+ def _initialize_theme(self) -> None:
39
+ """Initialize the application theme."""
40
+
41
+ theme_dir = files(
42
+ f"{IMPORT_NAME}.data.themes.forest"
43
+ )
44
+
45
+ self.root.tk.call(
46
+ "source",
47
+ str(theme_dir / "forest-light.tcl"),
48
+ )
49
+ self.root.tk.call(
50
+ "source",
51
+ str(theme_dir / "forest-dark.tcl"),
52
+ )
53
+
54
+ style = ttk.Style(self.root)
55
+ style.configure(".", padding=2)
56
+ style.configure("TFrame", padding=2)
57
+ style.configure("TLabelFrame", padding=(4, 2))
58
+ style.configure("TButton", padding=4)
59
+ style.configure("TCheckbutton", padding=2)
60
+ style.configure("TRadiobutton", padding=2)
61
+
62
+ style.theme_use("forest-dark")
63
+
64
+ def _configure_window(self) -> None:
65
+ self.root.title(f"{APP_NAME} v{__version__}")
66
+ self.root.geometry(
67
+ f"{APP_WIDTH}x{APP_HEIGHT}"
68
+ )
69
+ self.root.minsize(600, 400)
70
+
71
+ self._set_icon()
72
+
73
+ def _set_icon(self) -> None:
74
+ """Set the application icon when available."""
75
+ try:
76
+ icon_path = files(
77
+ f"{IMPORT_NAME}.data.icons"
78
+ ) / "water-green_256x256.png"
79
+
80
+ if icon_path.is_file():
81
+ self.icon_img = tk.PhotoImage(
82
+ file=str(icon_path)
83
+ )
84
+ self.root.iconphoto(
85
+ True,
86
+ self.icon_img,
87
+ )
88
+ except Exception:
89
+ logger.debug(
90
+ "Unable to load application icon",
91
+ exc_info=True,
92
+ )
93
+
94
+ def _create_menubar(self) -> None:
95
+ """Create the application menu bar."""
96
+
97
+ menubar = tk.Menu(self.root)
98
+ self.root.configure(menu=menubar)
99
+
100
+ options = tk.Menu(
101
+ menubar,
102
+ tearoff=False,
103
+ )
104
+
105
+ menubar.add_cascade(
106
+ label="Options",
107
+ menu=options,
108
+ )
109
+
110
+ options.add_command(
111
+ label="About",
112
+ command=self._show_about,
113
+ )
114
+
115
+ tools_menu = tk.Menu(menubar, tearoff=0)
116
+ menubar.add_cascade(label="Tools", menu=tools_menu)
117
+
118
+ tools_menu.add_command(label="Show Filled Files ", command=lambda: self._show_target_files_in_system_explorer())
119
+ tools_menu.add_command(label="Launch Configured Website ", command=lambda: self._launch_configured_website())
120
+
121
+
122
+ options.add_separator()
123
+
124
+ options.add_command(
125
+ label="Exit",
126
+ command=self.root.destroy,
127
+ )
128
+
129
+ def _create_widgets(self) -> None:
130
+ """Create application widgets."""
131
+
132
+ frame = ttk.Frame(
133
+ self.root,
134
+ padding=12,
135
+ )
136
+ frame.pack(
137
+ fill="both",
138
+ expand=True,
139
+ )
140
+
141
+ label = ttk.Label(
142
+ frame,
143
+ text=f"{APP_NAME} v{__version__}",
144
+ )
145
+ label.pack()
146
+
147
+ def _show_about(self) -> None:
148
+ messagebox.showinfo(
149
+ "About",
150
+ f"{APP_NAME} Version {__version__}",
151
+ )
152
+
153
+ def _show_target_files_in_system_explorer(self) -> None:
154
+ """
155
+ Opens the system file explorer to the directory containing
156
+ the exported files, with GUI error handling.
157
+ """
158
+ try:
159
+ target_dir = APP_DIR
160
+ pyhabitat.show_system_explorer(path = target_dir)
161
+ except Exception as e:
162
+ # The GUI catches the error to show a user-friendly popup
163
+ messagebox.showerror("Error", f"Could not open system explorer: {e}")
164
+
165
+ def _launch_configured_website(self):
166
+ launch_configured_website(path = CONFIG_PATH)
167
+
168
+
169
+ def apply_windows_taskbar_icon() -> None:
170
+ """Set a stable Windows AppUserModelID."""
171
+
172
+ if not pyhabitat.on_windows():
173
+ return
174
+
175
+ try:
176
+ app_id = (
177
+ f"CityOfMemphisWastewater."
178
+ f"{APP_NAME}.Application"
179
+ )
180
+
181
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
182
+ app_id
183
+ )
184
+ except Exception:
185
+ logger.debug(
186
+ "Unable to set Windows taskbar identity",
187
+ exc_info=True,
188
+ )
189
+
190
+
191
+ def start_gui(time_auto_close=None) -> None:
192
+ """Start the graphical application."""
193
+
194
+ apply_windows_taskbar_icon()
195
+
196
+ root = tk.Tk()
197
+ root.withdraw()
198
+
199
+ logger.debug("Starting %s", APP_NAME)
200
+
201
+ try:
202
+ app = GuiApp(root)
203
+ except Exception:
204
+ logger.exception("GUI startup failed")
205
+ root.destroy()
206
+ return
207
+
208
+ root.deiconify()
209
+ root.mainloop()
210
+
211
+ logger.debug("%s: GUI closed", APP_NAME)
212
+
213
+
214
+ if __name__ == "__main__":
215
+ start_gui()
@@ -0,0 +1,3 @@
1
+ # src/blindwindow/helpers.py
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,177 @@
1
+ # src/blindwindow/logging_setup.py
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sys
7
+ import traceback
8
+ from logging.handlers import RotatingFileHandler
9
+ from pathlib import Path
10
+
11
+ from .context import APP_NAME, LOG_FILE_PATH
12
+
13
+
14
+ # -----
15
+ # Constants
16
+ # -----
17
+
18
+ FILE_LOG_LEVEL = logging.DEBUG
19
+ FILE_LOG_MAX_BYTES = 5 * 1024 * 1024
20
+ FILE_LOG_BACKUP_COUNT = 3
21
+
22
+
23
+ # -----
24
+ # Logger Access
25
+ # -----
26
+
27
+ def get_logger(name: str | None = None) -> logging.Logger:
28
+ """Return the package logger or a sub-logger under its namespace."""
29
+ target_name = name or "blindwindow"
30
+ return logging.getLogger(target_name)
31
+
32
+
33
+ # -----
34
+ # Formatters
35
+ # -----
36
+
37
+ def _file_formatter() -> logging.Formatter:
38
+ """Return the formatter used for persistent file logging."""
39
+ return logging.Formatter(
40
+ "%(asctime)s %(levelname)-8s %(name)s: %(message)s",
41
+ datefmt="%Y-%m-%d %H:%M:%S",
42
+ )
43
+
44
+
45
+ def _console_formatter(
46
+ debug: bool = False,
47
+ verbose: bool = False,
48
+ ) -> logging.Formatter:
49
+ """Return the formatter used for console output."""
50
+ if debug:
51
+ fmt = "%(levelname)-7s %(message)s"
52
+ elif verbose:
53
+ fmt = "%(message)s"
54
+ else:
55
+ fmt = "%(levelname)s: %(message)s"
56
+
57
+ return logging.Formatter(fmt)
58
+
59
+
60
+ # -----
61
+ # Handlers
62
+ # -----
63
+
64
+ def setup_file_logging() -> logging.Handler | None:
65
+ """Create the persistent application log handler safely."""
66
+ if LOG_FILE_PATH is None:
67
+ return None
68
+
69
+ try:
70
+ log_path = Path(LOG_FILE_PATH)
71
+ log_path.parent.mkdir(parents=True, exist_ok=True)
72
+
73
+ handler = RotatingFileHandler(
74
+ log_path,
75
+ maxBytes=FILE_LOG_MAX_BYTES,
76
+ backupCount=FILE_LOG_BACKUP_COUNT,
77
+ encoding="utf-8",
78
+ )
79
+ handler.setLevel(FILE_LOG_LEVEL)
80
+ handler.setFormatter(_file_formatter())
81
+ return handler
82
+ except (OSError, PermissionError) as err:
83
+ sys.stderr.write(f"Warning: Failed to initialize file log at {LOG_FILE_PATH}: {err}\n")
84
+ return None
85
+
86
+
87
+ # -----
88
+ # Configurations
89
+ # -----
90
+
91
+ def configure_logging_for_application(
92
+ debug: bool = False,
93
+ verbose: bool = False,
94
+ log_to_file: bool = True,
95
+ ) -> logging.Logger:
96
+ """Configure logging for an executable application entrypoint."""
97
+ logger = get_logger()
98
+
99
+ logger.setLevel(logging.DEBUG)
100
+ logger.propagate = False
101
+
102
+ if logger.hasHandlers():
103
+ logger.handlers.clear()
104
+
105
+ # File Logging
106
+ if log_to_file:
107
+ file_handler = setup_file_logging()
108
+ if file_handler is not None:
109
+ logger.addHandler(file_handler)
110
+
111
+ # Console Logging
112
+ if debug:
113
+ console_level = logging.DEBUG
114
+ elif verbose:
115
+ console_level = logging.INFO
116
+ else:
117
+ console_level = logging.WARNING
118
+
119
+ console_handler = logging.StreamHandler(sys.stderr)
120
+ console_handler.setLevel(console_level)
121
+ console_handler.setFormatter(
122
+ _console_formatter(debug=debug, verbose=verbose)
123
+ )
124
+ logger.addHandler(console_handler)
125
+
126
+ logger.debug("Application logging configured for %s.", APP_NAME)
127
+ return logger
128
+
129
+
130
+ def configure_logging_for_library(
131
+ debug: bool = False,
132
+ verbose: bool = False,
133
+ ) -> logging.Logger:
134
+ """Configure logging when this package is consumed as a library module."""
135
+ logger = get_logger()
136
+
137
+ if debug:
138
+ level = logging.DEBUG
139
+ elif verbose:
140
+ level = logging.INFO
141
+ else:
142
+ level = logging.WARNING
143
+
144
+ logger.setLevel(level)
145
+ logger.propagate = True
146
+
147
+ if not logger.handlers:
148
+ logger.addHandler(logging.NullHandler())
149
+
150
+ return logger
151
+
152
+
153
+ def configure_logging_all_debug() -> None:
154
+ """Force DEBUG level logging globally across the root logger and third-party tools."""
155
+ root_logger = logging.getLogger()
156
+ root_logger.setLevel(logging.DEBUG)
157
+
158
+ if root_logger.hasHandlers():
159
+ root_logger.handlers.clear()
160
+
161
+ console_handler = logging.StreamHandler(sys.stderr)
162
+ console_handler.setLevel(logging.DEBUG)
163
+ console_handler.setFormatter(
164
+ logging.Formatter("%(asctime)s - %(levelname)-7s - [%(name)s] %(message)s")
165
+ )
166
+ root_logger.addHandler(console_handler)
167
+
168
+
169
+ # -----
170
+ # Diagnostics
171
+ # -----
172
+
173
+ def log_traceback(logger_instance: logging.Logger | None = None) -> None:
174
+ """Safely print stack traces to stderr if debug level is active."""
175
+ target = logger_instance or get_logger()
176
+ if target.isEnabledFor(logging.DEBUG):
177
+ traceback.print_exc(file=sys.stderr)
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: blindwindow
3
+ Version: 0.1.0
4
+ Summary: Route console stderr and stdout to blindwindow, to either duplicate or display what is otherwise hidden. Namely for MSIX deployed CLI's
5
+ Author-email: George Clayton Bennett <george.bennett@memphistn.gov>
6
+ Maintainer-email: George Clayton Bennett <george.bennett@memphistn.gov>
7
+ License-Expression: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Typing :: Typed
18
+ Classifier: Development Status :: 3 - Alpha
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: dworshak-config>=0.2.8
22
+ Requires-Dist: pyhabitat>=1.3.9
23
+ Requires-Dist: typer>=0.27.0
24
+ Requires-Dist: typer-helptree>=0.2.12
25
+ Requires-Dist: maxson-gui-utils>=0.1.5
26
+ Provides-Extra: gui
27
+ Requires-Dist: maxson_gui_utils>=0.1.4; extra == "gui"
28
+
29
+ # Blindwindow
30
+
31
+ Render console stderr and stdout to a TKinter based REPL widget masquerading as a log pange.
32
+
33
+ Use Case: When calling a CLI packaged inside of a MSIX that has been delivered via the Microsoft Store. This use case typically does not allow prints.
34
+ Example: `pdflinkcheck.exe serve --help`
35
+
36
+
37
+
38
+ ## Helptree
39
+
40
+ See the `blindwindow` Typer CLI structure.
41
+
42
+ ```bash
43
+ blindwindow helptree
44
+ ```
@@ -0,0 +1,20 @@
1
+ MANIFEST.in
2
+ README.md
3
+ pyproject.toml
4
+ src/blindwindow/VERSION
5
+ src/blindwindow/__init__.py
6
+ src/blindwindow/__main__.py
7
+ src/blindwindow/_version.py
8
+ src/blindwindow/cli.py
9
+ src/blindwindow/config.py
10
+ src/blindwindow/context.py
11
+ src/blindwindow/core.py
12
+ src/blindwindow/gui.py
13
+ src/blindwindow/helpers.py
14
+ src/blindwindow/logging_setup.py
15
+ src/blindwindow.egg-info/PKG-INFO
16
+ src/blindwindow.egg-info/SOURCES.txt
17
+ src/blindwindow.egg-info/dependency_links.txt
18
+ src/blindwindow.egg-info/entry_points.txt
19
+ src/blindwindow.egg-info/requires.txt
20
+ src/blindwindow.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ blindwindow = blindwindow.__main__:app
@@ -0,0 +1,8 @@
1
+ dworshak-config>=0.2.8
2
+ pyhabitat>=1.3.9
3
+ typer>=0.27.0
4
+ typer-helptree>=0.2.12
5
+ maxson-gui-utils>=0.1.5
6
+
7
+ [gui]
8
+ maxson_gui_utils>=0.1.4
@@ -0,0 +1 @@
1
+ blindwindow