qltool 0.2.4__tar.gz → 0.2.7__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.
@@ -1,5 +1,5 @@
1
1
  [bumpversion]
2
- current_version = 0.2.4
2
+ current_version = 0.2.7
3
3
  commit = True
4
4
  tag = True
5
5
 
@@ -0,0 +1,52 @@
1
+ {
2
+ "packages": [
3
+ {
4
+ "source": "https://github.com/jaromrax/Crossbar",
5
+ "autoload": false,
6
+ "extensions": [
7
+ "-src/index.ts"
8
+ ]
9
+ },
10
+ {
11
+ "source": "https://github.com/jaromrax/pi-advisor",
12
+ "autoload": false,
13
+ "extensions": [
14
+ "-dist/index.js"
15
+ ]
16
+ },
17
+ {
18
+ "source": "git:github.com/jaromrax/pi-pass-secrets",
19
+ "autoload": false,
20
+ "extensions": [
21
+ "-index.ts"
22
+ ]
23
+ },
24
+ {
25
+ "source": "git:github.com/jaromrax/pi-podman-containers",
26
+ "autoload": false,
27
+ "skills": [
28
+ "-skills/podman-containers/SKILL.md"
29
+ ]
30
+ },
31
+ {
32
+ "source": "npm:pi-intercom",
33
+ "autoload": false,
34
+ "extensions": [
35
+ "-index.ts"
36
+ ],
37
+ "skills": [
38
+ "-skills/pi-intercom/SKILL.md"
39
+ ]
40
+ },
41
+ {
42
+ "source": "npm:trelly",
43
+ "autoload": false,
44
+ "skills": [
45
+ "-skills/README.md",
46
+ "-skills/trelly/SKILL.md",
47
+ "-skills/trelly-card-display.md",
48
+ "-skills/trelly-mcp/SKILL.md"
49
+ ]
50
+ }
51
+ ]
52
+ }
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: qltool
3
- Version: 0.2.4
3
+ Version: 0.2.7
4
4
  Summary: PRINT simple labels on ql700
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: click>=8.2.1
@@ -77,3 +77,7 @@ You point to a line of a table and `c-c q`{.verbatim} or
77
77
 
78
78
  and this is run
79
79
  `./bin_qltool.py "id: D4-12345678; name: najaka blbost z inventury owner:Pepa " -q`
80
+
81
+ \## LAST DEVELOPMENTS IN BANNER MODE mosquitto~pub~ -t qltool/print -m
82
+ \'KLADIVA INBUS --banner --font-size 220 -p\' uv run src/qltool/main.py
83
+ \$\'KLADIVA`\nINBUS`{=latex}\' --banner --font-size 220
@@ -63,3 +63,7 @@ You point to a line of a table and `c-c q`{.verbatim} or
63
63
 
64
64
  and this is run
65
65
  `./bin_qltool.py "id: D4-12345678; name: najaka blbost z inventury owner:Pepa " -q`
66
+
67
+ \## LAST DEVELOPMENTS IN BANNER MODE mosquitto~pub~ -t qltool/print -m
68
+ \'KLADIVA INBUS --banner --font-size 220 -p\' uv run src/qltool/main.py
69
+ \$\'KLADIVA`\nINBUS`{=latex}\' --banner --font-size 220
@@ -147,3 +147,8 @@ You point to a line of a table and =c-c q= or =c-c w= will view/print the code f
147
147
 
148
148
  and this is run
149
149
  ~./bin_qltool.py "id: D4-12345678; name: najaka blbost z inventury owner:Pepa " -q~
150
+
151
+
152
+ ## LAST DEVELOPMENTS IN BANNER MODE
153
+ mosquitto_pub -t qltool/print -m 'KLADIVA INBUS --banner --font-size 220 -p'
154
+ uv run src/qltool/main.py $'KLADIVA\nINBUS' --banner --font-size 220
@@ -0,0 +1,175 @@
1
+ // Starter config generated by `fence config init`; this file extends "code".
2
+ // Rules from "code" are inherited and not shown below.
3
+ // Add your project-specific overrides in this file.
4
+ // Run `fence --list-templates` to see available templates.
5
+ // Configuration reference: https://github.com/fencesandbox/fence/blob/main/docs/configuration.md
6
+ {
7
+ "network": {
8
+ "deniedDomains": ["lupa.cz"],
9
+ "allowedDomains": ["*"],
10
+ "allowLocalOutbound": true,
11
+ "allowLocalOutboundPorts": [3128, 1080],
12
+ "httpProxyPort":3128,
13
+ "socksProxyPort":1080
14
+ },
15
+ "filesystem": {
16
+ "defaultDenyRead": true,
17
+ "allowRead": [
18
+ "~/DATA/pv_live_rec/logs/*",
19
+ ".",
20
+ "./**",
21
+ "/usr",
22
+ "/lib",
23
+ "/lib64",
24
+ "/bin",
25
+ "/tmp/**",
26
+ "/var/log/lxini/**",
27
+ "~/.tmux.conf",
28
+ "~/.bashrc",
29
+ "~/.password-store/**",
30
+ "~/.gnupg",
31
+ "~/.gnupg/*",
32
+ "~/.gnupg/**",
33
+ "/run/user/1000/gnupg/**",
34
+ "/run/user/1000/gnupg/**",
35
+ "~/.local/bin/env",
36
+ "~/.npm-global/lib/node_modules/**",
37
+ "~/.npm-global/bin/**",
38
+ "~/44_lxc/**"
39
+ ],
40
+ "allowWrite": [
41
+ ".",
42
+ // Temp files
43
+ "/tmp",
44
+
45
+ // Local cache, needed by tools like `uv`
46
+ "~/.cache/**",
47
+
48
+ // Claude Code
49
+ "~/.claude*",
50
+ "~/.claude/**",
51
+
52
+ // Amp
53
+ "~/.amp",
54
+
55
+ // Codex
56
+ "~/.codex/**",
57
+
58
+ // Cursor
59
+ "~/.cursor/**",
60
+
61
+ // OpenCode
62
+ "~/.opencode/**",
63
+ "~/.local/state/**",
64
+
65
+ // Gemini CLI
66
+ "~/.gemini/**",
67
+
68
+ // Factory CLI (droid)
69
+ "~/.factory/**",
70
+
71
+ // Pi
72
+ "~/.pi/**",
73
+
74
+ // Copilot
75
+ "~/.copilot/**",
76
+
77
+ // Kiro CLI
78
+ "~/.kiro/**",
79
+ "~/Library/Application Support/kiro-cli/**",
80
+
81
+ // Package manager caches
82
+ "~/.npm/_cacache",
83
+ "~/.npm/_npx",
84
+ "~/.cache",
85
+ "~/.bun/**",
86
+
87
+ // Cargo cache (Rust, used by Codex)
88
+ "~/.cargo/registry/**",
89
+ "~/.cargo/git/**",
90
+ "~/.cargo/.package-cache",
91
+
92
+ // Shell completion cache
93
+ "~/.zcompdump*",
94
+
95
+ // XDG directories for app configs/data
96
+ "~/.local/share/**",
97
+ "~/.config/**",
98
+ "~/.gnupg",
99
+ "~/.gnupg/*",
100
+ "~/.gnupg/**",
101
+ "/run/user/1000/gnupg/**",
102
+ "/run/user/1000/gnupg/**",
103
+ "./**"
104
+ ],
105
+ "denyRead": [
106
+ // SSH private keys and config
107
+ "~/.ssh/id_*",
108
+ "~/.ssh/config",
109
+ "~/.ssh/*.pem",
110
+
111
+
112
+ // Cloud provider credentials
113
+ "~/.aws/**",
114
+ "~/.config/gcloud/**",
115
+ "~/.kube/**",
116
+
117
+ // Docker config (may contain registry auth)
118
+ "~/.docker/**",
119
+
120
+ // Package manager auth tokens
121
+ "~/.pypirc",
122
+ "~/.netrc",
123
+ "~/.git-credentials",
124
+ "~/.cargo/credentials",
125
+ "~/.cargo/credentials.toml"
126
+ ]
127
+ },
128
+ "command": {
129
+ "useDefaults": true,
130
+ "deny": [
131
+ // Git commands that modify remote state
132
+ "git push",
133
+ "git reset",
134
+ "git clean",
135
+ "git checkout --",
136
+ "git rebase",
137
+ "git merge",
138
+
139
+ // Package publishing commands
140
+ "npm publish",
141
+ "pnpm publish",
142
+ "yarn publish",
143
+ "cargo publish",
144
+ "twine upload",
145
+ "gem push",
146
+
147
+ // Privilege escalation
148
+ "sudo",
149
+
150
+ // GitHub CLI commands that modify remote state
151
+ "gh pr create",
152
+ "gh pr merge",
153
+ "gh pr close",
154
+ "gh pr reopen",
155
+ "gh pr review",
156
+ "gh pr comment",
157
+ "gh release create",
158
+ "gh release delete",
159
+ "gh repo create",
160
+ "gh repo fork",
161
+ "gh repo delete",
162
+ "gh issue create",
163
+ "gh issue close",
164
+ "gh issue comment",
165
+ "gh gist create",
166
+ "gh workflow run",
167
+ "gh api",
168
+ "gh auth login",
169
+ "gh secret set",
170
+ "gh secret delete",
171
+ "gh variable set",
172
+ "gh variable delete"
173
+ ]
174
+ }
175
+ }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "qltool"
3
- version = "0.2.4"
3
+ version = "0.2.7"
4
4
  description = "PRINT simple labels on ql700"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -42,37 +42,10 @@ def on_message(client, userdata, msg):
42
42
 
43
43
  text = data.decode("utf8")
44
44
 
45
- blocks = shlex.split(text)
46
- print(blocks)
47
-
48
- FS = None
49
- Q = False
50
- P = False
51
- lines = []
52
- for i, obj in enumerate(blocks):
53
-
54
- print(f"#{i}# {obj}")
55
-
56
- if obj.find("-") < 0:
57
- lines.append(obj)
58
- elif obj == "-q":
59
- print("create qr")
60
- Q = True
61
- lines.append("-q")
62
- elif obj == "-p":
63
- print("print")
64
- lines.append("-p")
65
- P = True
66
- elif obj == "--font-size":
67
- FS = obj[i + 1]
68
-
69
- if FS is None:
70
- FS = 72
71
-
72
- print( f" {lines}, {FS}, create qr ={Q}, print {P}")
73
- #cli( lines, font_size=FS, qr_create=Q, do_print=P)
74
- #cli( lines, qr_create=Q, do_print=P)
75
- res = call_with_runner( lines )
45
+ args = shlex.split(text)
46
+ print(args)
47
+
48
+ res = call_with_runner(args)
76
49
  for i in res:
77
50
  print(i)
78
51
  print("")
@@ -63,19 +63,88 @@ def is_user_in_lp_group():
63
63
  #
64
64
  # ----------------------------------------------------------------------
65
65
  def get_data_path():
66
- #src = files("data").joinpath("ql570")
67
- src = files("qltool").joinpath("data", "ql570")
68
- with as_file(src) as p:
69
- return str(p)
66
+ """Get path to ql570 binary, works both when installed and run directly."""
67
+ try:
68
+ # Try installed package first
69
+ src = files("qltool").joinpath("data", "ql570")
70
+ with as_file(src) as p:
71
+ return str(p)
72
+ except Exception:
73
+ # Fall back to relative path when running directly
74
+ # __file__ is src/qltool/main.py, data is in src/qltool/data/
75
+ fallback = os.path.join(os.path.dirname(__file__), "data", "ql570")
76
+ if os.path.exists(fallback):
77
+ return fallback
78
+ raise
70
79
 
71
80
  # =====================================================================
72
81
  #
73
82
  # ----------------------------------------------------------------------
74
- def check_lpx():
75
- prs = glob.glob("/dev/usb/lp*")
76
- if not prs:
77
- raise RuntimeError("No LP device found under /dev/usb/lp*")
78
- return prs[0]
83
+ def _read(f):
84
+ try:
85
+ with open(f) as fh:
86
+ return fh.read().strip()
87
+ except Exception:
88
+ return ""
89
+
90
+ def list_lp_candidates():
91
+ """Find /dev/usb/lp* candidates, enriched with USB VID/PID via sysfs.
92
+ Returns list of dicts: {dev, name, vid, pid, manufacturer, product}.
93
+ Works even when /dev/usb is invisible but /sys/class/usbmisc is not."""
94
+ names = {}
95
+ for p in glob.glob("/sys/class/usbmisc/lp*"):
96
+ names[os.path.basename(p)] = p
97
+ for p in glob.glob("/dev/usb/lp*"):
98
+ names.setdefault(os.path.basename(p), p)
99
+ out = []
100
+ for name in sorted(names):
101
+ sysfs = f"/sys/class/usbmisc/{name}"
102
+ base = f"{sysfs}/device/.."
103
+ out.append({
104
+ "dev": f"/dev/usb/{name}",
105
+ "name": name,
106
+ "vid": _read(f"{base}/idVendor").lower(),
107
+ "pid": _read(f"{base}/idProduct").lower(),
108
+ "manufacturer": _read(f"{base}/manufacturer"),
109
+ "product": _read(f"{base}/product"),
110
+ })
111
+ return out
112
+
113
+ def check_lpx(prefer_vid="04f9"):
114
+ """Auto-detect printer device. Prefers Brother (VID 04f9), else single/first."""
115
+ cands = list_lp_candidates()
116
+ if not cands:
117
+ raise RuntimeError(
118
+ "No LP device found (checked /dev/usb/lp* + /sys/class/usbmisc/lp*). "
119
+ "The image was created OK - only the printer device is missing. "
120
+ "Try: ls -l /dev/usb/ ; lsusb -d 04f9: ; "
121
+ "unplug/replug + power on the printer, then "
122
+ "sudo modprobe usblp && ls -l /dev/usb/lp*. "
123
+ "If the node appears under another name, pass it with -d/--device."
124
+ )
125
+ if len(cands) == 1:
126
+ c = cands[0]
127
+ print(f"i... auto-detected printer: {c['dev']} "
128
+ f"({c['manufacturer']} {c['product']} {c['vid']}:{c['pid']})".strip())
129
+ return c["dev"]
130
+ # multiple: prefer Brother VID (covers QL-500/570/700/800/1100...)
131
+ if prefer_vid:
132
+ matches = [c for c in cands if c["vid"] == prefer_vid.lower()]
133
+ if len(matches) == 1:
134
+ c = matches[0]
135
+ print(f"i... auto-detected Brother printer: {c['dev']} "
136
+ f"({c['manufacturer']} {c['product']} {c['vid']}:{c['pid']})".strip())
137
+ return c["dev"]
138
+ if len(matches) > 1:
139
+ lst = ", ".join(m["dev"] for m in matches)
140
+ print(f"W... multiple Brother printers ({lst}), using {matches[0]['dev']} - "
141
+ f"override with -d/--device")
142
+ return matches[0]["dev"]
143
+ lst = ", ".join(f"{c['dev']} [{c['manufacturer']} {c['product']} "
144
+ f"{c['vid']}:{c['pid']}]".strip() for c in cands)
145
+ print(f"W... multiple LP devices ({lst}), using {cands[0]['dev']} - "
146
+ f"override with -d/--device")
147
+ return cands[0]["dev"]
79
148
 
80
149
  # =====================================================================
81
150
  #
@@ -205,6 +274,69 @@ def rotate_mono_90(img, clockwise=True):
205
274
  return img.transpose(Image.ROTATE_270 if clockwise else Image.ROTATE_90)
206
275
 
207
276
 
277
+ # =====================================================================
278
+ #
279
+ # ----------------------------------------------------------------------
280
+ def make_banner_image(lines, paper_width=714, font_size=None, pad=13):
281
+ """
282
+ Create a banner image with text running along the paper length.
283
+
284
+ Creates a horizontal text image with dimensions (text_length x 714).
285
+ This is NOT rotated - sent to printer as-is so text prints along paper length.
286
+
287
+ - lines: one or more strings stacked vertically
288
+ - paper_width: width of the paper (714px for 62mm) - used as height here
289
+ - font_size: font size (uses CLI font_size or default)
290
+ - pad: padding around text
291
+
292
+ Returns a PIL.Image in RGB mode with dimensions (text_length x 714).
293
+ """
294
+ FSIZ = font_size or 36
295
+ try:
296
+ font = ImageFont.truetype(FONT_PATH, FSIZ) if FONT_PATH else ImageFont.load_default()
297
+ except Exception:
298
+ font = ImageFont.load_default()
299
+
300
+ PAD = int((FSIZ / 36) * pad)
301
+
302
+ if isinstance(lines, str):
303
+ lines = lines.split("\n")
304
+ lines = [line for line in lines if line != ""]
305
+ if not lines:
306
+ raise ValueError("Provide at least 1 line.")
307
+
308
+ # Measure the text
309
+ tmp = Image.new("L", (10000, 1000), 255)
310
+ draw_tmp = ImageDraw.Draw(tmp)
311
+ line_sizes = []
312
+ for line in lines:
313
+ bbox = draw_tmp.textbbox((0, 0), line, font=font)
314
+ line_sizes.append((bbox[2] - bbox[0], bbox[3] - bbox[1]))
315
+
316
+ text_width = max(w for w, _ in line_sizes)
317
+ spacing = int(0.2 * (line_sizes[0][1] if line_sizes else 12)) or 4
318
+ text_height = sum(h for _, h in line_sizes) + spacing * (len(lines) - 1)
319
+ print(f"D... Banner text measurements: text_width={text_width}, text_height={text_height}, lines={len(lines)}, PAD={PAD}")
320
+
321
+ # Create horizontal text image: width = text length + padding, height = paper width
322
+ # Banners are NOT rotated - they are sent to printer as-is with height=714
323
+ img_width = int(text_width + PAD * 2)
324
+ img_height = int(paper_width)
325
+ print(f"D... Banner image dimensions: {img_width}x{img_height} (NOT rotated, height stays 714)")
326
+
327
+ im = Image.new("RGB", (img_width, img_height), "white")
328
+ draw = ImageDraw.Draw(im)
329
+
330
+ # Center the text block vertically and each line horizontally.
331
+ y = (img_height - text_height) // 2
332
+ for (line_width, line_height), line in zip(line_sizes, lines):
333
+ x = PAD + max((text_width - line_width) // 2, 0)
334
+ draw.text((x, y), line, font=font, fill=0)
335
+ y += line_height + spacing
336
+
337
+ return im
338
+
339
+
208
340
 
209
341
 
210
342
  # =====================================================================
@@ -347,7 +479,7 @@ def make_bw_text_with_qr(lines,
347
479
  @click.version_option(version("qltool"), prog_name="qltool")
348
480
  @click.argument("lines", nargs=-1, required=True)
349
481
  @click.option("-p", "--print", "do_print", is_flag=True, default=False, help="Send to printer (calls ql570).")
350
- @click.option("-d", "--device", "device", default=None, help="Printer device (defaults to first /dev/usb/lp*).")
482
+ @click.option("-d", "--device", "device", default=None, help="Printer device (default: auto-detect Brother /dev/usb/lp*).")
351
483
  @click.option("-f", "--format", "fmt", default=62, help="Format code for ql570 (default 62 at the width of 714).")
352
484
  @click.option("--width", default=714, show_default=True, help="Total image width.")
353
485
  @click.option("--gray", "gray", default=50, show_default=True, help="Gray bias percent (0..100), default 50.")
@@ -355,17 +487,19 @@ def make_bw_text_with_qr(lines,
355
487
  @click.option("--qr", "qr_path", default=None, show_default=True, help="QR image path.")
356
488
  @click.option("-q", "qr_create", is_flag=True, default=False, show_default=True, help="do QR")
357
489
  @click.option("--address", "address", default=None, show_default=True, help="Address/URL to encode in QR code.")
358
- def cli(lines, do_print, device, fmt, width, gray, font_size, qr_path, qr_create, address):
490
+ @click.option("--banner", "banner", is_flag=True, default=False, show_default=True, help="Create a banner with text along the paper length.")
491
+ def cli(lines, do_print, device, fmt, width, gray, font_size, qr_path, qr_create, address, banner):
359
492
  """
360
493
  Create a black-on-white PNG from 1-3 lines of text.
361
494
  By default displays the image with OpenCV. Use -p to print via ql570.
495
+ Use --banner to create a long text banner along the paper length.
362
496
  """
363
497
  #print(lines, type(lines), len(lines))
364
498
  if type(lines)is not tuple:
365
499
  print("X... the text should be python-tuple for some reason....")
366
500
  sys.exit(1)
367
501
 
368
- if len(lines) > 3: # "A" "B" "C"
502
+ if not banner and len(lines) > 3: # "A" "B" "C"
369
503
  print("Error: max 3 lines allowed.", file=sys.stderr)
370
504
  sys.exit(2)
371
505
 
@@ -377,7 +511,7 @@ def cli(lines, do_print, device, fmt, width, gray, font_size, qr_path, qr_create
377
511
  else:
378
512
  print("Q... lines ...I do not understand")
379
513
 
380
- if len(lines) > 3: # "A" "B" "C"
514
+ if not banner and len(lines) > 3: # "A" "B" "C"
381
515
  print("Error: max 3 lines allowed.", file=sys.stderr)
382
516
  sys.exit(2)
383
517
 
@@ -400,13 +534,26 @@ def cli(lines, do_print, device, fmt, width, gray, font_size, qr_path, qr_create
400
534
  lines = ["\n".join(lines) ] # BACK TO NICE \n
401
535
 
402
536
  im = None
403
- if use_qr_version:
537
+ if banner:
538
+ # Banner mode: text runs along paper length, with optional stacked lines.
539
+ banner_lines = lines[0].split("\n")
540
+ im = make_banner_image(banner_lines, paper_width=714, font_size=font_size)
541
+ print(f"D... Banner image size: {im.size}")
542
+ elif use_qr_version:
404
543
  im = make_bw_text_with_qr(list(lines), my_qr_path, width=714, font_size=font_size)
405
544
  else:
406
545
  im = make_bw_text_image(list(lines), width=714, font_size=font_size)
407
546
 
408
547
  mono = convert_to_mono_floyd(im, gray_percent=int(gray))
409
- mono = rotate_mono_90(mono, clockwise=True)
548
+ print(f"D... After convert_to_mono_floyd: {mono.size}")
549
+ # Rotate only normal labels, NOT banners
550
+ # Banners are already created with correct orientation: width=variable, height=714
551
+ if not banner:
552
+ mono = rotate_mono_90(mono, clockwise=True)
553
+ print(f"D... After rotation for normal label: {mono.size}")
554
+ else:
555
+ print(f"D... Banner - keeping original orientation")
556
+ print(f"D... Final mono image size: {mono.size}, mode: {mono.mode}")
410
557
 
411
558
  out = get_tmp_fname()
412
559
  print(f"D.... {out}")
@@ -428,6 +575,8 @@ def cli(lines, do_print, device, fmt, width, gray, font_size, qr_path, qr_create
428
575
  exe = get_data_path()
429
576
  dev = device if device else check_lpx()
430
577
  CMD = f"{exe} {dev} {fmt} {out}"
578
+ print(f"D... Printer command: {CMD}")
579
+ print(f"D... Image file: {out}, size: {mono.size}")
431
580
 
432
581
  if not is_user_in_lp_group():
433
582
  print("X... ", fg.red, "you are not in lp group", fg.default)
File without changes
File without changes
File without changes
File without changes
File without changes