zebra-day 0.0.37__py3-none-any.whl → 2.0.0__py3-none-any.whl
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.
- zebra_day/__init__.py +35 -0
- zebra_day/bin/__init__.py +0 -0
- zebra_day/cli/__init__.py +240 -0
- zebra_day/cli/cognito.py +121 -0
- zebra_day/cli/gui.py +338 -0
- zebra_day/cli/printer.py +168 -0
- zebra_day/cli/template.py +176 -0
- zebra_day/cmd_mgr.py +35 -0
- zebra_day/etc/Monoid-Regular-HalfTight-Dollar-0-1-l.ttf +0 -0
- zebra_day/etc/label_styles/blank.zpl +0 -0
- zebra_day/etc/label_styles/cornersStripOf4Squares_1inX1in.zpl +55 -0
- zebra_day/etc/label_styles/corners_1inX2in.zpl +28 -0
- zebra_day/etc/label_styles/corners_20cmX30cm.zpl +6 -0
- zebra_day/etc/label_styles/corners_smallTube.zpl +7 -0
- zebra_day/etc/label_styles/corners_unspecifiedDimensions.zpl +15 -0
- zebra_day/etc/label_styles/generic_2inX1in.zpl +21 -0
- zebra_day/etc/label_styles/plate_1inX0.25in.zpl +9 -0
- zebra_day/etc/label_styles/plate_1inX0.25inHD.zpl +9 -0
- zebra_day/etc/label_styles/smallTubeWdotHD_prod.zpl +8 -0
- zebra_day/etc/label_styles/smallTubeWdot_corners.zpl +7 -0
- zebra_day/etc/label_styles/smallTubeWdot_prod.zpl +8 -0
- zebra_day/etc/label_styles/smallTubeWdot_prodAlt1.zpl +6 -0
- zebra_day/etc/label_styles/smallTubeWdot_prodAlt1b.zpl +3 -0
- zebra_day/etc/label_styles/smallTubeWdot_prodV2.zpl +8 -0
- zebra_day/etc/label_styles/smallTubeWdot_reagent.zpl +29 -0
- zebra_day/etc/label_styles/stripOf4Squares_1inX1in.zpl +32 -0
- zebra_day/etc/label_styles/test_800dX800dCoordinateArray.zpl +1 -0
- zebra_day/etc/label_styles/tmps/.hold +0 -0
- zebra_day/etc/label_styles/tmps/tmp_zpl_templates.here +0 -0
- zebra_day/etc/label_styles/tube_20mmX30mmA.zpl +7 -0
- zebra_day/etc/label_styles/tube_2inX0.3in.zpl +15 -0
- zebra_day/etc/label_styles/tube_2inX0.5in.zpl +15 -0
- zebra_day/etc/label_styles/tube_2inX0.5inHD.zpl +15 -0
- zebra_day/etc/label_styles/tube_2inX1in.zpl +25 -0
- zebra_day/etc/label_styles/tube_2inX1inHD.zpl +22 -0
- zebra_day/etc/label_styles/tube_2inX1inHDv3.zpl +21 -0
- zebra_day/etc/old_printer_config/.hold +0 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:50:25.022846_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:50:25.033657_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:50:25.039597_printer_config.json +3 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:50:25.047295_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:50:25.055804_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:50:25.061337_printer_config.json +3 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:24.073326_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:24.081950_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:24.088251_printer_config.json +3 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:24.096501_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:24.104767_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:24.110364_printer_config.json +3 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:24.118239_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:24.125950_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:24.349866_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:24.361085_printer_config.json +3 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:24.558323_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:24.565756_printer_config.json +3 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:29.739070_printer_config.json +16 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:29.753796_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:29.760201_printer_config.json +3 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:29.768747_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:29.775312_printer_config.json +3 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:29.782533_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:29.789287_printer_config.json +1 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:29.794230_printer_config.json +3 -0
- zebra_day/etc/old_printer_config/2026-02-01_01:51:29.800021_printer_config.json +5 -0
- zebra_day/etc/printer_config.json +4 -0
- zebra_day/etc/printer_config.template.json +24 -0
- zebra_day/etc/tmp_printers0.json +5 -0
- zebra_day/etc/tmp_printers120.json +10 -0
- zebra_day/etc/tmp_printers145.json +10 -0
- zebra_day/etc/tmp_printers207.json +10 -0
- zebra_day/etc/tmp_printers374.json +5 -0
- zebra_day/etc/tmp_printers383.json +5 -0
- zebra_day/etc/tmp_printers450.json +5 -0
- zebra_day/etc/tmp_printers469.json +10 -0
- zebra_day/etc/tmp_printers485.json +10 -0
- zebra_day/etc/tmp_printers504.json +5 -0
- zebra_day/etc/tmp_printers531.json +10 -0
- zebra_day/etc/tmp_printers540.json +10 -0
- zebra_day/etc/tmp_printers542.json +10 -0
- zebra_day/etc/tmp_printers552.json +10 -0
- zebra_day/etc/tmp_printers608.json +5 -0
- zebra_day/etc/tmp_printers657.json +5 -0
- zebra_day/etc/tmp_printers715.json +10 -0
- zebra_day/etc/tmp_printers838.json +5 -0
- zebra_day/etc/tmp_printers839.json +5 -0
- zebra_day/etc/tmp_printers933.json +5 -0
- zebra_day/etc/tmp_printers957.json +5 -0
- zebra_day/etc/tmp_printers972.json +10 -0
- zebra_day/exceptions.py +88 -0
- zebra_day/files/.hold +0 -0
- zebra_day/files/blank_preview.png +0 -0
- zebra_day/files/corners_20cmX30cm_preview.png +0 -0
- zebra_day/files/generic_2inX1in_preview.png +0 -0
- zebra_day/files/hold +0 -0
- zebra_day/files/test_png_12020.png +0 -0
- zebra_day/files/test_png_12352.png +0 -0
- zebra_day/files/test_png_15472.png +0 -0
- zebra_day/files/test_png_17696.png +0 -0
- zebra_day/files/test_png_23477.png +0 -0
- zebra_day/files/test_png_24493.png +0 -0
- zebra_day/files/test_png_28157.png +0 -0
- zebra_day/files/test_png_30069.png +0 -0
- zebra_day/files/test_png_35832.png +0 -0
- zebra_day/files/test_png_36400.png +0 -0
- zebra_day/files/test_png_40816.png +0 -0
- zebra_day/files/test_png_47791.png +0 -0
- zebra_day/files/test_png_47799.png +0 -0
- zebra_day/files/test_png_49564.png +0 -0
- zebra_day/files/test_png_53848.png +0 -0
- zebra_day/files/test_png_55588.png +0 -0
- zebra_day/files/test_png_58809.png +0 -0
- zebra_day/files/test_png_62542.png +0 -0
- zebra_day/files/test_png_67242.png +0 -0
- zebra_day/files/test_png_89893.png +0 -0
- zebra_day/files/test_png_91597.png +0 -0
- zebra_day/files/test_png_93633.png +0 -0
- zebra_day/files/tmpbjo3k7q1.png +0 -0
- zebra_day/files/tmpigtr4pwy.png +0 -0
- zebra_day/files/tube_20mmX30mmA_preview.png +0 -0
- zebra_day/files/zpl_label_tube_2inX1in_2026-02-01_01:51:24.370964.png +0 -0
- zebra_day/logging_config.py +74 -0
- zebra_day/logs/.hold +0 -0
- zebra_day/logs/print_requests.log +2 -0
- zebra_day/paths.py +143 -0
- zebra_day/print_mgr.py +557 -117
- zebra_day/static/datschund.css +140 -0
- zebra_day/static/datschund.png +0 -0
- zebra_day/static/daylily.png +0 -0
- zebra_day/static/favicon.svg +20 -0
- zebra_day/static/general.css +99 -0
- zebra_day/static/js/zebra_modern.js +172 -0
- zebra_day/static/lsmc.css +354 -0
- zebra_day/static/moon.jpeg +0 -0
- zebra_day/static/oakland.css +197 -0
- zebra_day/static/petrichor.css +150 -0
- zebra_day/static/popday_daylily.css +140 -0
- zebra_day/static/style.css +183 -0
- zebra_day/static/triangles.css +122 -0
- zebra_day/static/tron.css +277 -0
- zebra_day/static/zebra_modern.css +771 -0
- zebra_day/static/zebras.css +176 -0
- zebra_day/templates/modern/base.html +98 -0
- zebra_day/templates/modern/config.html +141 -0
- zebra_day/templates/modern/config_backups.html +59 -0
- zebra_day/templates/modern/config_editor.html +95 -0
- zebra_day/templates/modern/config_new.html +93 -0
- zebra_day/templates/modern/dashboard.html +160 -0
- zebra_day/templates/modern/print_request.html +145 -0
- zebra_day/templates/modern/print_result.html +88 -0
- zebra_day/templates/modern/printer_detail.html +244 -0
- zebra_day/templates/modern/printers.html +144 -0
- zebra_day/templates/modern/save_result.html +46 -0
- zebra_day/templates/modern/template_editor.html +175 -0
- zebra_day/templates/modern/templates.html +122 -0
- zebra_day/web/__init__.py +9 -0
- zebra_day/web/app.py +248 -0
- zebra_day/web/auth.py +172 -0
- zebra_day/web/middleware.py +159 -0
- zebra_day/web/routers/__init__.py +2 -0
- zebra_day/web/routers/api.py +313 -0
- zebra_day/web/routers/ui.py +636 -0
- zebra_day/zpl_renderer.py +273 -0
- zebra_day-2.0.0.dist-info/METADATA +847 -0
- zebra_day-2.0.0.dist-info/RECORD +168 -0
- {zebra_day-0.0.37.dist-info → zebra_day-2.0.0.dist-info}/WHEEL +1 -1
- zebra_day-2.0.0.dist-info/entry_points.txt +4 -0
- zebra_day/bin/scan_for_networed_zebra_printers.py +0 -23
- zebra_day/bin/te.py +0 -905
- zebra_day/bin/zserve.py +0 -620
- zebra_day-0.0.37.dist-info/METADATA +0 -1177
- zebra_day-0.0.37.dist-info/RECORD +0 -10
- {zebra_day-0.0.37.dist-info → zebra_day-2.0.0.dist-info/licenses}/LICENSE +0 -0
- {zebra_day-0.0.37.dist-info → zebra_day-2.0.0.dist-info}/top_level.txt +0 -0
zebra_day/cli/gui.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""GUI server management commands for zebra_day CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import signal
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Literal
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from zebra_day import paths as xdg
|
|
16
|
+
|
|
17
|
+
gui_app = typer.Typer(help="Web UI server management commands")
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
# PID and log file locations
|
|
21
|
+
STATE_DIR = xdg.get_state_dir()
|
|
22
|
+
LOG_DIR = xdg.get_logs_dir()
|
|
23
|
+
CONFIG_DIR = xdg.get_config_dir()
|
|
24
|
+
PID_FILE = STATE_DIR / "gui.pid"
|
|
25
|
+
DEFAULT_CERT_DIR = CONFIG_DIR / "certs"
|
|
26
|
+
DEFAULT_CERT_FILE = DEFAULT_CERT_DIR / "server.crt"
|
|
27
|
+
DEFAULT_KEY_FILE = DEFAULT_CERT_DIR / "server.key"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ensure_dirs():
|
|
31
|
+
"""Ensure state and log directories exist."""
|
|
32
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _get_log_file() -> Path:
|
|
37
|
+
"""Get timestamped log file path."""
|
|
38
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
39
|
+
return LOG_DIR / f"gui_{ts}.log"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_latest_log() -> Optional[Path]:
|
|
43
|
+
"""Get the most recent log file."""
|
|
44
|
+
logs = sorted(LOG_DIR.glob("gui_*.log"), reverse=True)
|
|
45
|
+
return logs[0] if logs else None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _get_pid() -> Optional[int]:
|
|
49
|
+
"""Get the running server PID if exists."""
|
|
50
|
+
if PID_FILE.exists():
|
|
51
|
+
try:
|
|
52
|
+
pid = int(PID_FILE.read_text().strip())
|
|
53
|
+
os.kill(pid, 0)
|
|
54
|
+
return pid
|
|
55
|
+
except (ValueError, ProcessLookupError, PermissionError):
|
|
56
|
+
PID_FILE.unlink(missing_ok=True)
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _check_auth_dependencies() -> bool:
|
|
61
|
+
"""Check if auth dependencies are available."""
|
|
62
|
+
try:
|
|
63
|
+
import jose # noqa: F401
|
|
64
|
+
return True
|
|
65
|
+
except ImportError:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _resolve_ssl_paths(
|
|
70
|
+
cert: Optional[str], key: Optional[str]
|
|
71
|
+
) -> tuple[Optional[str], Optional[str], bool]:
|
|
72
|
+
"""
|
|
73
|
+
Resolve SSL certificate and key paths.
|
|
74
|
+
|
|
75
|
+
Priority:
|
|
76
|
+
1. Explicit --cert/--key arguments
|
|
77
|
+
2. SSL_CERT_PATH/SSL_KEY_PATH environment variables
|
|
78
|
+
3. Default paths in ~/.config/zebra_day/certs/
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Tuple of (cert_path, key_path, use_https)
|
|
82
|
+
"""
|
|
83
|
+
cert_path = cert
|
|
84
|
+
key_path = key
|
|
85
|
+
|
|
86
|
+
# Check environment variables
|
|
87
|
+
if not cert_path:
|
|
88
|
+
cert_path = os.environ.get("SSL_CERT_PATH")
|
|
89
|
+
if not key_path:
|
|
90
|
+
key_path = os.environ.get("SSL_KEY_PATH")
|
|
91
|
+
|
|
92
|
+
# Check default paths
|
|
93
|
+
if not cert_path and DEFAULT_CERT_FILE.exists():
|
|
94
|
+
cert_path = str(DEFAULT_CERT_FILE)
|
|
95
|
+
if not key_path and DEFAULT_KEY_FILE.exists():
|
|
96
|
+
key_path = str(DEFAULT_KEY_FILE)
|
|
97
|
+
|
|
98
|
+
# Validate both exist
|
|
99
|
+
if cert_path and key_path:
|
|
100
|
+
if Path(cert_path).exists() and Path(key_path).exists():
|
|
101
|
+
return cert_path, key_path, True
|
|
102
|
+
|
|
103
|
+
return None, None, False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@gui_app.command("start")
|
|
107
|
+
def start(
|
|
108
|
+
port: int = typer.Option(8118, "--port", "-p", help="Port to run the server on"),
|
|
109
|
+
host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind to"),
|
|
110
|
+
auth: str = typer.Option("none", "--auth", "-a", help="Authentication mode: none or cognito"),
|
|
111
|
+
reload: bool = typer.Option(False, "--reload", "-r", help="Enable auto-reload (foreground)"),
|
|
112
|
+
background: bool = typer.Option(True, "--background/--foreground", "-b/-f", help="Run in background"),
|
|
113
|
+
cert: Optional[str] = typer.Option(None, "--cert", help="Path to SSL certificate file"),
|
|
114
|
+
key: Optional[str] = typer.Option(None, "--key", help="Path to SSL private key file"),
|
|
115
|
+
no_https: bool = typer.Option(False, "--no-https", help="Disable HTTPS even if certificates are available"),
|
|
116
|
+
):
|
|
117
|
+
"""Start the zebra_day web UI server.
|
|
118
|
+
|
|
119
|
+
By default, HTTPS is enabled if certificates are found in:
|
|
120
|
+
- Explicit --cert/--key arguments
|
|
121
|
+
- SSL_CERT_PATH/SSL_KEY_PATH environment variables
|
|
122
|
+
- ~/.config/zebra_day/certs/server.crt and server.key
|
|
123
|
+
|
|
124
|
+
Use --no-https to force HTTP mode.
|
|
125
|
+
"""
|
|
126
|
+
_ensure_dirs()
|
|
127
|
+
|
|
128
|
+
# Validate auth option
|
|
129
|
+
if auth not in ("none", "cognito"):
|
|
130
|
+
console.print(f"[red]✗[/red] Invalid auth mode: {auth}. Use 'none' or 'cognito'.")
|
|
131
|
+
raise typer.Exit(1)
|
|
132
|
+
|
|
133
|
+
# Check if already running
|
|
134
|
+
pid = _get_pid()
|
|
135
|
+
if pid:
|
|
136
|
+
console.print(f"[yellow]⚠[/yellow] Server already running (PID {pid})")
|
|
137
|
+
console.print(f" URL: [cyan]http://{host}:{port}[/cyan]")
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
# Check auth dependencies if cognito mode
|
|
141
|
+
if auth == "cognito":
|
|
142
|
+
if not _check_auth_dependencies():
|
|
143
|
+
console.print("[red]✗[/red] Authentication requested but python-jose is not installed")
|
|
144
|
+
console.print(" Install with: [cyan]pip install -e \".[auth]\"[/cyan]")
|
|
145
|
+
raise typer.Exit(1)
|
|
146
|
+
|
|
147
|
+
# Check required env vars
|
|
148
|
+
missing = []
|
|
149
|
+
if not os.environ.get("COGNITO_USER_POOL_ID"):
|
|
150
|
+
missing.append("COGNITO_USER_POOL_ID")
|
|
151
|
+
if not os.environ.get("COGNITO_APP_CLIENT_ID"):
|
|
152
|
+
missing.append("COGNITO_APP_CLIENT_ID")
|
|
153
|
+
if missing:
|
|
154
|
+
console.print("[red]✗[/red] Cognito auth enabled but environment variables missing:")
|
|
155
|
+
for var in missing:
|
|
156
|
+
console.print(f" • {var}")
|
|
157
|
+
raise typer.Exit(1)
|
|
158
|
+
console.print("[green]✓[/green] Cognito authentication enabled")
|
|
159
|
+
|
|
160
|
+
# Resolve SSL paths
|
|
161
|
+
cert_path, key_path, use_https = _resolve_ssl_paths(cert, key)
|
|
162
|
+
|
|
163
|
+
if no_https:
|
|
164
|
+
use_https = False
|
|
165
|
+
cert_path = None
|
|
166
|
+
key_path = None
|
|
167
|
+
|
|
168
|
+
protocol = "https" if use_https else "http"
|
|
169
|
+
|
|
170
|
+
if use_https:
|
|
171
|
+
console.print(f"[green]✓[/green] HTTPS enabled")
|
|
172
|
+
console.print(f" Certificate: [dim]{cert_path}[/dim]")
|
|
173
|
+
console.print(f" Private key: [dim]{key_path}[/dim]")
|
|
174
|
+
else:
|
|
175
|
+
console.print("[yellow]⚠[/yellow] Running in HTTP mode (insecure)")
|
|
176
|
+
console.print(" For HTTPS, generate certificates with mkcert:")
|
|
177
|
+
console.print(f" [dim]mkdir -p {DEFAULT_CERT_DIR}[/dim]")
|
|
178
|
+
console.print(f" [dim]mkcert -cert-file {DEFAULT_CERT_FILE} -key-file {DEFAULT_KEY_FILE} localhost 127.0.0.1 ::1[/dim]")
|
|
179
|
+
|
|
180
|
+
# Build command with SSL parameters
|
|
181
|
+
ssl_args = ""
|
|
182
|
+
if use_https and cert_path and key_path:
|
|
183
|
+
ssl_args = f", ssl_certfile='{cert_path}', ssl_keyfile='{key_path}'"
|
|
184
|
+
|
|
185
|
+
cmd = [
|
|
186
|
+
sys.executable,
|
|
187
|
+
"-c",
|
|
188
|
+
f"from zebra_day.web.app import run_server; run_server(host='{host}', port={port}, reload={reload}, auth='{auth}'{ssl_args})",
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
# Set up environment
|
|
192
|
+
env = os.environ.copy()
|
|
193
|
+
env["PYTHONUNBUFFERED"] = "1"
|
|
194
|
+
env["ZEBRA_DAY_AUTH_MODE"] = auth
|
|
195
|
+
if cert_path:
|
|
196
|
+
env["SSL_CERT_PATH"] = cert_path
|
|
197
|
+
if key_path:
|
|
198
|
+
env["SSL_KEY_PATH"] = key_path
|
|
199
|
+
|
|
200
|
+
if reload:
|
|
201
|
+
background = False
|
|
202
|
+
console.print("[dim]Auto-reload enabled (foreground mode)[/dim]")
|
|
203
|
+
|
|
204
|
+
if background:
|
|
205
|
+
log_file = _get_log_file()
|
|
206
|
+
log_f = open(log_file, "w", buffering=1)
|
|
207
|
+
|
|
208
|
+
proc = subprocess.Popen(
|
|
209
|
+
cmd,
|
|
210
|
+
stdout=log_f,
|
|
211
|
+
stderr=subprocess.STDOUT,
|
|
212
|
+
start_new_session=True,
|
|
213
|
+
cwd=Path.cwd(),
|
|
214
|
+
env=env,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
time.sleep(2)
|
|
218
|
+
if proc.poll() is not None:
|
|
219
|
+
log_f.close()
|
|
220
|
+
console.print("[red]✗[/red] Server failed to start. Check logs:")
|
|
221
|
+
console.print(f" [dim]{log_file}[/dim]")
|
|
222
|
+
if log_file.exists():
|
|
223
|
+
content = log_file.read_text().strip()
|
|
224
|
+
if content:
|
|
225
|
+
console.print("\n[dim]--- Last error ---[/dim]")
|
|
226
|
+
for line in content.split("\n")[-10:]:
|
|
227
|
+
console.print(f" {line}")
|
|
228
|
+
raise typer.Exit(1)
|
|
229
|
+
|
|
230
|
+
PID_FILE.write_text(str(proc.pid))
|
|
231
|
+
console.print(f"[green]✓[/green] Server started (PID {proc.pid})")
|
|
232
|
+
console.print(f" URL: [cyan]{protocol}://{host}:{port}[/cyan]")
|
|
233
|
+
console.print(f" Logs: [dim]{log_file}[/dim]")
|
|
234
|
+
else:
|
|
235
|
+
console.print(f"[green]✓[/green] Starting server on [cyan]{protocol}://{host}:{port}[/cyan]")
|
|
236
|
+
console.print(" Press Ctrl+C to stop\n")
|
|
237
|
+
try:
|
|
238
|
+
result = subprocess.run(cmd, cwd=Path.cwd(), env=env)
|
|
239
|
+
if result.returncode != 0:
|
|
240
|
+
raise typer.Exit(result.returncode)
|
|
241
|
+
except KeyboardInterrupt:
|
|
242
|
+
console.print("\n[yellow]⚠[/yellow] Server stopped")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@gui_app.command("stop")
|
|
246
|
+
def stop():
|
|
247
|
+
"""Stop the zebra_day web UI server."""
|
|
248
|
+
pid = _get_pid()
|
|
249
|
+
if not pid:
|
|
250
|
+
console.print("[yellow]⚠[/yellow] No server running")
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
os.kill(pid, signal.SIGTERM)
|
|
255
|
+
for _ in range(10):
|
|
256
|
+
time.sleep(0.5)
|
|
257
|
+
try:
|
|
258
|
+
os.kill(pid, 0)
|
|
259
|
+
except ProcessLookupError:
|
|
260
|
+
break
|
|
261
|
+
else:
|
|
262
|
+
os.kill(pid, signal.SIGKILL)
|
|
263
|
+
|
|
264
|
+
PID_FILE.unlink(missing_ok=True)
|
|
265
|
+
console.print(f"[green]✓[/green] Server stopped (was PID {pid})")
|
|
266
|
+
except ProcessLookupError:
|
|
267
|
+
PID_FILE.unlink(missing_ok=True)
|
|
268
|
+
console.print("[yellow]⚠[/yellow] Server was not running")
|
|
269
|
+
except PermissionError:
|
|
270
|
+
console.print(f"[red]✗[/red] Permission denied stopping PID {pid}")
|
|
271
|
+
raise typer.Exit(1)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@gui_app.command("status")
|
|
275
|
+
def status():
|
|
276
|
+
"""Check the status of the zebra_day web UI server."""
|
|
277
|
+
pid = _get_pid()
|
|
278
|
+
if pid:
|
|
279
|
+
log_file = _get_latest_log()
|
|
280
|
+
# Check if HTTPS is likely enabled based on cert availability
|
|
281
|
+
_, _, use_https = _resolve_ssl_paths(None, None)
|
|
282
|
+
protocol = "https" if use_https else "http"
|
|
283
|
+
console.print(f"[green]●[/green] Server is [green]running[/green] (PID {pid})")
|
|
284
|
+
console.print(f" URL: [cyan]{protocol}://0.0.0.0:8118[/cyan]")
|
|
285
|
+
if log_file:
|
|
286
|
+
console.print(f" Logs: [dim]{log_file}[/dim]")
|
|
287
|
+
else:
|
|
288
|
+
console.print("[dim]○[/dim] Server is [dim]not running[/dim]")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@gui_app.command("logs")
|
|
292
|
+
def logs(
|
|
293
|
+
lines: int = typer.Option(50, "--tail", "-n", help="Number of lines to show"),
|
|
294
|
+
follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output"),
|
|
295
|
+
all_logs: bool = typer.Option(False, "--all", "-a", help="List all log files"),
|
|
296
|
+
):
|
|
297
|
+
"""View zebra_day web UI server logs."""
|
|
298
|
+
if all_logs:
|
|
299
|
+
log_files = sorted(LOG_DIR.glob("gui_*.log"), reverse=True)
|
|
300
|
+
if not log_files:
|
|
301
|
+
console.print("[yellow]⚠[/yellow] No log files found.")
|
|
302
|
+
return
|
|
303
|
+
console.print(f"[bold]Server log files ({len(log_files)}):[/bold]")
|
|
304
|
+
for lf in log_files[:20]:
|
|
305
|
+
size = lf.stat().st_size
|
|
306
|
+
console.print(f" {lf.name} [dim]({size:,} bytes)[/dim]")
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
log_file = _get_latest_log()
|
|
310
|
+
if not log_file:
|
|
311
|
+
console.print("[yellow]⚠[/yellow] No log file found. Start the server first.")
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
if follow:
|
|
315
|
+
console.print(f"[dim]Following {log_file.name} (Ctrl+C to stop)[/dim]\n")
|
|
316
|
+
try:
|
|
317
|
+
subprocess.run(["tail", "-f", "-n", str(lines), str(log_file)])
|
|
318
|
+
except KeyboardInterrupt:
|
|
319
|
+
console.print("\n")
|
|
320
|
+
else:
|
|
321
|
+
console.print(f"[dim]Showing last {lines} lines of {log_file.name}[/dim]\n")
|
|
322
|
+
try:
|
|
323
|
+
subprocess.run(["tail", "-n", str(lines), str(log_file)])
|
|
324
|
+
except Exception as e:
|
|
325
|
+
console.print(f"[red]✗[/red] Error reading log: {e}")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@gui_app.command("restart")
|
|
329
|
+
def restart(
|
|
330
|
+
port: int = typer.Option(8118, "--port", "-p", help="Port to run the server on"),
|
|
331
|
+
host: str = typer.Option("0.0.0.0", "--host", "-h", help="Host to bind to"),
|
|
332
|
+
auth: str = typer.Option("none", "--auth", "-a", help="Authentication mode: none or cognito"),
|
|
333
|
+
):
|
|
334
|
+
"""Restart the zebra_day web UI server."""
|
|
335
|
+
stop()
|
|
336
|
+
time.sleep(1)
|
|
337
|
+
start(port=port, host=host, auth=auth, reload=False, background=True)
|
|
338
|
+
|
zebra_day/cli/printer.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Printer fleet management commands for zebra_day CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import socket
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from zebra_day import paths as xdg
|
|
12
|
+
|
|
13
|
+
printer_app = typer.Typer(help="Printer fleet management commands")
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_local_ip() -> str:
|
|
18
|
+
"""Get local IP address."""
|
|
19
|
+
try:
|
|
20
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
21
|
+
s.connect(("8.8.8.8", 80))
|
|
22
|
+
ip = s.getsockname()[0]
|
|
23
|
+
s.close()
|
|
24
|
+
return ip
|
|
25
|
+
except Exception:
|
|
26
|
+
return "127.0.0.1"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@printer_app.command("scan")
|
|
30
|
+
def scan(
|
|
31
|
+
ip_stub: Optional[str] = typer.Option(None, "--ip-stub", "-i", help="IP stub to scan (e.g., 192.168.1)"),
|
|
32
|
+
wait: float = typer.Option(0.25, "--wait", "-w", help="Seconds to wait per IP probe"),
|
|
33
|
+
lab: str = typer.Option("scan-results", "--lab", "-l", help="Lab name to assign found printers"),
|
|
34
|
+
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
35
|
+
):
|
|
36
|
+
"""Scan network for Zebra printers."""
|
|
37
|
+
# Determine IP stub if not provided
|
|
38
|
+
if not ip_stub:
|
|
39
|
+
local_ip = _get_local_ip()
|
|
40
|
+
ip_stub = ".".join(local_ip.split(".")[:-1])
|
|
41
|
+
|
|
42
|
+
if not json_output:
|
|
43
|
+
console.print(f"[cyan]→[/cyan] Scanning {ip_stub}.* for Zebra printers...")
|
|
44
|
+
console.print("[dim] This may take a few minutes...[/dim]")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
import zebra_day.print_mgr as zdpm
|
|
48
|
+
zp = zdpm.zpl()
|
|
49
|
+
zp.probe_zebra_printers_add_to_printers_json(
|
|
50
|
+
ip_stub=ip_stub,
|
|
51
|
+
scan_wait=str(wait),
|
|
52
|
+
lab=lab,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
found = []
|
|
56
|
+
if hasattr(zp, "printers") and "labs" in zp.printers and lab in zp.printers["labs"]:
|
|
57
|
+
for name, info in zp.printers["labs"][lab].items():
|
|
58
|
+
if isinstance(info, dict) and info.get("ip_address") not in ["dl_png"]:
|
|
59
|
+
found.append({
|
|
60
|
+
"name": name,
|
|
61
|
+
"ip": info.get("ip_address"),
|
|
62
|
+
"model": info.get("model", "unknown"),
|
|
63
|
+
"serial": info.get("serial", "unknown"),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if json_output:
|
|
67
|
+
console.print(json.dumps(found, indent=2))
|
|
68
|
+
else:
|
|
69
|
+
console.print(f"\n[green]✓[/green] Found {len(found)} printer(s)")
|
|
70
|
+
if found:
|
|
71
|
+
table = Table()
|
|
72
|
+
table.add_column("Name", style="cyan")
|
|
73
|
+
table.add_column("IP Address")
|
|
74
|
+
table.add_column("Model")
|
|
75
|
+
table.add_column("Serial")
|
|
76
|
+
for p in found:
|
|
77
|
+
table.add_row(p["name"], p["ip"], p["model"], p["serial"])
|
|
78
|
+
console.print(table)
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
if json_output:
|
|
82
|
+
console.print(json.dumps({"error": str(e)}))
|
|
83
|
+
else:
|
|
84
|
+
console.print(f"[red]✗[/red] Scan error: {e}")
|
|
85
|
+
raise typer.Exit(1)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@printer_app.command("list")
|
|
89
|
+
def list_printers(
|
|
90
|
+
lab: Optional[str] = typer.Option(None, "--lab", "-l", help="Filter by lab name"),
|
|
91
|
+
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
92
|
+
):
|
|
93
|
+
"""List configured printers."""
|
|
94
|
+
try:
|
|
95
|
+
import zebra_day.print_mgr as zdpm
|
|
96
|
+
zp = zdpm.zpl()
|
|
97
|
+
|
|
98
|
+
printers = []
|
|
99
|
+
if hasattr(zp, "printers") and "labs" in zp.printers:
|
|
100
|
+
for lab_name, lab_printers in zp.printers["labs"].items():
|
|
101
|
+
if lab and lab_name != lab:
|
|
102
|
+
continue
|
|
103
|
+
for name, info in lab_printers.items():
|
|
104
|
+
if isinstance(info, dict):
|
|
105
|
+
printers.append({
|
|
106
|
+
"lab": lab_name,
|
|
107
|
+
"name": name,
|
|
108
|
+
"ip": info.get("ip_address", "unknown"),
|
|
109
|
+
"model": info.get("model", "unknown"),
|
|
110
|
+
"styles": info.get("label_zpl_styles", []),
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
if json_output:
|
|
114
|
+
console.print(json.dumps(printers, indent=2))
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
if not printers:
|
|
118
|
+
console.print("[yellow]⚠[/yellow] No printers configured")
|
|
119
|
+
console.print(" Run [cyan]zday printer scan[/cyan] to discover printers")
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
table = Table(title="Configured Printers")
|
|
123
|
+
table.add_column("Lab", style="cyan")
|
|
124
|
+
table.add_column("Name")
|
|
125
|
+
table.add_column("IP Address")
|
|
126
|
+
table.add_column("Model")
|
|
127
|
+
table.add_column("Label Styles")
|
|
128
|
+
for p in printers:
|
|
129
|
+
styles = ", ".join(p["styles"][:2])
|
|
130
|
+
if len(p["styles"]) > 2:
|
|
131
|
+
styles += f" (+{len(p['styles'])-2})"
|
|
132
|
+
table.add_row(p["lab"], p["name"], p["ip"], p["model"], styles)
|
|
133
|
+
console.print(table)
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
if json_output:
|
|
137
|
+
console.print(json.dumps({"error": str(e)}))
|
|
138
|
+
else:
|
|
139
|
+
console.print(f"[red]✗[/red] Error: {e}")
|
|
140
|
+
raise typer.Exit(1)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@printer_app.command("test")
|
|
144
|
+
def test_print(
|
|
145
|
+
printer_name: str = typer.Argument(..., help="Printer name or IP address"),
|
|
146
|
+
lab: str = typer.Option("scan-results", "--lab", "-l", help="Lab containing the printer"),
|
|
147
|
+
label_style: str = typer.Option("tube_2inX1in", "--style", "-s", help="Label style to print"),
|
|
148
|
+
):
|
|
149
|
+
"""Send a test print to a specific printer."""
|
|
150
|
+
try:
|
|
151
|
+
import zebra_day.print_mgr as zdpm
|
|
152
|
+
zp = zdpm.zpl()
|
|
153
|
+
|
|
154
|
+
console.print(f"[cyan]→[/cyan] Sending test print to {printer_name}...")
|
|
155
|
+
result = zp.print_zpl(
|
|
156
|
+
lab=lab,
|
|
157
|
+
printer_name=printer_name,
|
|
158
|
+
uid_barcode="TEST-PRINT",
|
|
159
|
+
alt_a="Test Label",
|
|
160
|
+
alt_b="zebra_day CLI",
|
|
161
|
+
label_zpl_style=label_style,
|
|
162
|
+
)
|
|
163
|
+
console.print(f"[green]✓[/green] Test print sent successfully")
|
|
164
|
+
|
|
165
|
+
except Exception as e:
|
|
166
|
+
console.print(f"[red]✗[/red] Print error: {e}")
|
|
167
|
+
raise typer.Exit(1)
|
|
168
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""ZPL template management commands for zebra_day CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from zebra_day import paths as xdg
|
|
14
|
+
|
|
15
|
+
template_app = typer.Typer(help="ZPL template management commands")
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_template_dirs() -> list[Path]:
|
|
20
|
+
"""Get all template directories."""
|
|
21
|
+
from importlib.resources import files
|
|
22
|
+
|
|
23
|
+
dirs = []
|
|
24
|
+
# XDG data directory
|
|
25
|
+
xdg_styles = xdg.get_label_styles_dir()
|
|
26
|
+
if xdg_styles.exists():
|
|
27
|
+
dirs.append(xdg_styles)
|
|
28
|
+
|
|
29
|
+
# Package directory
|
|
30
|
+
try:
|
|
31
|
+
pkg_styles = Path(str(files("zebra_day"))) / "etc" / "label_styles"
|
|
32
|
+
if pkg_styles.exists():
|
|
33
|
+
dirs.append(pkg_styles)
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
return dirs
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _find_template(name: str) -> Optional[Path]:
|
|
41
|
+
"""Find a template file by name."""
|
|
42
|
+
for template_dir in _get_template_dirs():
|
|
43
|
+
# Try exact match
|
|
44
|
+
for ext in ["", ".zpl", ".txt"]:
|
|
45
|
+
path = template_dir / f"{name}{ext}"
|
|
46
|
+
if path.exists():
|
|
47
|
+
return path
|
|
48
|
+
# Try with zpl_ prefix
|
|
49
|
+
for ext in ["", ".zpl", ".txt"]:
|
|
50
|
+
path = template_dir / f"zpl_{name}{ext}"
|
|
51
|
+
if path.exists():
|
|
52
|
+
return path
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@template_app.command("list")
|
|
57
|
+
def list_templates(
|
|
58
|
+
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"),
|
|
59
|
+
verbose: bool = typer.Option(False, "--verbose", "-v", help="Show full paths"),
|
|
60
|
+
):
|
|
61
|
+
"""List available ZPL templates."""
|
|
62
|
+
templates = []
|
|
63
|
+
|
|
64
|
+
for template_dir in _get_template_dirs():
|
|
65
|
+
for f in template_dir.iterdir():
|
|
66
|
+
if f.is_file() and not f.name.startswith("."):
|
|
67
|
+
if f.suffix in [".zpl", ".txt", ""] or f.name.startswith("zpl_"):
|
|
68
|
+
templates.append({
|
|
69
|
+
"name": f.stem,
|
|
70
|
+
"path": str(f),
|
|
71
|
+
"size": f.stat().st_size,
|
|
72
|
+
"source": "user" if "zebra_day" not in str(template_dir) else "package",
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
# Dedupe by name, prefer user templates
|
|
76
|
+
seen = {}
|
|
77
|
+
for t in templates:
|
|
78
|
+
if t["name"] not in seen or t["source"] == "user":
|
|
79
|
+
seen[t["name"]] = t
|
|
80
|
+
templates = list(seen.values())
|
|
81
|
+
|
|
82
|
+
if json_output:
|
|
83
|
+
console.print(json.dumps(templates, indent=2))
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
if not templates:
|
|
87
|
+
console.print("[yellow]⚠[/yellow] No templates found")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
table = Table(title="ZPL Templates")
|
|
91
|
+
table.add_column("Name", style="cyan")
|
|
92
|
+
table.add_column("Source")
|
|
93
|
+
table.add_column("Size")
|
|
94
|
+
if verbose:
|
|
95
|
+
table.add_column("Path", style="dim")
|
|
96
|
+
|
|
97
|
+
for t in sorted(templates, key=lambda x: x["name"]):
|
|
98
|
+
source_style = "[green]user[/green]" if t["source"] == "user" else "[dim]package[/dim]"
|
|
99
|
+
if verbose:
|
|
100
|
+
table.add_row(t["name"], source_style, f"{t['size']} bytes", t["path"])
|
|
101
|
+
else:
|
|
102
|
+
table.add_row(t["name"], source_style, f"{t['size']} bytes")
|
|
103
|
+
|
|
104
|
+
console.print(table)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@template_app.command("preview")
|
|
108
|
+
def preview(
|
|
109
|
+
template_name: str = typer.Argument(..., help="Template name to preview"),
|
|
110
|
+
output: Optional[str] = typer.Option(None, "--output", "-o", help="Output PNG file path"),
|
|
111
|
+
):
|
|
112
|
+
"""Generate a PNG preview of a ZPL template."""
|
|
113
|
+
template_path = _find_template(template_name)
|
|
114
|
+
if not template_path:
|
|
115
|
+
console.print(f"[red]✗[/red] Template not found: {template_name}")
|
|
116
|
+
raise typer.Exit(1)
|
|
117
|
+
|
|
118
|
+
console.print(f"[cyan]→[/cyan] Generating preview for {template_name}...")
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
import zebra_day.print_mgr as zdpm
|
|
122
|
+
zp = zdpm.zpl()
|
|
123
|
+
|
|
124
|
+
# Read template
|
|
125
|
+
zpl_content = template_path.read_text()
|
|
126
|
+
|
|
127
|
+
# Generate PNG
|
|
128
|
+
if not output:
|
|
129
|
+
output_path = xdg.get_generated_files_dir() / f"{template_name}_preview.png"
|
|
130
|
+
else:
|
|
131
|
+
output_path = Path(output)
|
|
132
|
+
|
|
133
|
+
result = zp.generate_label_png(zpl_content, str(output_path), False)
|
|
134
|
+
console.print(f"[green]✓[/green] Preview generated: {output_path}")
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
console.print(f"[red]✗[/red] Preview error: {e}")
|
|
138
|
+
raise typer.Exit(1)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@template_app.command("edit")
|
|
142
|
+
def edit(
|
|
143
|
+
template_name: str = typer.Argument(..., help="Template name to edit"),
|
|
144
|
+
editor: Optional[str] = typer.Option(None, "--editor", "-e", help="Editor command"),
|
|
145
|
+
):
|
|
146
|
+
"""Open a ZPL template in an editor."""
|
|
147
|
+
template_path = _find_template(template_name)
|
|
148
|
+
if not template_path:
|
|
149
|
+
console.print(f"[red]✗[/red] Template not found: {template_name}")
|
|
150
|
+
raise typer.Exit(1)
|
|
151
|
+
|
|
152
|
+
# Determine editor
|
|
153
|
+
if not editor:
|
|
154
|
+
editor = os.environ.get("VISUAL") or os.environ.get("EDITOR") or "vi"
|
|
155
|
+
|
|
156
|
+
console.print(f"[cyan]→[/cyan] Opening {template_path} with {editor}...")
|
|
157
|
+
try:
|
|
158
|
+
subprocess.run([editor, str(template_path)])
|
|
159
|
+
except Exception as e:
|
|
160
|
+
console.print(f"[red]✗[/red] Error opening editor: {e}")
|
|
161
|
+
raise typer.Exit(1)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@template_app.command("show")
|
|
165
|
+
def show(
|
|
166
|
+
template_name: str = typer.Argument(..., help="Template name to display"),
|
|
167
|
+
):
|
|
168
|
+
"""Display the contents of a ZPL template."""
|
|
169
|
+
template_path = _find_template(template_name)
|
|
170
|
+
if not template_path:
|
|
171
|
+
console.print(f"[red]✗[/red] Template not found: {template_name}")
|
|
172
|
+
raise typer.Exit(1)
|
|
173
|
+
|
|
174
|
+
console.print(f"[dim]# {template_path}[/dim]\n")
|
|
175
|
+
console.print(template_path.read_text())
|
|
176
|
+
|