diff --git a/python/apps/pclock.py b/python/apps/pclock.py index 2c062c4..9ed7ae0 100644 --- a/python/apps/pclock.py +++ b/python/apps/pclock.py @@ -1,20 +1,21 @@ -""" pclock.py - Python clock +r""" +usage: pclock.py [-h] -Synopsis: - pclock.py +DESCRIPTION + A Python graphical clock. -Description: - A simple clock. +options: + -h, --help show this help message and exit -Notes: - -https://www.daniweb.com/programming/software-development/code/216785/tkinter-digital-clock-python -https://www.daniweb.com/programming/software-development/threads/70076/disabling-the-title-bar -https://stackoverflow.com/questions/29641616/drag-window-when-using-overrideredirect -https://stackoverflow.com/questions/39529600/remove-titlebar-without-overrideredirect-using-tkinter -https://stackoverflow.com/questions/28467285/how-do-i-bind-the-escape-key-to-close-this-window +NOTES + https://www.daniweb.com/programming/software-development/code/216785/tkinter-digital-clock-python + https://www.daniweb.com/programming/software-development/threads/70076/disabling-the-title-bar + https://stackoverflow.com/questions/29641616/drag-window-when-using-overrideredirect + https://stackoverflow.com/questions/39529600/remove-titlebar-without-overrideredirect-using-tkinter + https://stackoverflow.com/questions/28467285/how-do-i-bind-the-escape-key-to-close-this-window """ +import argparse import tkinter import tkinter.font import sys @@ -46,6 +47,7 @@ def __init__(self, master=None): self._offsety = 0 self.bind('', self.button1) self.bind('', self.b1motion) + self.bind('', self.escape) self.bind('', self.escape) def button1(self, event): @@ -59,7 +61,7 @@ def b1motion(self, event): def escape(self, event): self.withdraw() - sys.exit() + sys.exit(0) class Clock(FloatingBorderlessWindow): @@ -95,10 +97,23 @@ def tick(self): self._clock.after(1000, self.tick) -def main(): +def main(args): clock = Clock() clock.mainloop() + return 0 if __name__ == '__main__': - main() + parser = argparse.ArgumentParser( + description="""DESCRIPTION + A Python graphical clock.""", + epilog="""NOTES + https://www.daniweb.com/programming/software-development/code/216785/tkinter-digital-clock-python + https://www.daniweb.com/programming/software-development/threads/70076/disabling-the-title-bar + https://stackoverflow.com/questions/29641616/drag-window-when-using-overrideredirect + https://stackoverflow.com/questions/39529600/remove-titlebar-without-overrideredirect-using-tkinter + https://stackoverflow.com/questions/28467285/how-do-i-bind-the-escape-key-to-close-this-window""", + formatter_class=argparse.RawDescriptionHelpFormatter) + + args = parser.parse_args() + sys.exit(main(args)) diff --git a/python/apps/refresh_help.py b/python/apps/refresh_help.py new file mode 100644 index 0000000..4864e9e --- /dev/null +++ b/python/apps/refresh_help.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +r""" +usage: refresh_help.py [-h] [-p] source_file + +Refresh module docstring from -h output + +positional arguments: + source_file Python script to update + +options: + -h, --help show this help message and exit + -p, --preview Preview changes without writing to file (pipe to + head/tail/etc as needed) +""" + +import argparse +import re +import shutil +import subprocess +import sys +from pathlib import Path + + +def get_help_output(script_path: Path) -> str: + """Run the script with -h and return clean output.""" + try: + result = subprocess.run( + [sys.executable, str(script_path), "-h"], + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + except subprocess.CalledProcessError as e: + print(f"Error running {script_path} with -h:\n{e.stderr or e}") + sys.exit(1) + except FileNotFoundError: + print(f"Script not found: {script_path}") + sys.exit(1) + + +def create_backup(file_path: Path): + """Create a .bak copy.""" + bak_path = file_path.with_suffix(file_path.suffix + ".bak") + shutil.copy2(file_path, bak_path) + print(f"💾 Backup created: {bak_path}") + + +def update_docstring(file_path: Path, help_text: str, preview: bool = False): + """Replace the module docstring.""" + content = file_path.read_text(encoding="utf-8") + + # Match leading comments + first triple-quoted string + docstring_pattern = re.compile( + r'^((#.*\n)*?)(r?"""[\s\S]*?"""|r?\'\'\'[\s\S]*?\'\'\')', + re.MULTILINE + ) + match = docstring_pattern.search(content) + + new_docstring = f'r"""\n{help_text}\n"""' + + if match: + prefix = match.group(1) # shebang + comments + updated = prefix + new_docstring + content[match.end():] + else: + # No docstring — insert after leading comments + lines = content.splitlines(keepends=True) + insert_pos = 0 + for i, line in enumerate(lines): + if not line.strip().startswith("#") and line.strip(): + insert_pos = i + break + lines.insert(insert_pos, new_docstring + "\n\n") + updated = "".join(lines) + + if preview: + print(updated) + else: + create_backup(file_path) + file_path.write_text(updated, encoding="utf-8") + print(f"✅ Successfully updated docstring in {file_path}") + + +def main(): + parser = argparse.ArgumentParser(description="Refresh module docstring from -h output") + parser.add_argument("-p", "--preview", action="store_true", + help="Preview changes without writing to file (pipe to head/tail/etc as needed)") + parser.add_argument("source_file", type=Path, help="Python script to update") + args = parser.parse_args() + + if not args.source_file.exists(): + print(f"File not found: {args.source_file}") + sys.exit(1) + + help_output = get_help_output(args.source_file) + update_docstring(args.source_file, help_output, preview=args.preview) + + +if __name__ == "__main__": + main() diff --git a/python/apps/xv.py b/python/apps/xv.py index 503909f..1b59f53 100644 --- a/python/apps/xv.py +++ b/python/apps/xv.py @@ -1,7 +1,9 @@ r""" usage: xv.py [-h] filename -A simple image viewer and a tribute to the original xv image display editing program for the X Window System. +DESCRIPTION + A simple image viewer and a tribute to the original xv image display + editing program for the X Window System. positional arguments: filename Image file to display. @@ -9,57 +11,89 @@ options: -h, --help show this help message and exit -NOTES: - sudo apt-get install python3-pip - sudo pip3 install pillow - sudo apt-get install python3-pil.imagetk - http://www.trilon.com/xv/ +REQUIREMENTS + One-time system setup: + sudo apt update + sudo apt install python3-tk python3-pil python3-pil.imagetk + + To use this application in a venv, you'll have to bring in + these system packages when you create the venv: + python3 -m venv --system-site-packages venv_name + source venv_name/bin/activate + pip install -r requirements.txt # any other deps + +NOTES + http://www.trilon.com/xv/ (gives 404 as of 6/20/2026) + https://itsfoss.community/t/resurrecting-xv-the-original-linux-image-viewer/12426 + https://github.com/nevillejackson/Unix/tree/main/xv """ import argparse +import os import sys import tkinter -import PIL.Image -import PIL.ImageTk +from PIL import Image, ImageTk, UnidentifiedImageError class Window(tkinter.Tk): def __init__(self, master=None): tkinter.Tk.__init__(self, master) self.bind('', self.escape) + self.bind('', self.escape) # classic quit + self.bind('', self.toggle_fullscreen) - def escape(self, event): + def escape(self, event=None): self.withdraw() - sys.exit() + sys.exit(0) + + def toggle_fullscreen(self, event=None): + self.attributes('-fullscreen', not self.attributes('-fullscreen')) class ImageDisplay(Window): def __init__(self, filename, master=None): Window.__init__(self, master) - self.title('xv') + self.title(f'xv - {os.path.basename(filename)}') self.configure(background='grey') + try: - image = PIL.Image.open(filename) - except: - print('ERROR: IOError - Could not open %s' % (filename)) + image = Image.open(filename) + except (UnidentifiedImageError, FileNotFoundError, PermissionError) as e: + print(f'ERROR: Could not open {filename}: {e}') + sys.exit(1) + except Exception as e: # fallback for anything else + print(f'ERROR: Unexpected error opening {filename}: {e}') + sys.exit(1) + + width, height = image.size + MAX_DIM = 1024 + + if width > MAX_DIM or height > MAX_DIM: + scale = MAX_DIM / float(max(width, height)) + scaled_width = int(width * scale) + scaled_height = int(height * scale) + + # Compatibility shim for Pillow >= 10 (ANTIALIAS -> LANCZOS) + try: + resample = Image.Resampling.LANCZOS + except AttributeError: + resample = Image.LANCZOS # older Pillow + + image = image.resize((scaled_width, scaled_height), resample) else: - width, height = image.size - scale = 1.0 - if width > 1024: - scale = 1024 / float(width) + scaled_width, scaled_height = width, height - scaled_width = int((scale * float(width))) - scaled_height = int((scale * float(height))) - image = image.resize((scaled_width, scaled_height), PIL.Image.ANTIALIAS) + # Keep a strong reference to the PhotoImage instance. + # (Otherwise the label's image would disappear + # when the PhotoImage assigned to it goes out + # of scope.) + self._photo_image = ImageTk.PhotoImage(image) - # Keep a reference to the PhotoImage instance. - # (Otherwise the label's image would disappear - # when the PhotoImage assigned to it goes out - # of scope.) - self._photo_image = PIL.ImageTk.PhotoImage(image) + label = tkinter.Label(self, image=self._photo_image, bg='grey') + label.pack(side='bottom', fill='both', expand='yes') - label = tkinter.Label(self, image=self._photo_image) - label.pack(side='bottom', fill='both', expand='yes') + # Size the window to the (possibly scaled) image + self.geometry(f"{scaled_width}x{scaled_height}") def main(args): @@ -67,8 +101,28 @@ def main(args): image_display.mainloop() return 0 + if __name__ == '__main__': - parser = argparse.ArgumentParser(description='A simple image viewer and a tribute to the original xv image display editing program for the X Window System.') + parser = argparse.ArgumentParser( + description="""DESCRIPTION + A simple image viewer and a tribute to the original xv image display editing program for the X Window System.""", + epilog="""REQUIREMENTS + One-time system setup: + sudo apt update + sudo apt install python3-tk python3-pil python3-pil.imagetk + + To use this application in a venv, you'll have to bring in + these system packages when you create the venv: + python3 -m venv --system-site-packages venv_name + source venv_name/bin/activate + pip install -r requirements.txt # any other deps + +NOTES + http://www.trilon.com/xv/ (gives 404 as of 6/20/2026) + https://itsfoss.community/t/resurrecting-xv-the-original-linux-image-viewer/12426 + https://github.com/nevillejackson/Unix/tree/main/xv""", + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument( 'filename', help='Image file to display.') diff --git a/python/doc/venv.md b/python/doc/venv.md index 6daf04d..e877b38 100644 --- a/python/doc/venv.md +++ b/python/doc/venv.md @@ -1,5 +1,9 @@ # Python Virtual Environment +# BASIC + +This is the basic workflow for using a virtual environment: + ```bash mkdir ./venv python -m venv venv @@ -9,4 +13,24 @@ pip install -r requirements.txt . . deactivate -``` \ No newline at end of file +``` + +# ADVANCED + +You may find it necessary to install some Python packages at the system level. + +`tkinter` is one such example. `Pillow` is one you can install via `pip` but you'll probably want to do all these at once: + +```bash +sudo apt update +sudo apt install python3-tk python3-pil python3-pil.imagetk +``` + +The problem is that site packages are not visible to a `venv`. Therefore, you'll have to bring them into your `venv` when you create it via the `--system-site-packages` option. + +```bash +python3 -m venv --system-site-packages venv_name +source venv_name/bin/activate +pip install -r requirements.txt # any other deps +``` +