Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 30 additions & 15 deletions python/apps/pclock.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -46,6 +47,7 @@ def __init__(self, master=None):
self._offsety = 0
self.bind('<Button-1>', self.button1)
self.bind('<B1-Motion>', self.b1motion)
self.bind('<q>', self.escape)
self.bind('<Escape>', self.escape)

def button1(self, event):
Expand All @@ -59,7 +61,7 @@ def b1motion(self, event):

def escape(self, event):
self.withdraw()
sys.exit()
sys.exit(0)


class Clock(FloatingBorderlessWindow):
Expand Down Expand Up @@ -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))
100 changes: 100 additions & 0 deletions python/apps/refresh_help.py
Original file line number Diff line number Diff line change
@@ -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()
112 changes: 83 additions & 29 deletions python/apps/xv.py
Original file line number Diff line number Diff line change
@@ -1,74 +1,128 @@
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.

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('<Escape>', self.escape)
self.bind('<q>', self.escape) # classic quit
self.bind('<f>', 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):
image_display = ImageDisplay(args.filename)
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.')
Expand Down
26 changes: 25 additions & 1 deletion python/doc/venv.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -9,4 +13,24 @@ pip install -r requirements.txt
.
.
deactivate
```
```

# 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
```

Loading