diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 41e66ca..ee6733a 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -25,11 +25,11 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v7
+ uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
- uses: github/codeql-action/init@v4.37.3
+ uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -43,7 +43,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
- uses: github/codeql-action/autobuild@v4.37.3
+ uses: github/codeql-action/autobuild@v3
# βΉοΈ Command-line programs to run using the OS shell.
# π See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -56,4 +56,4 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v4.37.3
+ uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/docs-lint.yml b/.github/workflows/docs-lint.yml
deleted file mode 100644
index 8f97869..0000000
--- a/.github/workflows/docs-lint.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-# Documentation CI for this repository.
-#
-# Runs on pull requests: markdown style, a strict MkDocs build, and an
-# external link check. Never deploys.
-
-name: Docs Lint
-
-on:
- pull_request:
- paths:
- - 'docs/**'
- - 'overrides/**'
- - 'mkdocs.yml'
- - '.github/workflows/docs-lint.yml'
- workflow_dispatch:
-
-permissions:
- contents: read
-
-jobs:
- lint:
- uses: willtheorangeguy/mkdocs/.github/workflows/docs-lint.yml@main
- # Repos with many third-party links can turn the link checker off:
- # with:
- # check_links: false
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
deleted file mode 100644
index c3c4b06..0000000
--- a/.github/workflows/docs.yml
+++ /dev/null
@@ -1,50 +0,0 @@
-# Documentation site for this repository.
-#
-# All build logic lives in willtheorangeguy/mkdocs. This file only says
-# "build my docs" and grants the permissions the reusable workflow needs.
-# Do not add build steps here β change the template repository instead.
-
-name: Docs
-
-on:
- push:
- # Covers both branch conventions without per-repo editing.
- branches: [main, master]
- paths:
- - 'docs/**'
- - 'overrides/**'
- - 'mkdocs.yml'
- - '.github/workflows/docs.yml'
- - 'CHANGELOG.md'
- - 'CONTRIBUTING.md'
- - 'CODE_OF_CONDUCT.md'
- - 'SECURITY.md'
- - 'LICENSE.md'
- workflow_dispatch:
-
-# A reusable workflow cannot grant its own permissions. These three are
-# required; the deploy step fails with a confusing OIDC error without them.
-permissions:
- contents: read
- pages: write
- id-token: write
-
-# Never cancel a deployment mid-flight β a half-published site is worse
-# than a slightly stale one.
-concurrency:
- group: pages
- cancel-in-progress: false
-
-jobs:
- docs:
- uses: willtheorangeguy/mkdocs/.github/workflows/docs-build.yml@main
-
- # ---------------------------------------------------------------------
- # ONLY for repos that already serve an application at the Pages root.
- # Uncomment to publish the app at / and the docs at /docs/.
- #
- # You must also DELETE the repo's existing Pages workflow. Two workflows
- # deploying to Pages will fight over the deployment and one will fail.
- # ---------------------------------------------------------------------
- # with:
- # docs_subpath: docs
diff --git a/.github/workflows/push-to-pypi.yml b/.github/workflows/push-to-pypi.yml
index 8f0aacd..62c9b1d 100644
--- a/.github/workflows/push-to-pypi.yml
+++ b/.github/workflows/push-to-pypi.yml
@@ -13,9 +13,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@v6
- name: Set up Python
- uses: actions/setup-python@v7
+ uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Install dependencies
diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
index 3f05071..54dfae3 100644
--- a/.github/workflows/pylint.yml
+++ b/.github/workflows/pylint.yml
@@ -14,9 +14,9 @@ jobs:
matrix:
python-version: ["3.9"]
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v7
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 467d536..1ede2ba 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -16,13 +16,13 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
- python-version: ["3.10", "3.11", "3.12"]
+ python-version: ["3.9", "3.10", "3.11", "3.12"]
steps:
- - uses: actions/checkout@v7
+ - uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v7
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
@@ -53,12 +53,12 @@ jobs:
- name: Run tests (Linux)
if: runner.os == 'Linux'
run: |
- xvfb-run -a python -m pytest tests/ -v --cov=. --cov-report=xml --cov-report=term
+ xvfb-run -a python -m pytest tests/ -v --cov=programver --cov-report=xml --cov-report=term
- name: Run tests (Windows/macOS)
if: runner.os != 'Linux'
run: |
- python -m pytest tests/ -v --cov=. --cov-report=xml --cov-report=term
+ python -m pytest tests/ -v --cov=programver --cov-report=xml --cov-report=term
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v7
diff --git a/.gitignore b/.gitignore
index 5465507..894a44c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -102,26 +102,3 @@ venv.bak/
# mypy
.mypy_cache/
-.bandit/
-.recall/
-
-# Append these lines to the target repository's .gitignore.
-#
-# All three are generated at build time from the template repository. If any
-# of them gets committed, a stale copy will start overriding the shared
-# version and the site will silently drift from the rest.
-
-# --- Documentation build artifacts ---
-site/
-.mkdocs-shared/
-docs/stylesheets/
-docs/javascript/
-docs/images/favicon.svg
-
-# Theme overrides staged from the template repository. Staging is
-# no-clobber, so a committed copy would freeze this repo on an old version
-# of a shared partial. To genuinely override one for this repo, remove its
-# line here and commit the file deliberately.
-overrides/main.html
-overrides/partials/
-overrides/.icons/
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index ae4b3c8..0000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1,107 +0,0 @@
-# CLAUDE.md
-
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
-
-## Project Overview
-
-ProgramVer is a Python/tkinter GUI app that replicates Microsoft's `winver` β it displays a customizable window with program version info, copyright notices, and buttons to open a License or EULA file in a secondary window. It is published to PyPI as `programver` and is designed to be forked and customized per-program. Current version: **1.9.0**.
-
-## Commands
-
-### Install dependencies
-```bash
-pip install -r requirements.txt
-```
-
-### Run all tests
-```bash
-# Linux (requires xvfb for headless tkinter)
-xvfb-run -a python -m pytest tests/ -v
-
-# Windows / macOS
-python -m pytest tests/ -v
-```
-
-### Run a single test
-```bash
-# Linux
-xvfb-run -a python -m pytest tests/test_main.py::TestClassName::test_name -v
-
-# Windows / macOS
-python -m pytest tests/test_main.py::TestClassName::test_name -v
-```
-
-### Run tests with coverage
-```bash
-xvfb-run -a python -m pytest tests/ --cov=. --cov-report=term-missing
-```
-
-### Lint
-```bash
-pylint $(git ls-files '*.py')
-```
-
-## Architecture
-
-All application logic lives in a single module: **`main.py`**. It exposes four functions:
-
-- `get_resource_path(filename)` β resolves paths relative to the module file (needed for PyPI installs where the CWD may differ from the package location).
-- `ProgramVer()` β builds and runs the main tkinter window: logo images, version/copyright labels, and two buttons. Calls `window.mainloop()` so it blocks until the window is closed.
-- `openLicense()` β opens `LICENSE.txt` in a new `Tk()` window.
-- `openEULA()` β opens `EULA.txt` in a new `Tk()` window.
-
-### Entry points
-
-- `__main__.py` β calls `ProgramVer()`, enabling `python -m programver`.
-- `__init__.py` β declares `__all__ = ["main"]` for PyPI packaging.
-- `setup.cfg` / `pyproject.toml` / `setup.py` β all register the `programver` console script pointing at `main:ProgramVer`.
-
-### Key files
-
-| Path | Purpose |
-|------|---------|
-| `main.py` | All application logic |
-| `tests/test_main.py` | Unit tests (mocked tkinter) |
-| `imgs/` | Image assets (`dfdlogo.gif`, `pythonpoweredlengthgif.gif`) |
-| `LICENSE.txt` | License text displayed at runtime by `openLicense()` |
-| `EULA.txt` | EULA text displayed at runtime by `openEULA()` |
-| `pytest.ini` | Pytest configuration (testpaths, addopts) |
-| `.deepsource.toml` | DeepSource static analysis config (uses `black` formatter) |
-
-**Customization intent:** The strings inside `ProgramVer()` (window title, version label, trademark text, license blurb) and the image files in `imgs/` are expected to be replaced when the project is forked. `LICENSE.txt` and `EULA.txt` in the repo root are the files opened at runtime.
-
-## Testing
-
-Tests are in `tests/test_main.py` using `unittest.TestCase` with five test classes:
-
-- `TestGetResourcePath` β path resolution helper
-- `TestOpenLicense` β license window creation and content display
-- `TestOpenEULA` β EULA window creation and content display
-- `TestProgramVer` β main window components (images, labels, buttons, commands)
-- `TestModuleIntegration` β import and callable checks
-
-All tkinter calls are mocked with `unittest.mock.patch` so tests run headlessly.
-
-### CI Workflows (`.github/workflows/`)
-
-| Workflow | Trigger | What it does |
-|----------|---------|--------------|
-| `tests.yml` | push/PR to `master` | Runs pytest across Ubuntu/Windows/macOS x Python 3.9-3.12; uploads coverage to Codecov |
-| `pylint.yml` | any push | Runs pylint on all `.py` files (Python 3.9) |
-| `codeql-analysis.yml` | push/PR to `master`, weekly schedule | CodeQL security scanning |
-| `push-to-pypi.yml` | GitHub release published | Builds and publishes to PyPI |
-
-The default branch is `master`.
-
-## Coding Conventions
-
-- 4-space indentation (no tabs).
-- Semantic Versioning for releases.
-- Version number appears in **four places** β update all on a version bump:
- 1. `main.py` (the `info` label text)
- 2. `pyproject.toml` (`[project] version`)
- 3. `setup.cfg` (`[metadata] version`)
- 4. `setup.py` (`version` kwarg)
-- The `# pylint: disable=import-error, invalid-name` comments at the top of `main.py`, `__main__.py`, `__init__.py`, and `test_main.py` are intentional β do not remove them.
-- `test_main.py` also disables `wrong-import-position`, `import-outside-toplevel`, and `unused-argument` β do not remove these either.
-- Black is configured as the code formatter via `.deepsource.toml`.
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..619fe91
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,121 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at the project's
+[Discord](https://discord.gg/x3G8adwVUe).
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
+version 2.0.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..8b9f716
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,59 @@
+# Contributing to ProgramVer
+
+π Thanks for taking the time to contribute! π
+
+The following is a set of guidelines for contributing to the ProgramVer project on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
+
+## Code of Conduct
+
+This project and everyone participating in it is governed by the [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to our [Discord](https://discord.gg/x3G8adwVUe).
+
+## New Contributor Guide
+
+To get an overview of the project, read the [README](README.md). Here are some resources to help you get started with open source contributions:
+
+- [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github).
+- [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git).
+- [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow).
+- [Collaborating with pull requests](https://docs.github.com/en/github/collaborating-with-pull-requests).
+
+Please contribute using [GitHub Flow](https://guides.github.com/introduction/flow). Create a branch, add commits, and [open a pull request](https://github.com/willtheorangeguy/ProgramVer/compare).
+
+## How to Contribute
+
+### Did you find a bug?
+
+- **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/willtheorangeguy/ProgramVer/issues).
+- If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/willtheorangeguy/ProgramVer/issues/new/choose). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring.
+- Use the issue templates to create the issue. They include step by step guidelines and have lists of things that should be included.
+
+### Did you write a patch that fixes a bug or updates the documentation?
+
+- Open a new [Pull Request](https://github.com/willtheorangeguy/ProgramVer/compare) on GitHub with the patch.
+- Ensure the pull request description clearly describes the problem and solution. Be sure to include a **title**, and as much relevant information as possible. Include the relevant issue number if applicable.
+
+### Did you fix whitespace, format code, or make a purely cosmetic patch?
+
+- Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of ProgramVer are still accepted! Just follow the same instructions as above.
+
+### Do you intend to add a new feature or change an existing one?
+
+- **Ensure the feature was not already introduced** by searching on GitHub under [Issues](https://github.com/willtheorangeguy/ProgramVer/issues). Look for those under the [enhancement](https://github.com/willtheorangeguy/ProgramVer/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) tag.
+- If you're unable to find an open issue addressing the new feature, [open a new one](https://github.com/willtheorangeguy/ProgramVer/issues/new/choose)!
+
+### Do you have questions about the source code?
+
+- Ask any question about how to use ProgramVer on our [Discord](https://discord.gg/x3G8adwVUe).
+- If it does not get answered, open an [issue](https://github.com/willtheorangeguy/ProgramVer/issues/new/choose).
+
+## Coding Conventions
+
+Start reading our code and you'll get the hang of it.
+
+- We indent using four spaces (NOT tabs).
+- Every function or piece of code should be heavily commented.
+- We use [Semantic Versioning](https://semver.org/).
+
+## Attribution
+
+This document is adapted from the Contributing documents of [Rails](https://github.com/rails/rails/blob/main/CONTRIBUTING.md), the [GitHub Docs](https://github.com/github/docs/blob/main/CONTRIBUTING.md), [Atom](https://github.com/atom/atom/blob/master/CONTRIBUTING.md), and [OpenGovernment](https://github.com/opengovernment/opengovernment/blob/master/CONTRIBUTING.md?plain=1).
diff --git a/EULA.md b/EULA.md
new file mode 100644
index 0000000..249e753
--- /dev/null
+++ b/EULA.md
@@ -0,0 +1,32 @@
+# End-User License Agreement (EULA)
+
+**ProgramVer**
+**Effective Date: 2026-01-01**
+
+By installing, copying, or otherwise using ProgramVer, you agree to the terms of this End-User License Agreement.
+
+## 1. License Grant
+
+Subject to the terms of this agreement and the MIT License, you are granted a non-exclusive, worldwide, royalty-free license to use, copy, modify, and distribute ProgramVer.
+
+## 2. Restrictions
+
+You may not:
+- Remove or alter any copyright notices or license text included with the software.
+- Misrepresent the origin of the software or claim authorship of the original work.
+
+## 3. Disclaimer of Warranty
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. THE AUTHORS ARE NOT LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE.
+
+## 4. Termination
+
+This agreement is effective until terminated. Your rights under this agreement will terminate automatically if you fail to comply with any of its terms.
+
+## 5. Governing Law
+
+This agreement shall be governed by the laws of Canada.
+
+---
+
+Copyright (C) 2017-2026 Dog Face Development Co.
diff --git a/LICENSE.md b/LICENSE.md
index fef2957..83011c3 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -2,8 +2,8 @@
Copyright Β© 2026 willtheorangeguy
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the βSoftwareβ), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+THE SOFTWARE IS PROVIDED βAS ISβ, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/MANIFEST.in b/MANIFEST.in
index 7ead327..eb4d3a0 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,2 +1 @@
-include imgs/dfdlogo.gif
-include imgs/pythonpoweredlengthgif.gif
\ No newline at end of file
+recursive-include programver/imgs *.gif *.png
diff --git a/README.md b/README.md
index fda37d3..9feb043 100644
--- a/README.md
+++ b/README.md
@@ -1,93 +1,153 @@
-
+
ProgramVer
-A Python version of Microsoft's winver β a copyright and licence window you drop into your own program.
+A Python version of Microsoft's 'winver', built to be customizable, and to show copyright info and licenses.
+

+

+

+

+

+

+

+
+

+
+

+
+
- Status β’
Key Features β’
- Installation β’
- Usage β’
- Documentation β’
+ Download β’
+ How To Use β’
Support β’
Contributing β’
- Credits β’
- License
+ Changelog β’
+ Credits & Contributors
-
+
-

+
-## Status
+## Key Features
-**Currently broken as shipped.** `ProgramVer()` loads `imgs/dfdlogo.gif`, which is not in the repository, so the window fails before it appears. The two buttons read `LICENSE.txt` and `EULA.txt`, neither of which exists either.
+* Display a version window.
+* Can be called and imported as a function.
+* Links to License text.
+* Links to EULA text.
+* Includes logo and Python Powered images.
+* Cross platform.
-The test suite passes β it mocks every file access and every widget β so CI is green and the program still cannot start. Details and suggested fixes are in [`docs/internal/known-issues.md`](docs/internal/known-issues.md).
+## Download
-The template is sound and the customisation points are real; it needs its assets back.
+You can **[download](https://github.com/willtheorangeguy/ProgramVer/releases/latest) the source code** to run the scripts from the command line on Windows, macOS and Linux. **This will require [Python](https://www.python.org/downloads/).**
-## Key Features
+You can **[download](https://github.com/willtheorangeguy/ProgramVer/releases/latest) the latest executable version** of ProgramVer for Windows. **This does not require Python.**
+
+## How To Use
+
+To run the application, you can use [Git and the Python Interpreter](https://github.com/willtheorangeguy/ProgramVer/main/README.md#git), which allows you to clone and run the application, or [`pip`](https://github.com/willtheorangeguy/ProgramVer/main/README.md#pip) to create a command line application.
-- A `winver`-style window: logo, program name and version, trademark notice, licence blurb.
-- **Open License** and **Open EULA** buttons that display the full text in their own windows.
-- Importable as a function, so you can wire it to your own program's About menu.
-- Python-Powered badge included.
-- Pure standard library β Tkinter only.
-- Cross-platform.
+### Git
-## Installation
+To clone and run this application, you'll need [Git](https://git-scm.com/downloads) and [Python](https://www.python.org/downloads/) installed on your computer. If you would rather not use Git, you can just download the script from GitHub above. From your command line:
```bash
-git clone https://github.com/willtheorangeguy/ProgramVer
-cd ProgramVer
-python main.py
+# Clone this repository
+$ git clone https://github.com/willtheorangeguy/ProgramVer
+
+# Go into the repository
+$ cd ProgramVer
+
+# Run the CLI
+$ python main.py
```
-See [`docs/installation.md`](docs/installation.md), including what you need to supply before it runs.
+### `pip`
-## Usage
+You can install the program from the [Python Package Index](https://pypi.org/project/programver/) through `pip`.
-```python
-from main import ProgramVer
-ProgramVer()
-```
+```bash
+# Install via pip
+$ pip install programver
-Every string in the window is meant to be edited for your project β see [`docs/configuration.md`](docs/configuration.md).
+# Run the CLI
+$ programver
+```
-## Documentation
+However, you may want to add the version window to your program. To do so, follow these steps:
-Full documentation lives in [`docs/`](docs/index.md):
-[Quickstart](docs/quickstart.md) Β· [Installation](docs/installation.md) Β· [Configuration](docs/configuration.md) Β· [Architecture](docs/architecture.md) Β· [Development](docs/development.md) Β· [Testing](docs/testing.md) Β· [FAQ](docs/faq.md) Β· [Troubleshooting](docs/troubleshooting.md) Β· [Roadmap](docs/roadmap.md)
+1. Download the latest source code release from [GitHub Releases](https://github.com/willtheorangeguy/ProgramVer/releases/latest) page.
+2. Extract the source code files using a program like [7-Zip](https://www.7-zip.org/).
+3. Copy the `ProgramVer.py` file to your project's main directory.
+4. Import ProgramVer by adding `from ProgramVer import *` to your Python `import` statements.
+5. Call ProgramVer through the `ProgramVer()` function.
+6. Enjoy your new version window!
## Support
-Open a [GitHub Discussion](https://github.com/willtheorangeguy/ProgramVer/discussions), file an [issue](https://github.com/willtheorangeguy/ProgramVer/issues/new/choose), or join the [Discord](https://discord.gg/x3G8adwVUe).
+Customization for ProgramVer can be found in the [`CUSTOMIZATION`](https://github.com/willtheorangeguy/ProgramVer/blob/master/docs/CUSTOMIZATION.md) doc. More documentation is available in the **[Documentation](https://github.com/willtheorangeguy/ProgramVer/tree/master/docs)** and on the **[Wiki](https://github.com/willtheorangeguy/ProgramVer/wiki)**. If more support is required, please open a **[GitHub Discussion](https://github.com/willtheorangeguy/ProgramVer/discussions)** or join our **[Discord](https://discord.gg/x3G8adwVUe)**.
+
+## Testing
+
+ProgramVer includes a comprehensive test suite to ensure code quality and reliability. The test suite achieves 100% code coverage for the main module.
+
+### Running Tests
+
+To run the test suite locally:
+
+```bash
+# Install test dependencies
+pip install -r requirements.txt
+
+# Run tests (Linux)
+xvfb-run -a python -m pytest tests/ -v
+
+# Run tests (Windows/macOS)
+python -m pytest tests/ -v
+
+# Run tests with coverage
+python -m pytest tests/ --cov=main --cov-report=term-missing
+```
+
+For more information about testing, see the [tests README](tests/README.md).
+
+### Continuous Integration
+
+Tests are automatically run on GitHub Actions for every push and pull request across:
+
+- Operating Systems: Ubuntu, Windows, and macOS
+- Python Versions: 3.9, 3.10, 3.11, and 3.12
## Contributing
Please contribute using [GitHub Flow](https://guides.github.com/introduction/flow). Create a branch, add commits, and [open a pull request](https://github.com/willtheorangeguy/ProgramVer/compare).
-See the org-wide [Contributing Guide](https://github.com/willtheorangeguy/.github/blob/main/CONTRIBUTING.md) and [Code of Conduct](https://github.com/willtheorangeguy/.github/blob/main/CODE_OF_CONDUCT.md).
+Please read [`CONTRIBUTING`](CONTRIBUTING.md) for details on our [`CODE OF CONDUCT`](CODE_OF_CONDUCT.md), and the process for submitting pull requests to us.
+
+## Changelog
+
+See the [`CHANGELOG`](CHANGELOG.md) file for details.
## Credits
@@ -112,10 +172,16 @@ This software uses the following open source packages, projects, services or web
-Sponsor [@willtheorangeguy](https://github.com/willtheorangeguy) on [PayPal](https://paypal.me/wvdg44?country.x=CA&locale.x=en_US).
+## Contributors
-## License
+* [@willtheorangeguy](https://github.com/willtheorangeguy) - Sponsor on [PayPal](https://paypal.me/wvdg44?country.x=CA&locale.x=en_US)
-MIT β see [`LICENSE.md`](LICENSE.md).
+## You may also like...
+
+* [PyWorkout](https://github.com/willtheorangeguy/PyWorkout) - A minimal CLI to keep you inspired during your workout!
+* [PyAvatar](https://github.com/willtheorangeguy/PyAvatar) - Easily display all of your creative avatars to keep them consistent across websites.
+* [Periodic Table Info](https://github.com/willtheorangeguy/Periodic-Table-Info) - Print all the elements in the Periodic Table of the Elements, with an interactive prompt to learn more.
+
+## License
-> Note the window itself currently displays a GPL blurb and a different copyright holder. That text is placeholder content meant to be replaced per project, but it does not match this repository's own licence β see [`docs/internal/known-issues.md`](docs/internal/known-issues.md).
+This project is licensed under the [MIT License](https://mit-license.org/) - see the [`LICENSE`](LICENSE.md) file for details.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..704e5b9
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,29 @@
+# Security Policy
+
+The security of our software products and services is serious, which includes all source code repositories managed by [willtheorangeguy](https://github.com/orgs/willtheorangeguy).
+
+If you believe you have found a security vulnerability in any repository, please report it to us as described below in [Reporting a Vulnerability](#reporting-a-vulnerability).
+
+## Supported Versions
+
+The following versions of ProgramVer are currently supported:
+
+| Version | Supported |
+| ------- | ------------------ |
+| `1.x.x` | :white_check_mark: |
+| `0.x.x` | :x: |
+
+## Reporting a Vulnerability
+
+Please open an [issue](https://github.com/willtheorangeguy/ProgramVer/issues/new/choose) to report a security vulnerability. Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
+
+* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
+* Full paths of source file(s) related to the manifestation of the issue
+* The location of the affected source code (tag/branch/commit or direct URL)
+* Any special configuration required to reproduce the issue
+* Step-by-step instructions to reproduce the issue
+* Proof-of-concept or exploit code (if possible)
+* Impact of the issue, including how an attacker might exploit the issue
+* This information will help us triage your report more quickly.
+
+Contributions should follow the [`CODE_OF_CONDUCT`](CODE_OF_CONDUCT.md) and [`CONTRIBUTING`](CONTRIBUTING.md) documents.
diff --git a/__init__.py b/__init__.py
deleted file mode 100644
index 7abf536..0000000
--- a/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""Initialize PyPI Package"""
-
-# pylint: disable=import-error, invalid-name
-
-__all__ = ["main"]
diff --git a/__main__.py b/__main__.py
deleted file mode 100644
index 3677f08..0000000
--- a/__main__.py
+++ /dev/null
@@ -1,8 +0,0 @@
-"""Main entry point for the application."""
-
-# pylint: disable=import-error, invalid-name
-
-from main import ProgramVer
-
-if __name__ == "__main__":
- ProgramVer()
diff --git a/docs/CUSTOMIZATION.md b/docs/CUSTOMIZATION.md
new file mode 100644
index 0000000..a768e47
--- /dev/null
+++ b/docs/CUSTOMIZATION.md
@@ -0,0 +1,56 @@
+# ProgramVer Customization
+
+ProgramVer is designed to be highly customizable, in order to make the version window suit each individual project. The license and EULA texts can be changed, as well as the program name, version number and more.
+
+All of these instructions require [a text editor](https://code.visualstudio.com/) to be installed.
+
+## Set the License File and Text
+
+The version window can be configured to use a custom license file and text.
+
+1. _Line 11_: Replace the `'LICENSE.txt'` with the name (or path) to the license file.
+2. _Line 40_: Replace the text in between the single quotes (`'...'`) with the trademark/license blurb. For example, here is the one from `winver.exe`:
+
+```text
+Β© Microsoft Corporation. All rights reserved.
+```
+
+3. _Line 41_: Replace the text in between the triple double quotes (`"""..."""`) with the license blurb, if your license includes this. For example, the GNU Public License includes a section of text that should be included with each program:
+
+```text
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+```
+
+## Set the EULA File
+
+The version window can be configured to use a custom EULA file.
+
+1. _Line 21_: Replace the `'EULA.txt'` with the name (or path) to the End User License Agreement file.
+
+## Set the Window Title
+
+The version window can be configured to have a custom title.
+
+1. _Line 33_: Replace the `'Copyright & Version Info for ProgramVer'` with the name of the project ProgramVer is being added to.
+2. _Line 39_: Replace the `'ProgramVer \n Version: 1.8.0 (Build 1080)'` with the program name and the version number.
+
+## Set the Images
+
+The version window can be configured to show a company logo and the Python Powered logo.
+
+1. _Line 35_: Replace the `'dfdlogo.gif'` with the name (or path) of the company logo image.
+2. _Line 36_: Replace the `'pythonpoweredlengthgif.gif'` with the name (or path) to another Python Powered image, or any other image that fits.
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..9c2185a
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,12 @@
+# ProgramVer Documentation
+
+This folder includes all of the docs for ProgramVer. More can be found on the [wiki](https://github.com/willtheorangeguy/ProgramVer/wiki).
+
+```text
+βββ docs
+| βββ images
+| βββ CUSTOMIZATION.md
+| βββ TESTING.md
+| βββ USAGE.md
+βββ README.md
+```
diff --git a/docs/TESTING.md b/docs/TESTING.md
new file mode 100644
index 0000000..4b17237
--- /dev/null
+++ b/docs/TESTING.md
@@ -0,0 +1,100 @@
+# ProgramVer Test Suite
+
+This directory contains the comprehensive test suite for ProgramVer.
+
+## Running Tests
+
+### Prerequisites
+
+Install the required testing dependencies:
+
+```bash
+pip install -r requirements.txt
+```
+
+On Linux systems, you'll also need to install tkinter and xvfb for headless GUI testing:
+
+```bash
+sudo apt-get install python3-tk xvfb
+```
+
+### Running All Tests
+
+To run all tests:
+
+```bash
+# On Linux (headless environment)
+xvfb-run -a python -m pytest tests/ -v
+
+# On Windows/macOS (with display)
+python -m pytest tests/ -v
+```
+
+### Running Tests with Coverage
+
+To run tests with coverage report:
+
+```bash
+# On Linux
+xvfb-run -a python -m pytest tests/ --cov=. --cov-report=term-missing --cov-report=html
+
+# On Windows/macOS
+python -m pytest tests/ --cov=. --cov-report=term-missing --cov-report=html
+```
+
+The HTML coverage report will be generated in the `htmlcov` directory.
+
+### Running Specific Tests
+
+To run a specific test file:
+
+```bash
+xvfb-run -a python -m pytest tests/test_main.py -v
+```
+
+To run a specific test class:
+
+```bash
+xvfb-run -a python -m pytest tests/test_main.py::TestOpenLicense -v
+```
+
+To run a specific test method:
+
+```bash
+xvfb-run -a python -m pytest tests/test_main.py::TestOpenLicense::test_openLicense_creates_window -v
+```
+
+## Test Structure
+
+The test suite is organized as follows:
+
+- `test_main.py` - Tests for the main ProgramVer module
+ - `TestOpenLicense` - Tests for the openLicense function
+ - `TestOpenEULA` - Tests for the openEULA function
+ - `TestProgramVer` - Tests for the ProgramVer main function
+ - `TestModuleIntegration` - Integration tests for the module
+
+## GitHub Actions Integration
+
+The test suite is automatically run on GitHub Actions for every push and pull request. The workflow:
+
+- Runs on Ubuntu, Windows, and macOS
+- Tests against Python 3.9, 3.10, 3.11, and 3.12
+- Generates coverage reports
+- Uploads coverage to Codecov (for master branch)
+
+See `.github/workflows/tests.yml` for the complete configuration.
+
+## Writing New Tests
+
+When adding new features to ProgramVer, please add corresponding tests following these guidelines:
+
+1. Create test classes that inherit from `unittest.TestCase`
+2. Use descriptive test method names that start with `test_`
+3. Use mocking for GUI components to avoid requiring a display
+4. Add docstrings to explain what each test verifies
+5. Ensure tests are independent and can run in any order
+
+## Coverage Goals
+
+We aim to maintain at least 90% code coverage for the main module. Currently, we have 100% coverage for `main.py`.
diff --git a/docs/USAGE.md b/docs/USAGE.md
new file mode 100644
index 0000000..c802899
--- /dev/null
+++ b/docs/USAGE.md
@@ -0,0 +1,26 @@
+# ProgramVer Usage
+
+To install ProgramVer, you can use an executable package (Windows), run the scripts through Python (Windows, macOS, Linux), or install from the [Python Package Index](https://pypi.org/).
+
+## Executable Package
+
+1. To run the executable package, download the latest `.zip` file from [GitHub Releases](https://github.com/willtheorangeguy/ProgramVer/releases/latest) page.
+2. Extract the `.zip` file using a program like [7-Zip](https://www.7-zip.org/).
+3. _(Optional) Move the files to `C:\Program Files` and create a shortcut._
+4. Double click on `send.exe`.
+5. Enjoy the program!
+
+## Python Script
+
+1. To run the Python script, download the latest source code release from [GitHub Releases](https://github.com/willtheorangeguy/ProgramVer/releases/latest) page.
+2. Download and install [Python](https://www.python.org/downloads/).
+3. Extract the source code files using a program like [7-Zip](https://www.7-zip.org/).
+4. Double click on `send.py`, or right-click and open with IDLE and press `F5`.
+5. Enjoy the program!
+
+## Python Package Index (`pip`)
+
+1. Download and install [Python](https://www.python.org/downloads/).
+2. Open a terminal and run the command: `pip install lego-block-creator`.
+3. Start the program by running the command: `lego-block-creator`.
+4. Enjoy the program!
diff --git a/docs/architecture.md b/docs/architecture.md
deleted file mode 100644
index ee57001..0000000
--- a/docs/architecture.md
+++ /dev/null
@@ -1,69 +0,0 @@
-# ProgramVer β Architecture
-
-One module, three functions, no dependencies.
-
-```text
-main.py
-βββ get_resource_path(filename) resolve against __file__
-βββ openLicense() read LICENSE.txt β its own window
-βββ openEULA() read EULA.txt β its own window
-βββ ProgramVer() the main window, then mainloop()
-```
-
-## `get_resource_path`
-
-```python
-base_dir = os.path.dirname(os.path.abspath(__file__))
-return os.path.join(base_dir, filename)
-```
-
-Resolves against the **module**, not the working directory, so the window works wherever it is
-launched from and survives being copied into another project. Used for both images and both text
-files.
-
-## `ProgramVer()`
-
-Builds the window and calls `mainloop()` itself, so it **blocks** until closed. That makes it a
-drop-in call from a menu handler, and it means calling it from an existing Tkinter application
-starts a second event loop β see below.
-
-Widgets, top to bottom: logo, name and version, trademark notice, licence blurb, the two
-buttons, and the Python-Powered badge at the bottom.
-
-Note that neither `PhotoImage` is bound to a lasting reference. They survive because they are
-locals of a function that blocks in `mainloop()` β if `ProgramVer()` were refactored to return
-the window instead, both images would be garbage-collected and the labels would render blank
-with no error.
-
-## `openLicense` and `openEULA`
-
-Identical in shape: construct a `Tk()`, read a text file, insert it into a `Text` widget, pack.
-
-Two things follow. Neither calls `mainloop()` β they rely on the main window's loop, which works
-because they are invoked from a button callback inside it. And both construct a second `Tk()`
-root rather than a `Toplevel()`; Tkinter supports one true root per process, so closing the
-wrong window can take the others with it.
-
-Neither file is present in the repository. See
-[`internal/known-issues.md`](./internal/known-issues.md).
-
-## Why it is a template, not a library
-
-Every displayed string is a literal, each marked `# change as needed`. There is no parameter, no
-config object, and no settings file. The intended workflow is copying `main.py` into your project
-and editing it.
-
-That is a reasonable design for a component this small β a configurable version would need a
-schema for something a developer edits once β but it does mean the shipped defaults are visible
-to anyone who forgets to change them.
-
-## Standard library only
-
-Tkinter and `os`. Nothing to install, and nothing added to the dependency tree of whatever
-project copies it in.
-
-## What breaks it
-
-The design is sound; three files it opens are absent. `imgs/dfdlogo.gif` stops the window
-entirely, and the two `.txt` files break a button each. `MANIFEST.in` lists the image for
-packaging, so its absence is an omission rather than a decision.
diff --git a/docs/changelog.md b/docs/changelog.md
deleted file mode 100644
index 5209698..0000000
--- a/docs/changelog.md
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
---8<-- "CHANGELOG.md"
diff --git a/docs/configuration.md b/docs/configuration.md
deleted file mode 100644
index d8e92e5..0000000
--- a/docs/configuration.md
+++ /dev/null
@@ -1,80 +0,0 @@
-# ProgramVer β Configuration
-
-ProgramVer is a template. There is no config file β you edit `main.py`, and every editable
-string carries a `# change as needed` comment.
-
-Earlier documentation gave **line numbers** for these edits. They no longer match the file, so
-this page names the symbols instead.
-
-## The window title
-
-```python
-window.title("Copyright & Version Info for ProgramVer")
-```
-
-In `ProgramVer()`. Use your program's name.
-
-## Program name and version
-
-```python
-info = Label(window, text="ProgramVer \n Version: 1.9.0 (Build #)")
-```
-
-Hardcoded. Reading it from your package metadata
-(`importlib.metadata.version("yourpackage")`) keeps it from drifting after a release.
-
-## Trademark notice
-
-```python
-trademarks = Label(window, text="Copyright (C) 2017 - 2024 Dog Face Development Co. ...")
-```
-
-Placeholder text naming a different company, and dated 2024. Replace it. The `winver` equivalent
-is a single line:
-
-```text
-Β© Microsoft Corporation. All rights reserved.
-```
-
-## Licence blurb
-
-```python
-licenseblurb = Label(window, text="""...GNU General Public License...""")
-```
-
-The shipped text is the GPL v3 notice for "Dog Face Development Company", while **this repository
-is MIT**. For a tool whose purpose is displaying licence information, showing the wrong licence
-is worth fixing before anything else β see
-[`internal/known-issues.md`](./internal/known-issues.md).
-
-Use the notice your own licence recommends. MIT does not require one in-window; GPL does.
-
-## The licence and EULA files
-
-```python
-license_path = get_resource_path("LICENSE.txt") # in openLicense
-eula_path = get_resource_path("EULA.txt") # in openEULA
-```
-
-Both are read as plain text with UTF-8 encoding and shown in a `Text` widget. Neither file is in
-this repository β supply them, or repoint the calls at files you have.
-
-If you point `openLicense` at a Markdown file, note it is displayed raw, with no rendering.
-
-## Images
-
-| Image | Path | Shown |
-|---|---|---|
-| Your logo | `imgs/dfdlogo.gif` | Top |
-| Python-Powered badge | `imgs/pythonpoweredlengthgif.gif` | Bottom |
-
-**GIF or PNG only** β Tkinter's `PhotoImage` reads nothing else without Pillow. Both are loaded
-through `get_resource_path`, so they resolve relative to the module.
-
-Remember `MANIFEST.in` if you add images and intend to package them.
-
-## Layout
-
-Everything is `pack`ed: logo, info, trademarks, blurb, and buttons from the top; the Python badge
-at the bottom. Buttons get `pady=5`. There is no styling beyond Tkinter's defaults, deliberately β
-it is meant to look like a system dialog.
diff --git a/docs/development.md b/docs/development.md
deleted file mode 100644
index 647f89c..0000000
--- a/docs/development.md
+++ /dev/null
@@ -1,77 +0,0 @@
-# ProgramVer β Development
-
-## Setup
-
-```bash
-git clone https://github.com/willtheorangeguy/ProgramVer
-cd ProgramVer
-pip install -r requirements.txt
-python main.py
-```
-
-No runtime dependencies; `requirements.txt` is the tooling.
-
-## Commands
-
-```bash
-python main.py # run
-pytest # 14 tests
-pylint $(git ls-files '*.py') # what CI lints with
-```
-
-## Packaging
-
-`pyproject.toml`, `setup.py`, and `setup.cfg` all describe the package at version `1.9.0`. Two of
-them contain a mistake worth understanding before touching them:
-
-```python
-packages=find_packages(where="imgs"),
-package_dir={"": "imgs"},
-py_modules=["main"],
-```
-
-`imgs/` holds images, not Python packages, so `find_packages` finds nothing and the `package_dir`
-mapping points the root at a directory with no modules in it. Only `py_modules=["main"]` actually
-ships code. `setup.cfg` repeats the same mapping.
-
-`MANIFEST.in` lists `imgs/dfdlogo.gif` and `imgs/pythonpoweredlengthgif.gif`. The first is not in
-the repository. See [`internal/known-issues.md`](./internal/known-issues.md).
-
-Consolidating on `pyproject.toml` would remove the duplication and the chance of the three
-drifting.
-
-## Style
-
-- **Pylint**, with per-file disables at the top (`import-error`, `invalid-name`).
-- **Module docstring and copyright header** on every file.
-- **`get_resource_path` for every file access.** Never a bare relative path β it is what makes the
- module portable into another project.
-- **GIF or PNG images.** Tkinter reads nothing else without Pillow, and Pillow would be the first
- runtime dependency.
-
-## If you change the display strings
-
-They are the template's whole surface, and [Configuration](./configuration.md) names each by
-symbol. The old `CUSTOMIZATION.md` located them by **line number**, which had drifted out of
-date β avoid reintroducing that.
-
-## If you extend the tests
-
-Mock the display, not the filesystem. See [Testing](./testing.md) β the current suite mocks both,
-which is why it passes against missing files.
-
-## CI
-
-| Workflow | Does |
-|---|---|
-| `tests.yml` | pytest |
-| `pylint.yml` | Lint |
-| `codeql-analysis.yml` | Security scan |
-| `push-to-pypi.yml` | Publish on release |
-
-Nothing runs the built package, so a release that cannot start still publishes green.
-
-## Recording defects
-
-Bugs found while working here go in [`internal/known-issues.md`](./internal/known-issues.md)
-rather than being fixed in passing, unless fixing them is the job you are on.
diff --git a/docs/docs.instructions.md b/docs/docs.instructions.md
deleted file mode 100644
index 9023e65..0000000
--- a/docs/docs.instructions.md
+++ /dev/null
@@ -1,480 +0,0 @@
-# Documentation writing standard
-
-This file defines how documentation is written across every repository. It is
-about **prose**: voice, structure, and what belongs on a page. The mechanics of
-which files to create and how to migrate an existing `docs/` folder are in
-[`ROLLOUT.md`](https://github.com/willtheorangeguy/mkdocs/blob/main/ROLLOUT.md).
-
-Read this before writing any page. It ships into every project repo as
-`docs/docs.instructions.md`, so a session working in that repo finds it without
-needing the template checked out.
-
----
-
-## Audience
-
-**Developers.** Assume programming literacy and command-line fluency. Assume
-zero familiarity with *this* project.
-
-- Never explain what a terminal, a package manager, or an environment variable
- is.
-- Always explain what *this project's* flags, files, and concepts mean, even
- when the name looks self-evident.
-- When a project has non-developer users too, write for the developer and add a
- short, clearly-labelled section for the rest. Do not lower the whole page.
-
-The reader arrived from a search engine, a README badge, or a broken build.
-They want one specific answer. Write so they can find it without reading the
-page top to bottom.
-
----
-
-## Voice and style
-
-| Rule | Do | Don't |
-|---|---|---|
-| Person | "You run `build`" | "The user runs `build`" |
-| Voice | "The parser reads the file" | "The file is read by the parser" |
-| Tense | "The command returns 0" | "The command will return 0" |
-| Mood | "Install the package" | "You should install the package" |
-| Case | "Getting started" | "Getting Started" (headings) |
-
-Contractions are fine β "doesn't" reads better than "does not".
-
-**Banned words.** These either condescend or add nothing:
-
-> simply Β· just Β· easy Β· easily Β· obviously Β· of course Β· note that Β· basically Β·
-> as you can see Β· it should be noted
-
-If a step is genuinely easy the reader will notice without being told. If it
-isn't, the word is a lie. Delete the word; the sentence is almost always
-stronger.
-
-**No marketing language.** This is reference material, not a product page. No
-"powerful", "seamless", "blazing fast", "revolutionary". State what the thing
-does and let the reader judge.
-
-**Short sentences.** One idea each. If a sentence needs a semicolon to hold
-together, it usually wants to be two sentences.
-
-**Front-load.** Put the answer in the first sentence of a section, then the
-qualifications. Readers scan; they should not need the second paragraph to learn
-whether the first one applies to them.
-
----
-
-## Heading conventions
-
-- **Exactly one H1 per page**, on the first line, matching the page's nav label.
-- **H2** for major sections, **H3** for subsections. **H4 is discouraged** and
- nothing below it is allowed. Four levels of nesting means the page should be
- split.
-- **Sentence case.** "Configuration file" not "Configuration File".
-- **No numbered headings.** The table of contents handles ordering; manual
- numbers rot the moment a section is inserted.
-- **Unique within a page.** Duplicate headings collide on permalink anchors and
- the second one becomes unreachable. Sibling repeats under different parents
- are fine.
-- **No trailing punctuation**, except a question mark in FAQ entries.
-- Headings are navigation, not prose. "Environment variables" beats "How to
- configure the tool using environment variables".
-
----
-
-## Code blocks
-
-**Always tag the language.** An untagged fence fails lint and loses syntax
-highlighting.
-
-**Never include prompt prefixes.** No `$`, no `>`, no `PS>`. The copy button is
-enabled site-wide and a prompt character breaks paste.
-
-```bash
-pip install lego-block-creator
-```
-
-**Separate commands from output.** Two blocks, not one, so the command stays
-copyable.
-
-```bash
-mkdocs build --strict
-```
-
-```text
-INFO - Documentation built in 1.42 seconds
-```
-
-**Name a block that is a file** with the `title` attribute:
-
-````markdown
-```yaml title="mkdocs.yml"
-site_name: Example
-```
-````
-
-**Use tabs for per-platform instructions**, never sequential prose:
-
-````markdown
-=== "Windows"
-
- ```powershell
- py -m pip install example
- ```
-
-=== "macOS / Linux"
-
- ```bash
- python3 -m pip install example
- ```
-````
-
-### Two syntaxes that will break your build
-
-The macros plugin evaluates Jinja in all markdown, and the snippets extension
-processes include markers everywhere β **including inside code blocks**.
-
-- A literal `{{` or `{%` in a sample is parsed as a macro. Wrap the block in a
- raw tag so it is passed through untouched.
-- A literal include marker at the start of a line is parsed as an include.
- Prefix it with a semicolon to escape it: `;--8<--` renders as the marker
- itself.
-
-These are the two most common causes of a confusing CI failure on a page that
-looks fine locally.
-
----
-
-## Diagrams
-
-**Use Mermaid, never an image of a diagram.** Images don't reflow, don't survive
-dark mode, can't be searched, and go stale in a way nobody notices.
-
-````markdown
-```mermaid
-graph LR
- A[Source markdown] -->|mkdocs build| B[Static HTML]
- B -->|upload-pages-artifact| C[GitHub Pages]
-```
-````
-
-**When a diagram earns its place:** architecture and data flow, state machines,
-request/response sequences, CI pipelines. Anything where the *relationships*
-carry the meaning.
-
-**When it doesn't:** a list of components (use a list), a set of values (use a
-table), a linear procedure (use numbered steps). A diagram of a list is worse
-than the list.
-
-Rules:
-
-- **Under ~12 nodes.** Past that, split into two diagrams or describe the
- structure in prose. A wall of boxes communicates nothing.
-- **Label every edge.** An unlabelled arrow makes the reader guess the
- relationship.
-- **Never hardcode colours.** The theme supplies them; hardcoded fills go
- unreadable when the reader flips to dark mode.
-- **Introduce it in a sentence.** A diagram with no lead-in makes the reader
- reverse-engineer its purpose.
-
----
-
-## When to include examples
-
-Examples are not decoration. The rules are absolute:
-
-- **Every configuration option gets an example value.** A type and a
- description do not tell the reader what a valid value looks like.
-- **Every command gets one complete, runnable invocation.** Not a synopsis with
- placeholders β a line that works if pasted.
-- **Every API entry gets a call and its response.**
-- **A core page with no example is a defect.**
-
-**Examples must be real.** Draw them from the repo's actual code, tests, README,
-or CI logs. An invented example that doesn't run is worse than no example: it
-costs the reader the time to try it *and* their trust in the rest of the page.
-
-If you cannot find a real example, write:
-
-```markdown
-
-```
-
-A TODO is honest. Invented prose is a bug that ships.
-
----
-
-## Writing an FAQ
-
-Every repo gets an `faq.md`, whether or not anyone has asked a question yet.
-Waiting for real questions means the page never gets written for the quiet
-repos, which are exactly the ones where a reader has nowhere else to turn.
-
-**Anticipate the question, never invent the answer.** These are different acts,
-and only the second one is forbidden:
-
-- Predicting *what* someone will ask is judgement, and you have the evidence to
- do it well β you have just read the source, the tests, and the workflows.
-- Inventing *what the software does* is a defect, exactly as it is on every
- other page. Every answer must trace to something you verified.
-
-If you cannot answer a question from verified behaviour, the question does not
-belong on the page.
-
-### Where good questions come from
-
-Work from what the code told you, in roughly this order of value:
-
-1. **Surprises you hit while reading the source.** If a behaviour surprised
- you, it will surprise the reader. A CLI that keeps nothing after it exits, a
- container that bakes files in at build time, a command that must be run
- before another β each is a question waiting to happen.
-2. **Gaps between what the README promises and what the code does.** Readers
- arrive believing the README.
-3. **The first five minutes.** Install fails, the command is not on `PATH`, the
- program exits immediately, nothing appears on screen.
-4. **Decisions the reader must make.** Which install method, which image tag,
- which of two spellings.
-5. **Anything a placeholder, prompt, or error message hints at.** If the code
- prints "make sure this colour is in the database", someone has hit that.
-
-### What does not belong
-
-- Questions whose answer is "read the installation page". An FAQ is for things
- that are genuinely confusing, not a second table of contents.
-- Padding to reach a count. Four sharp entries beat twelve filler ones.
-- Anything you would have to guess at. Leave a `` instead.
-
-### Shape
-
-Phrase each entry as the reader would actually type it into a search box β
-"Why did my data disappear?", not "Data persistence". Use `???+ question` for
-the first entry so the page opens with something visible, `???` for the rest.
-Put failures with a visible symptom under Troubleshooting instead, keyed on the
-error text.
-
-## Documenting APIs
-
-Pick the form that matches the project's actual interface.
-
-### Python
-
-Rely on `mkdocstrings`. Write **Google-style docstrings in the source** rather
-than duplicating signatures in markdown β a hand-copied signature is wrong the
-first time the code changes.
-
-```markdown
-::: mypackage.core
- options:
- members:
- - Parser
- - load
-```
-
-`api.md` supplies the narrative around the generated blocks: what the module is
-for, which entry point to start with, how the pieces fit. It does not restate
-what the docstrings already say.
-
-### Command-line
-
-One table per command, plus a usage example. Never prose-describe flags.
-
-| Flag | Type | Default | Description |
-|---|---|---|---|
-| `--output` | path | `./out` | Directory for generated files |
-| `--verbose` | flag | off | Print each file as it is written |
-
-### HTTP
-
-For each endpoint: method and path, parameters, request body, response body,
-and the status codes it actually returns β including the error cases. An
-endpoint reference that documents only the happy path is half-written.
-
----
-
-## Documenting configuration
-
-**One reference table per source**, with these exact columns:
-
-| Option | Type | Default | Description |
-|---|---|---|---|
-| `log_level` | string | `info` | One of `debug`, `info`, `warning`, `error` |
-| `timeout` | integer | `30` | Seconds before a request is abandoned |
-
-- **State the precedence order explicitly**, once, at the top of the page:
- CLI flag > environment variable > config file > default. Readers debugging a
- setting that "isn't applying" are almost always hitting precedence.
-- **Give the environment-variable naming scheme once** ("uppercase the option
- and prefix `APP_`"), then don't repeat it per row.
-- **Enumerate valid values** for anything that isn't free-form. "One of
- `debug`, `info`, `warning`, `error`" beats "the log level".
-- **Say what happens on an invalid value** β ignored, warned, or fatal.
-
----
-
-## Admonitions
-
-| Type | Use for |
-|---|---|
-| `note` | An aside the reader can skip |
-| `tip` | An optional improvement |
-| `warning` | Something that will cause a problem |
-| `danger` | Data loss, security, or anything irreversible |
-| `example` | A worked case |
-| `question` | An FAQ entry |
-
-```markdown
-!!! warning
- Deleting the cache directory forces a full re-index on next start.
-```
-
-- **One per point.** Never stack two in a row β the second stops registering.
-- **Never decorative.** An admonition around ordinary prose trains readers to
- skip all of them, including the `danger` one that matters.
-- **Three per page is a lot.** More than that means the page structure is
- fighting the content.
-
----
-
-## Links and terminology
-
-- **Relative `.md` links internally**: `[configuration](configuration.md)`. The
- build validates these, so a rename that breaks a link fails CI instead of
- shipping a 404.
-- **Absolute URLs for external links.**
-- **Never bare URLs.** `[the Docker docs](https://docs.docker.com/)`, not the
- raw address.
-- **Link the first mention** of another page in a section, not every mention.
-- **Capitalize the product name consistently** within a repo. Pick the form the
- README uses and never vary it.
-- **Backticks** for file paths, identifiers, flags, values, and commands.
-- **Bold** for UI elements the reader clicks: press **Save**.
-- **Define an acronym on first use** per page. Readers arrive mid-site.
-
----
-
-## What not to do
-
-- **Don't invent.** No features, flags, install methods, or return values that
- you have not seen in the source, tests, README, or workflows. This is the
- single most damaging failure mode: invented documentation is indistinguishable
- from real documentation until someone tries it. Anticipating a *question*
- nobody has asked yet is fine and expected β see [Writing an FAQ](#writing-an-faq);
- inventing the *answer* is not.
-- **Don't duplicate root files.** `CHANGELOG.md`, `CONTRIBUTING.md`,
- `SECURITY.md`, `CODE_OF_CONDUCT.md`, and `LICENSE.md` are pulled in by
- reference. Copying them creates two versions that drift.
-- **Don't editorialize about the code.** No "this is a bit hacky", no "ideally
- this would be refactored". Document what it does.
-- **Don't write filler.** A section that restates its own heading in a sentence
- should be deleted, not padded.
-- **Don't rewrite the README's voice.** Reorganizing existing content into pages
- is not a licence to restyle prose that already works.
-- **Leave a TODO where information is missing.** Never plausible-sounding
- guesses.
-
----
-
-## Per-page contract
-
-The required structure for each core page. This is what makes 130 sites
-consistent structurally, not just tonally. Sections marked *optional* are
-dropped when they don't apply; the rest are required.
-
-### `index.md` β Home
-
-```markdown
-#
-
-
-
-## Key features
-<3-8 bullets. Concrete capabilities, not adjectives.>
-
-## Quick start
-
-
-## Where to next
-
-
-## Support
-
-```
-
-### `getting-started.md`
-
-Assumes nothing. Takes the reader from zero to one working result.
-
-```markdown
-# Getting started
-## Prerequisites <- exact versions, and how to check them
-## Install <- the single recommended path; link installation.md for the rest
-## First run <- numbered steps, each with a command and its expected output
-## What just happened <- brief explanation of the result
-## Next steps <- links onward
-```
-
-The test: someone who has never seen the project follows this page start to
-finish and gets a working result without opening another page.
-
-### `installation.md`
-
-```markdown
-# Installation
-## Requirements <- OS, runtime versions, system dependencies
-## <- executable / package manager / Docker / source
-## Verify the installation <- a command and the output proving it worked
-## Upgrading
-## Uninstalling
-```
-
-Every method the project actually supports gets a tab. Do not document an
-install path that has not been verified to exist.
-
-### `configuration.md`
-
-```markdown
-# Configuration
-## Precedence <- stated once, explicitly
-## <- config file / env vars / CLI flags
-## Examples <- at least one complete, working configuration
-## Troubleshooting <- optional
-```
-
-### `architecture.md`
-
-```markdown
-# Architecture
-## Overview <- one Mermaid diagram plus a lead-in paragraph
-## Components <- one H3 per major component: responsibility and boundaries
-## Data flow <- how a request or run moves through the system
-## Directory layout <- annotated tree of the significant paths only
-## Design decisions <- optional: what was chosen, and what it was chosen over
-```
-
-Written for someone about to modify the code. "Directory layout" lists the
-paths that matter, not every file.
-
-### `api.md`
-
-Structure follows the interface type β see [Documenting APIs](#documenting-apis).
-Always opens with a paragraph on what the public surface is and where to start.
-
-### `contributing.md` and the About group
-
-One line each β an include of the root file. No wrapper prose, no heading of
-their own: the root file supplies its own H1.
-
----
-
-## Checklist
-
-Before considering a page done:
-
-- [ ] One H1, matching the nav label
-- [ ] No banned words
-- [ ] Every code fence has a language tag and no prompt prefix
-- [ ] Every option, command, and endpoint has a real example
-- [ ] Internal links are relative `.md` paths
-- [ ] No invented features, flags, or behaviour
-- [ ] `mkdocs build --strict` passes with zero warnings
diff --git a/docs/faq.md b/docs/faq.md
deleted file mode 100644
index bf9f8d2..0000000
--- a/docs/faq.md
+++ /dev/null
@@ -1,64 +0,0 @@
-# ProgramVer β FAQ
-
-## It crashes on startup
-
-`ProgramVer()` loads `imgs/dfdlogo.gif`, which is not in the repository, so Tkinter raises a
-`TclError` before the window appears. Supply your own logo at that path β GIF or PNG.
-
-Recorded in [`internal/known-issues.md`](./internal/known-issues.md).
-
-## The License and EULA buttons crash
-
-They read `LICENSE.txt` and `EULA.txt`. Neither exists here β the repository ships `LICENSE.md`,
-and there is no EULA. Add plain-text files, or repoint the calls.
-
-## The tests pass, so how is it broken?
-
-The suite mocks every file read and every Tk widget, so it never touches the filesystem or a
-display. Every line runs; every line runs against a mock. See [Testing](./testing.md).
-
-## Why does the window show a GPL notice when the repo is MIT?
-
-The blurb, the trademark line, and the version are placeholder content for you to replace β each
-marked `# change as needed`. They name a different company and quote the GPL.
-
-For a tool whose purpose is displaying licence information, shipping the wrong licence as the
-default is worth fixing. Same known-issues file.
-
-## How do I use it in my program?
-
-```python
-from main import ProgramVer
-ProgramVer()
-```
-
-Copy `main.py` and `imgs/` into your project and edit the strings β see
-[Configuration](./configuration.md).
-
-## Earlier docs said to copy `ProgramVer.py`
-
-There is no such file; the module is `main.py`. Renaming it on the way into your project makes
-the old instruction true.
-
-## Does it block?
-
-Yes. `ProgramVer()` calls `mainloop()` itself, so it returns when the window is closed. Calling
-it from an existing Tkinter app starts a second event loop β see
-[Architecture](./architecture.md).
-
-## Can I use a PNG or JPEG logo?
-
-PNG yes, JPEG no. Tkinter's `PhotoImage` reads GIF and PNG only; JPEG needs Pillow.
-
-## Does it need internet, or write anything?
-
-Neither. It reads local files and draws a window.
-
-## Why Tkinter?
-
-Because it is in the standard library. A version window that added a GUI dependency to every
-project it was copied into would not be worth copying.
-
-## Is there a Windows executable?
-
-Yes, attached to [releases](https://github.com/willtheorangeguy/ProgramVer/releases/latest).
diff --git a/docs/images/logo.png b/docs/images/logo.png
new file mode 100644
index 0000000..f41c946
Binary files /dev/null and b/docs/images/logo.png differ
diff --git a/docs/images/welcome.png b/docs/images/welcome.png
new file mode 100644
index 0000000..fb92697
Binary files /dev/null and b/docs/images/welcome.png differ
diff --git a/docs/index.md b/docs/index.md
deleted file mode 100644
index 8a63098..0000000
--- a/docs/index.md
+++ /dev/null
@@ -1,57 +0,0 @@
-# ProgramVer β Documentation
-
-A `winver`-style copyright and version window for your own Python programs: a logo, a version
-line, a trademark notice, a licence blurb, and buttons that open the full licence and EULA.
-
-```text
-ProgramVer/
-βββ main.py get_resource_path, openLicense, openEULA, ProgramVer
-βββ imgs/ the window's images
-βββ tests/test_main.py
-βββ docs/ this documentation
-```
-
-## Pages
-
-- [Quickstart](./quickstart.md) β run it, and what you must supply first
-- [Installation](./installation.md) β source, PyPI, executable
-- [Configuration](./configuration.md) β every string and image to change for your project
-- [Architecture](./architecture.md) β three functions, one window
-- [Development](./development.md) β packaging and style
-- [Testing](./testing.md) β the suite, and what it does not check
-- [FAQ](./faq.md) β why it fails, what to customise, why GPL text
-- [Troubleshooting](./troubleshooting.md) β missing files, blank windows
-- [Roadmap](./roadmap.md) β direction and non-goals
-- [Known issues](./internal/known-issues.md) β recorded defects
-
-## It does not run as shipped
-
-Three files the code needs are absent from the repository:
-
-| Wanted by | File | Present |
-|---|---|---|
-| `ProgramVer()` | `imgs/dfdlogo.gif` | **No** |
-| `openLicense()` | `LICENSE.txt` | **No** β the repo has `LICENSE.md` |
-| `openEULA()` | `EULA.txt` | **No** |
-
-The first stops the window opening at all. `MANIFEST.in` lists `imgs/dfdlogo.gif` for packaging,
-so it is expected to exist and does not.
-
-CI is green throughout, because the test suite mocks file access and every Tk widget. See
-[`internal/known-issues.md`](./internal/known-issues.md).
-
-## What it is for
-
-Microsoft's `winver` shows a small window with the product name, version, and licensing notice.
-ProgramVer is the same idea for a Python program: a function you call from an About menu that
-presents the same information, with the licence text one click away.
-
-The design assumption is that **you edit it**. Program name, version, trademark line, licence
-blurb, and the two document filenames are all literals in `main.py`, each with a
-`# change as needed` comment. That is the intended workflow β see
-[Configuration](./configuration.md).
-
-## Standard library only
-
-Tkinter and nothing else. Dropping `main.py` into a project adds no dependencies, which is the
-point of a component meant to be copied rather than depended on.
diff --git a/docs/installation.md b/docs/installation.md
deleted file mode 100644
index e050d42..0000000
--- a/docs/installation.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# ProgramVer β Installation
-
-## Requirements
-
-| | |
-|---|---|
-| Python | 3.x with Tkinter |
-| Dependencies | None at runtime |
-
-Tkinter is bundled on Windows and macOS; on Linux install `python3-tk` (Debian, Ubuntu) or
-`python3-tkinter` (Fedora).
-
-## From source
-
-```bash
-git clone https://github.com/willtheorangeguy/ProgramVer
-cd ProgramVer
-python main.py
-```
-
-You will need to supply `imgs/dfdlogo.gif`, `LICENSE.txt`, and `EULA.txt` β see
-[Quickstart](./quickstart.md).
-
-Resource paths are resolved against `main.py`'s own location via `get_resource_path`, so the
-working directory does not matter.
-
-## As a component in your own project
-
-The intended use. Copy `main.py` into your project and import it:
-
-```python
-from main import ProgramVer
-ProgramVer()
-```
-
-Copy `imgs/` alongside it, or repoint the image paths.
-
-Earlier documentation told you to copy `ProgramVer.py` and `from ProgramVer import *`. There is
-no `ProgramVer.py` in this repository β the module is `main.py`, and the function inside it is
-`ProgramVer`. Renaming the file to `programver.py` on the way in is reasonable, and then the old
-instruction becomes true.
-
-## From PyPI
-
-```bash
-pip install programver
-programver
-```
-
-Two caveats. The console script points at `main:ProgramVer`, which does call `mainloop()`, so it
-would work β if the image were present. And the packaging declares `imgs/` as the package root
-(`package_dir={"": "imgs"}`, `find_packages(where="imgs")`) where there are no Python packages
-at all; only `py_modules=["main"]` ships anything. See
-[`internal/known-issues.md`](./internal/known-issues.md).
-
-## Windows executable
-
-Attached to [releases](https://github.com/willtheorangeguy/ProgramVer/releases/latest), built
-with PyInstaller. Note that a one-file build unpacks to a temporary directory β `get_resource_path`
-resolves against `__file__`, which PyInstaller sets appropriately, so this should hold.
-
-## Verify
-
-```bash
-python main.py
-```
-
-The window appears with your logo. A `TclError` about `dfdlogo.gif` means the image is still
-missing; see [Troubleshooting](./troubleshooting.md).
-
-## Tests
-
-```bash
-pip install -r requirements.txt
-pytest
-```
diff --git a/docs/internal/known-issues.md b/docs/internal/known-issues.md
deleted file mode 100644
index 7ee2525..0000000
--- a/docs/internal/known-issues.md
+++ /dev/null
@@ -1,93 +0,0 @@
-# Known Issues β ProgramVer
-
-Concrete defects and gaps found while writing this repository's documentation in
-August 2026. **Nothing here was changed** β each one needs a code, configuration, or
-licensing decision rather than a documentation one.
-
-Ordered by severity. See [`docs/roadmap.md`](../roadmap.md) for the narrative version,
-which also covers deliberate non-goals.
-
-**6 open:** 2 high, 3 medium, 1 low.
-
-## 1. The window cannot open: imgs/dfdlogo.gif is not in the repository
-
-**Severity:** High
-**Where:** `main.py` -> `ProgramVer`, `MANIFEST.in`
-
-**What:** `ProgramVer()` calls `PhotoImage(file=get_resource_path("imgs/dfdlogo.gif"))` as the first of its two images. `imgs/` contains only `pythonpoweredlengthgif.gif` and an egg-info directory -- `dfdlogo.gif` is absent, and `git ls-files` does not list it. `MANIFEST.in` explicitly includes it for packaging, so its absence is an omission rather than a decision.
-
-**Why it matters:** Tkinter raises `TclError` on that line, before any widget is packed, so the program produces a traceback and no window at all. This is the only thing the package does: there is no degraded mode, no fallback image, and no other entry point. Every route -- `python main.py`, the `programver` console script, the PyInstaller build, and copying the module into another project -- fails identically. CI stays green throughout, because the test suite patches `main.PhotoImage`.
-
-**Suggested fix:** Restore or replace `imgs/dfdlogo.gif` (GIF or PNG; Tkinter reads nothing else without Pillow). Consider falling back to a text label when the image is missing, since this is a template others copy half-configured.
-
-## 2. Both document buttons read files that do not exist
-
-**Severity:** High
-**Where:** `main.py` -> `openLicense`, `openEULA`
-
-**What:** `openLicense` opens `get_resource_path("LICENSE.txt")` and `openEULA` opens `get_resource_path("EULA.txt")`, each with a bare `open(...)` and no guard. Neither file is in the repository: the licence here is `LICENSE.md`, and there is no EULA at all.
-
-**Why it matters:** The two buttons are half the window's function -- a version dialog whose entire value is making the licence one click away. Both raise `FileNotFoundError` from inside a Tk callback, which prints a traceback to the console and leaves the window looking unresponsive rather than reporting anything to the user. A `.md` file sitting next to a `.txt` reference is exactly the kind of near-miss that survives review.
-
-**Suggested fix:** Add plain-text `LICENSE.txt` and `EULA.txt`, or point the calls at files that exist. Either way, wrap the read and show the error in the window instead of the console -- a template will be copied into projects where these filenames are wrong.
-
-## 3. The test suite mocks the filesystem, so it passes against a program that cannot start
-
-**Severity:** Medium
-**Where:** `tests/test_main.py`
-
-**What:** All 14 tests patch `main.Tk`, `main.Text`, `main.Label`, `main.Button`, `main.PhotoImage`, and `builtins.open` (via `mock_open`). `test_get_resource_path` asserts only that the returned string **ends with** `dfdlogo.gif` -- never that the path resolves. Earlier documentation reported '100% code coverage for the main module'.
-
-**Why it matters:** Both High-severity issues above are missing files, and both are invisible to this suite by construction. The result is a repository whose badge, coverage figure, and CI all report health while the program raises before drawing a window. Mocking the display is necessary for headless CI; mocking the filesystem as well removes the only check that would have caught this, and the coverage number then actively misleads.
-
-**Suggested fix:** Add a test that the resources exist -- roughly four lines, no mocking:
-
- for name in ("imgs/dfdlogo.gif", "imgs/pythonpoweredlengthgif.gif",
- "LICENSE.txt", "EULA.txt"):
- assert os.path.exists(get_resource_path(name)), name
-
-Keep mocking Tk; stop mocking `open` in tests whose purpose is to prove a file is read.
-
-## 4. The window displays a GPL notice and another company's copyright, in an MIT repository
-
-**Severity:** Medium
-**Where:** `main.py` -> `trademarks`, `licenseblurb`, `info` labels; `LICENSE.md`
-
-**What:** The `trademarks` label reads 'Copyright (C) 2017 - 2024 Dog Face Development Co. All rights reserved in all countries', and `licenseblurb` renders the GNU GPL v3 notice for 'Dog Face Development Company'. `LICENSE.md` in this repository is **MIT, Copyright 2026 willtheorangeguy**. Each label carries a `# change as needed` comment, so the text is placeholder by design.
-
-**Why it matters:** The program's single purpose is displaying accurate copyright and licence information, and its default output is neither -- wrong licence, wrong holder, wrong year. Anyone who copies the template and forgets one label ships a false licence claim in their own About box, which is the specific failure this tool exists to prevent. The `# change as needed` comments make the intent clear in the source and are invisible in the running window.
-
-**Suggested fix:** Make the defaults match this repository -- MIT, willtheorangeguy, the current year -- so an unedited copy is at least self-consistent. Better still, derive the name and version from package metadata and read the notice from the licence file, leaving less to be forgotten.
-
-## 5. Packaging declares imgs/ as the package root, where there are no packages
-
-**Severity:** Medium
-**Where:** `setup.py`, `setup.cfg`
-
-**What:** `setup.py` has `packages=find_packages(where="imgs")` and `package_dir={"": "imgs"}`; `setup.cfg` repeats `package_dir = \n = imgs` with `packages = find:` under `where = imgs`. `imgs/` contains image files and an egg-info directory -- no Python packages. `find_packages` therefore returns an empty list, and only `py_modules=["main"]` ships any code. A third description of the same package exists in `pyproject.toml`.
-
-**Why it matters:** The build succeeds and publishes, which is why this has survived: the wheel happens to contain the one module that matters, by a different mechanism than the one the configuration appears to be using. Anyone adding a real package later will find it silently excluded, and the `package_dir` mapping makes the failure hard to read -- setuptools will look for modules under `imgs/`. Three overlapping build descriptions make it likelier that a fix lands in the file that is not being read.
-
-**Suggested fix:** Drop the `package_dir` and `find_packages` lines -- this is a single-module project and `py_modules` is the correct declaration. Then consolidate on `pyproject.toml` and delete `setup.py` and `setup.cfg`.
-
-## 6. The README's integration instructions name a file that does not exist
-
-**Severity:** Low
-**Where:** `README.md` (corrected in this pass), `docs/CUSTOMIZATION.md` (removed in this pass)
-
-**What:** The How To Use section instructed: 'Copy the `ProgramVer.py` file to your project's main directory', then 'add `from ProgramVer import *`' and 'call ProgramVer through the `ProgramVer()` function'. There is no `ProgramVer.py` -- the module is `main.py`. Separately, `docs/CUSTOMIZATION.md` located each editable string by line number ('_Line 11_', '_Line 40_'), and those no longer match `main.py`.
-
-**Why it matters:** Copying the module into another project is the documented primary use, and the instructions for it name the wrong file -- so a reader either copies nothing or copies `main.py` and finds the import line wrong too. The line-number references fail more quietly: they point at real lines containing different code, so someone following them edits the wrong string and gets a window that still shows the old text.
-
-**Suggested fix:** Fixed in this pass -- the README and `docs/configuration.md` now name `main.py` and locate each editable string by symbol rather than line number. Renaming the module to `programver.py` would make the original instructions true and is worth considering, since `main.py` is a poor name for a file meant to be dropped into someone else's project.
-
----
-
-## Also, across every repository
-
-**`.bandit` is present on disk but untracked in git.** Verified in PyWorkout, treklogger,
-skyscanner-cli, booking-cli, piggy, and aibot β the config file exists locally in each but
-`git ls-files` does not know about it, so none of it reached GitHub.
-
-The August 2026 security sweep therefore looks complete locally and landed nowhere. Worth
-checking across all 44 repositories it covered.
diff --git a/docs/license.md b/docs/license.md
deleted file mode 100644
index 4b9f04f..0000000
--- a/docs/license.md
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
---8<-- "LICENSE.md"
diff --git a/docs/quickstart.md b/docs/quickstart.md
deleted file mode 100644
index 1c521bb..0000000
--- a/docs/quickstart.md
+++ /dev/null
@@ -1,70 +0,0 @@
-# ProgramVer β Quickstart
-
-## Before it will run
-
-The repository is missing three files the code opens. Supply them first:
-
-| File | Used by | Notes |
-|---|---|---|
-| `imgs/dfdlogo.gif` | `ProgramVer()` | Your logo. **GIF or PNG only** β Tkinter reads nothing else |
-| `LICENSE.txt` | **Open License** button | Plain text; the repo ships `LICENSE.md` |
-| `EULA.txt` | **Open EULA** button | Plain text |
-
-Without the first, the window does not open at all. See
-[`internal/known-issues.md`](./internal/known-issues.md).
-
-## Run it
-
-```bash
-git clone https://github.com/willtheorangeguy/ProgramVer
-cd ProgramVer
-python main.py
-```
-
-## What you get
-
-A window titled *Copyright & Version Info for ProgramVer*, containing:
-
-- Your logo
-- The program name and version
-- A trademark notice
-- A licence blurb
-- **Open License** and **Open EULA** buttons
-- The Python-Powered badge
-
-## Use it in your own program
-
-```python
-from main import ProgramVer
-
-ProgramVer() # opens the window and blocks until it is closed
-```
-
-`ProgramVer()` calls `mainloop()` itself, so it blocks. Calling it from an existing Tkinter app
-means running a second event loop β see [Architecture](./architecture.md).
-
-## Make it yours
-
-Every string in the window is a literal in `main.py` marked `# change as needed`:
-
-| Change | Where |
-|---|---|
-| Window title | `window.title(...)` in `ProgramVer` |
-| Name and version | The `info` label |
-| Trademark notice | The `trademarks` label |
-| Licence summary | The `licenseblurb` label |
-| Licence file | `get_resource_path("LICENSE.txt")` in `openLicense` |
-| EULA file | `get_resource_path("EULA.txt")` in `openEULA` |
-| Logo | `imgs/dfdlogo.gif` |
-
-The shipped text names a different company and quotes the GPL, while this repository is MIT. It
-is placeholder content β replace all of it. See [Configuration](./configuration.md).
-
-## Tests
-
-```bash
-pip install -r requirements.txt
-pytest
-```
-
-14 tests, passing. Read [Testing](./testing.md) before reading anything into that.
diff --git a/docs/roadmap.md b/docs/roadmap.md
deleted file mode 100644
index 418d29c..0000000
--- a/docs/roadmap.md
+++ /dev/null
@@ -1,54 +0,0 @@
-# ProgramVer β Roadmap
-
-Direction, not a schedule. Defects are in
-[`internal/known-issues.md`](./internal/known-issues.md).
-
-## Where it is
-
-A working template that does not currently run, because three files it opens are absent.
-Everything else β the layout, the customisation points, the resource resolution β is sound.
-
-## Considered
-
-**Restoring the missing files.** `imgs/dfdlogo.gif`, `LICENSE.txt`, and `EULA.txt`. The first
-alone is the difference between a program that starts and one that does not.
-
-**A test that the resources exist.** Four lines, no mocking, and it would have caught all three.
-
-**Defaults that match this repository.** The window shows a GPL notice and another company's
-copyright while the repo is MIT β placeholder text that a tool for displaying licences should
-not ship.
-
-**Reading the version from package metadata** rather than a hardcoded string, so a release bump
-reaches the window.
-
-**Fixing the packaging.** `package_dir={"": "imgs"}` and `find_packages(where="imgs")` point at a
-directory with no packages.
-
-**`Toplevel()` for the licence and EULA windows** instead of second `Tk()` roots.
-
-**Graceful degradation.** A missing logo could render a placeholder and a missing licence file an
-explanatory message, rather than a traceback β for a template others will copy half-configured,
-that is friendlier than failing hard.
-
-## Non-goals
-
-**A configuration file.** Editing the literals is the workflow; a schema for values a developer
-sets once would be more machinery than the component.
-
-**A GUI toolkit dependency.** Tkinter is in the standard library, and staying there is what makes
-this safe to copy into any project.
-
-**Rich text or Markdown rendering.** The licence is displayed as plain text in a `Text` widget.
-Rendering would mean a dependency, for a document nobody reads carefully in a dialog.
-
-**Becoming a general About-box framework.** It shows version and licence information. Update
-checks, credits screens, and telemetry consent are all different things.
-
-## Contributing
-
-Issues and pull requests welcome β see the
-[Contributing Guide](https://github.com/willtheorangeguy/.github/blob/main/CONTRIBUTING.md) or
-the [Discord](https://discord.gg/x3G8adwVUe).
-
-Adding `imgs/dfdlogo.gif` is the smallest change that turns this back into a working program.
diff --git a/docs/testing.md b/docs/testing.md
deleted file mode 100644
index bdd0946..0000000
--- a/docs/testing.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# ProgramVer β Testing
-
-```bash
-pip install -r requirements.txt
-pytest
-```
-
-14 tests, passing, in about a tenth of a second.
-
-## What is covered
-
-`tests/test_main.py` exercises all four functions:
-
-| Test group | Asserts |
-|---|---|
-| `get_resource_path` | The returned path ends with the requested filename |
-| `openLicense` | A window is created and a file is opened |
-| `openEULA` | The same, for the EULA |
-| `ProgramVer` | The widgets are constructed and packed |
-
-`unittest.mock` supplies `mock_open` for file reads and patches `main.Tk`, `main.Text`,
-`main.Label`, `main.Button`, and `main.PhotoImage`.
-
-## What is not covered, and it matters here
-
-Mocking every file read and every widget is what lets the suite run headless in CI. It also means
-the suite passes on a program that cannot start.
-
-Concretely: `test_get_resource_path` asserts the returned string **ends with** `dfdlogo.gif`. It
-never checks that the file exists β and it does not. `test_openLicense_creates_window` patches
-`builtins.open` with `mock_open`, so `LICENSE.txt` being absent is invisible.
-
-So "100% coverage of the main module", as earlier documentation put it, is true and says nothing
-about whether the program works. Every line executes; every line executes against a mock.
-
-## Worth adding
-
-A test that the files the code opens are actually in the repository:
-
-```python
-def test_required_resources_exist(self):
- for name in ("imgs/dfdlogo.gif", "imgs/pythonpoweredlengthgif.gif",
- "LICENSE.txt", "EULA.txt"):
- self.assertTrue(os.path.exists(get_resource_path(name)), name)
-```
-
-Four lines, no mocking, and it would have caught all three of this repository's High-severity
-issues. Recorded in [`internal/known-issues.md`](./internal/known-issues.md).
-
-The general point is worth keeping in mind when extending the suite: mock the **display**, not
-the **filesystem**. The display cannot be exercised in CI; the filesystem can.
-
-## CI
-
-`.github/workflows/tests.yml` runs the suite on push and pull request. `pylint.yml` lints,
-`codeql-analysis.yml` scans, `push-to-pypi.yml` publishes on release.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
deleted file mode 100644
index b15176e..0000000
--- a/docs/troubleshooting.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# ProgramVer β Troubleshooting
-
-## `TclError: couldn't open ".../imgs/dfdlogo.gif"`
-
-The file is not in the repository, and the code needs it before the window can appear. Supply
-your own logo at `imgs/dfdlogo.gif` β GIF or PNG.
-
-Recorded in [`internal/known-issues.md`](./internal/known-issues.md). `MANIFEST.in` lists it, so
-it is expected to exist.
-
-## `FileNotFoundError: LICENSE.txt` / `EULA.txt`
-
-The **Open License** and **Open EULA** buttons read those exact filenames. Neither is in the
-repository. Add them as plain text, or edit `openLicense` and `openEULA` to point at files you
-have β the repository's own licence is `LICENSE.md`.
-
-## `TclError: couldn't recognize data in image file`
-
-The image is not a GIF or PNG. Tkinter reads nothing else without Pillow.
-
-## A label is blank where an image should be
-
-Tkinter keeps no Python reference to a `PhotoImage`. In the current code they survive because
-`ProgramVer()` blocks in `mainloop()` and they stay in scope. If you refactor it to return the
-window, bind each image to a widget attribute (`label.image = img`) or they will vanish silently.
-
-## `ModuleNotFoundError: No module named 'tkinter'`
-
-A separate package on most Linux distributions:
-
-```bash
-sudo apt install python3-tk # Debian, Ubuntu
-sudo dnf install python3-tkinter # Fedora
-```
-
-## Closing the License window closed everything
-
-`openLicense` and `openEULA` each construct a second `Tk()` root rather than a `Toplevel()`.
-Multiple roots share one interpreter, so destroying the wrong one can take the others down. See
-[Architecture](./architecture.md).
-
-## The version or copyright is wrong
-
-They are placeholder literals in `main.py` β the shipped text names a different company and a
-2024 date. [Configuration](./configuration.md) names each by symbol.
-
-## `pip install programver` then `programver` fails
-
-The console script itself is wired correctly (`main:ProgramVer`, which does call `mainloop()`),
-but it fails on the missing image like any other route. The packaging also declares `imgs/` as
-the package root, which is wrong but harmless β only `py_modules=["main"]` ships code. See
-[`internal/known-issues.md`](./internal/known-issues.md).
-
-## Old docs referenced line numbers that do not match
-
-`docs/CUSTOMIZATION.md` located each editable string by line number, and the file has changed
-since. [Configuration](./configuration.md) names symbols instead.
-
-## Tests pass but the program does not run
-
-Expected, and the point of the [Testing](./testing.md) page: the suite mocks the filesystem as
-well as the display.
-
-## Still stuck
-
-[Open an issue](https://github.com/willtheorangeguy/ProgramVer/issues/new/choose) or ask on the
-[Discord](https://discord.gg/x3G8adwVUe), with your OS, Python version, and the traceback.
diff --git a/main.py b/main.py
index 3b5892e..73bb0ae 100644
--- a/main.py
+++ b/main.py
@@ -1,97 +1,47 @@
"""
-ProgramVer - A Python version of Microsoft's 'winver'.
+ProgramVer - Standalone demo/test entry point.
+Imports from the programver package and shows a demo version dialog.
Copyright (C) 2017-2026 willtheorangeguy
"""
# pylint: disable=import-error, invalid-name
import os
-from tkinter import Tk, Text, INSERT, PhotoImage, Label, Button, TOP, BOTTOM
+from programver import VersionDialog
-# Import Statements
-# Helper Functions
-
-
-def get_resource_path(filename):
- """Get the absolute path to a resource file."""
- base_dir = os.path.dirname(os.path.abspath(__file__))
- return os.path.join(base_dir, filename)
-
-
-# Document Functions
-
-
-def openLicense():
- """Opens the license file in a new window."""
- windowl = Tk()
- license_path = get_resource_path("LICENSE.txt")
- with open(license_path, "r", encoding="UTF-8") as licensefile:
- licensecontents = licensefile.read()
- windowl.title("License")
- licensetext = Text(windowl)
- licensetext.insert(INSERT, licensecontents)
- licensetext.pack()
-
-
-def openEULA():
- """Opens the EULA file in a new window."""
- windowl = Tk()
- eula_path = get_resource_path("EULA.txt")
- with open(eula_path, "r", encoding="UTF-8") as eulafile:
- eulacontents = eulafile.read()
- windowl.title("EULA")
- eulatext = Text(windowl)
- eulatext.insert(INSERT, eulacontents)
- eulatext.pack()
-
-
-# ProgramVer Function
def ProgramVer():
- """Main function for ProgramVer."""
- window = Tk()
- # Window Elements
- window.title(
- "Copyright & Version Info for ProgramVer"
- ) # change name based on program name
- # UI Elements
- dfdimage = PhotoImage(file=get_resource_path("imgs/dfdlogo.gif"))
- pythonimage = PhotoImage(file=get_resource_path("imgs/pythonpoweredlengthgif.gif"))
- dfdlogo = Label(window, image=dfdimage)
- pythonpowered = Label(window, image=pythonimage)
- info = Label(
- window, text="ProgramVer \n Version: 1.9.0 (Build #)"
- ) # change respectively
- trademarks = Label(
- window,
- text="Copyright (C) 2017 - 2024 Dog Face Development Co. \
- All rights reserved in all countries. \
- \n ProgramVer and its code, user interface and all other associated trademarks are protected \
- \nby trademarks and copyright in Canada, the United States and other countries.",
- ) # change as needed
- licenseblurb = Label(
- window,
- text="""\n ProgramVer - Version window for DFD Co.'s programs
- Copyright (C) 2017-2024 Dog Face Development Company
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, version 3 of the License.
+ """Main function for ProgramVer demo."""
+ base_dir = os.path.dirname(os.path.abspath(__file__))
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License below for more details.""",
- ) # change as needed
- licensebtn = Button(window, text="Open License", command=openLicense)
- eulabtn = Button(window, text="Open EULA", command=openEULA)
- # Pack Statements
- dfdlogo.pack(side=TOP)
- info.pack(side=TOP)
- trademarks.pack(side=TOP)
- licenseblurb.pack(side=TOP)
- licensebtn.pack(pady=5)
- eulabtn.pack(pady=5)
- pythonpowered.pack(side=BOTTOM)
- # Maintain Window
- window.mainloop()
+ dialog = VersionDialog(
+ app_name="ProgramVer",
+ version="2.0.0",
+ copyright_text=(
+ "Copyright (C) 2017-2026 Dog Face Development Co.\n"
+ "All rights reserved in all countries.\n"
+ "ProgramVer and its code, user interface and all other associated\n"
+ "trademarks are protected by trademarks and copyright in Canada,\n"
+ "the United States and other countries."
+ ),
+ license_path=os.path.join(base_dir, "LICENSE.md"),
+ eula_path=os.path.join(base_dir, "EULA.md"),
+ license_blurb=(
+ "\nProgramVer - Version window for DFD Co.'s programs\n"
+ "Copyright (C) 2017-2026 Dog Face Development Company\n\n"
+ "This program is free software: you can redistribute it and/or modify\n"
+ "it under the terms of the GNU General Public License as published by\n"
+ "the Free Software Foundation, version 3 of the License.\n\n"
+ "This program is distributed in the hope that it will be useful,\n"
+ "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
+ "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n"
+ "GNU General Public License below for more details."
+ ),
+ show_python_powered=True,
+ window_title="Copyright & Version Info for ProgramVer",
+ )
+ dialog.show()
+
+
+if __name__ == "__main__":
+ ProgramVer()
diff --git a/mkdocs.yml b/mkdocs.yml
deleted file mode 100644
index 0b5165e..0000000
--- a/mkdocs.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-# Documentation configuration for ProgramVer.
-
-INHERIT: .mkdocs-shared/shared/mkdocs.base.yml
-
-site_name: 'ProgramVer'
-site_description: 'A Python version of Microsoft''s ''winver'', built to be customizable, and to show copyright info and licenses.'
-site_author: willtheorangeguy
-site_url: https://williamvdg.me/ProgramVer/
-
-repo_url: https://github.com/willtheorangeguy/ProgramVer
-repo_name: willtheorangeguy/ProgramVer
-edit_uri: edit/master/docs/
-
-copyright: Copyright © willtheorangeguy
-
-nav:
- - Home: index.md
- - 'Quickstart': 'quickstart.md'
- - 'Installation': 'installation.md'
- - 'Configuration': 'configuration.md'
- - 'Architecture': 'architecture.md'
- - 'Development': 'development.md'
- - 'Testing': 'testing.md'
- - 'FAQ': 'faq.md'
- - 'Troubleshooting': 'troubleshooting.md'
- - 'Roadmap': 'roadmap.md'
- - 'ProgramVer': 'internal/known-issues.md'
- - Changelog: changelog.md
- - License: license.md
- - Contributing: https://github.com/willtheorangeguy/.github/blob/main/CONTRIBUTING.md
- - Code of Conduct: https://github.com/willtheorangeguy/.github/blob/main/CODE_OF_CONDUCT.md
- - Security: https://github.com/willtheorangeguy/.github/blob/main/SECURITY.md
-
-exclude_docs: |
- docs.instructions.md
\ No newline at end of file
diff --git a/overrides/.gitkeep b/overrides/.gitkeep
deleted file mode 100644
index aed728f..0000000
--- a/overrides/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-# Shared overrides are staged during documentation builds.
diff --git a/programver/__init__.py b/programver/__init__.py
new file mode 100644
index 0000000..2a5416c
--- /dev/null
+++ b/programver/__init__.py
@@ -0,0 +1,6 @@
+"""ProgramVer - A customizable version dialog for Python applications."""
+
+from programver.dialog import VersionDialog
+
+__all__ = ["VersionDialog"]
+__version__ = "2.0.0"
diff --git a/programver/__main__.py b/programver/__main__.py
new file mode 100644
index 0000000..3cce727
--- /dev/null
+++ b/programver/__main__.py
@@ -0,0 +1,30 @@
+"""Entry point for python -m programver."""
+
+import os
+from programver.dialog import VersionDialog
+
+
+def main():
+ """Run ProgramVer demo dialog."""
+ base_dir = os.path.dirname(os.path.abspath(__file__))
+ license_path = os.path.join(base_dir, "..", "LICENSE.md")
+
+ dialog = VersionDialog(
+ app_name="ProgramVer",
+ version="2.0.0",
+ copyright_text=(
+ "Copyright (C) 2017-2026 willtheorangeguy.\n"
+ "All rights reserved."
+ ),
+ license_path=license_path,
+ license_blurb=(
+ "ProgramVer - A customizable version dialog for Python applications.\n"
+ "This project is licensed under the MIT License."
+ ),
+ show_python_powered=True,
+ )
+ dialog.show()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/programver/_text_viewer.py b/programver/_text_viewer.py
new file mode 100644
index 0000000..e20f252
--- /dev/null
+++ b/programver/_text_viewer.py
@@ -0,0 +1,27 @@
+"""Read-only scrollable text viewer window."""
+
+import tkinter as tk
+
+
+class TextViewer:
+ """Displays a file's contents in a read-only, scrollable Toplevel window."""
+
+ def __init__(self, parent, title, file_path):
+ self.window = tk.Toplevel(parent)
+ self.window.title(title)
+
+ frame = tk.Frame(self.window)
+ frame.pack(fill=tk.BOTH, expand=True)
+
+ scrollbar = tk.Scrollbar(frame)
+ scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
+
+ text_widget = tk.Text(frame, yscrollcommand=scrollbar.set, wrap=tk.WORD)
+ text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
+ scrollbar.config(command=text_widget.yview)
+
+ with open(file_path, "r", encoding="UTF-8") as f:
+ content = f.read()
+
+ text_widget.insert(tk.INSERT, content)
+ text_widget.config(state=tk.DISABLED)
diff --git a/programver/_utils.py b/programver/_utils.py
new file mode 100644
index 0000000..6fd948e
--- /dev/null
+++ b/programver/_utils.py
@@ -0,0 +1,27 @@
+"""Internal utilities for ProgramVer."""
+
+import os
+import tkinter as tk
+
+
+def get_or_create_root():
+ """Detect whether a Tk root already exists.
+
+ Returns:
+ tuple: (window, is_standalone)
+ - If Tk root exists: (Toplevel(root), False)
+ - If no Tk root: (Tk(), True)
+ """
+ try:
+ existing_root = tk._default_root
+ if existing_root is not None and existing_root.winfo_exists():
+ return tk.Toplevel(existing_root), False
+ except Exception:
+ pass
+ root = tk.Tk()
+ return root, True
+
+
+def get_bundled_image_path(filename):
+ """Get path to a bundled image in the programver package."""
+ return os.path.join(os.path.dirname(os.path.abspath(__file__)), "imgs", filename)
diff --git a/programver/dialog.py b/programver/dialog.py
new file mode 100644
index 0000000..28587b8
--- /dev/null
+++ b/programver/dialog.py
@@ -0,0 +1,89 @@
+"""Core VersionDialog class for ProgramVer."""
+
+import tkinter as tk
+
+from programver._utils import get_or_create_root, get_bundled_image_path
+from programver._text_viewer import TextViewer
+
+
+class VersionDialog:
+ """A customizable 'winver'-style version information dialog.
+
+ Can be used standalone (creates its own Tk root and runs mainloop)
+ or embedded in an existing tkinter application (uses Toplevel).
+ """
+
+ def __init__(
+ self,
+ app_name,
+ version,
+ copyright_text,
+ license_path=None,
+ eula_path=None,
+ license_blurb=None,
+ logo_path=None,
+ show_python_powered=True,
+ window_title=None,
+ ):
+ self.app_name = app_name
+ self.version = version
+ self.copyright_text = copyright_text
+ self.license_path = license_path
+ self.eula_path = eula_path
+ self.license_blurb = license_blurb
+ self.logo_path = logo_path
+ self.show_python_powered = show_python_powered
+ self.window_title = window_title or f"About {app_name}"
+
+ def show(self):
+ """Display the version dialog.
+
+ If a Tk root already exists, creates a Toplevel window.
+ If no Tk root exists, creates Tk root and calls mainloop().
+ """
+ window, is_standalone = get_or_create_root()
+ window.title(self.window_title)
+
+ if self.logo_path:
+ logo_img = tk.PhotoImage(file=self.logo_path)
+ window._logo_img = logo_img
+ logo_label = tk.Label(window, image=logo_img)
+ logo_label.pack(side=tk.TOP)
+
+ info = tk.Label(
+ window, text=f"{self.app_name}\nVersion: {self.version}"
+ )
+ info.pack(side=tk.TOP)
+
+ trademarks = tk.Label(window, text=self.copyright_text)
+ trademarks.pack(side=tk.TOP)
+
+ if self.license_blurb:
+ blurb = tk.Label(window, text=self.license_blurb)
+ blurb.pack(side=tk.TOP)
+
+ if self.license_path:
+ license_btn = tk.Button(
+ window,
+ text="Open License",
+ command=lambda: TextViewer(window, "License", self.license_path),
+ )
+ license_btn.pack(pady=5)
+
+ if self.eula_path:
+ eula_btn = tk.Button(
+ window,
+ text="Open EULA",
+ command=lambda: TextViewer(window, "EULA", self.eula_path),
+ )
+ eula_btn.pack(pady=5)
+
+ if self.show_python_powered:
+ badge_path = get_bundled_image_path("pythonpoweredlengthgif.gif")
+ badge_img = tk.PhotoImage(file=badge_path)
+ window._badge_img = badge_img
+ badge_label = tk.Label(window, image=badge_img)
+ badge_label.pack(side=tk.BOTTOM)
+
+ if is_standalone:
+ window.mainloop()
diff --git a/programver/imgs/__init__.py b/programver/imgs/__init__.py
new file mode 100644
index 0000000..dfe20c9
--- /dev/null
+++ b/programver/imgs/__init__.py
@@ -0,0 +1 @@
+"""Image assets for ProgramVer."""
diff --git a/imgs/pythonpoweredlengthgif.gif b/programver/imgs/pythonpoweredlengthgif.gif
similarity index 100%
rename from imgs/pythonpoweredlengthgif.gif
rename to programver/imgs/pythonpoweredlengthgif.gif
diff --git a/pyproject.toml b/pyproject.toml
index be176a9..d9a9aff 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,11 +4,11 @@ requires = ["setuptools", "wheel"]
[project]
name = "ProgramVer"
-version = "1.9.0"
+version = "2.0.0"
authors = [
{ name= "willtheorangeguy" },
]
-description = "A Python version of Microsoft's 'winver', built to be customizable, and to show copyright info and licenses."
+description = "A customizable version dialog for Python applications, inspired by Microsoft's winver."
readme = "README.md"
license = { file="LICENSE.md" }
requires-python = ">=3.9"
@@ -24,11 +24,14 @@ classifiers = [
"Homepage" = "https://github.com/willtheorangeguy/ProgramVer"
"Bug Tracker" = "https://github.com/willtheorangeguy/ProgramVer/issues"
+[project.scripts]
+programver = "programver.__main__:main"
+
[tool.setuptools]
include-package-data = true
[tool.setuptools.packages.find]
-where = ["imgs"]
-[tool.bandit]
-exclude_dirs = ["tests", "test", ".venv", "venv", "build", "node_modules"]
-skips = ["B101"]
+include = ["programver", "programver.*"]
+
+[tool.setuptools.package-data]
+"programver.imgs" = ["*.gif", "*.png"]
diff --git a/requirements.txt b/requirements.txt
index 0e61a9d..f165c42 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
# Project Requirements
-pytest>=9.1.1
+pytest>=8.4.2
pytest-cov>=7.1.0
diff --git a/setup.cfg b/setup.cfg
index 5c00bca..276b000 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,16 +1,21 @@
[metadata]
name = programver
-version = 1.9.0
+version = 2.0.0
[options]
packages = find:
-package_dir =
- = imgs
include_package_data = True
[options.packages.find]
-where = imgs
+include =
+ programver
+ programver.*
+
+[options.package_data]
+programver.imgs =
+ *.gif
+ *.png
[options.entry_points]
console_scripts =
- programver = main:ProgramVer
\ No newline at end of file
+ programver = programver.__main__:main
diff --git a/setup.py b/setup.py
index 2e97d2e..4ff21d1 100644
--- a/setup.py
+++ b/setup.py
@@ -11,10 +11,11 @@ def readme():
setup(
name="programver",
- version="1.9.0",
- description="A Python version of Microsoft's 'winver', \
- built to be customizable, and to show copyright info and licenses.",
+ version="2.0.0",
+ description="A customizable version dialog for Python applications, "
+ "inspired by Microsoft's winver.",
long_description=readme(),
+ long_description_content_type="text/markdown",
classifiers=[
"Development Status :: 5 - Production/Stable",
"License :: OSI Approved :: MIT License",
@@ -25,9 +26,8 @@ def readme():
keywords="program version windows winver microsoft license gui",
url="https://github.com/willtheorangeguy/ProgramVer",
author="willtheorangeguy",
- packages=find_packages(where="imgs"),
- package_dir={"": "imgs"},
+ packages=find_packages(include=["programver", "programver.*"]),
+ package_data={"programver.imgs": ["*.gif", "*.png"]},
include_package_data=True,
- py_modules=["main"],
- entry_points={"console_scripts": ["programver=main:ProgramVer"]},
+ entry_points={"console_scripts": ["programver=programver.__main__:main"]},
)
diff --git a/tests/test_main.py b/tests/test_main.py
index 5576bbf..c967408 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1,6 +1,6 @@
"""
-Tests for ProgramVer main module.
-Copyright (C) 2017-2024 Dog Face Development Co.
+Tests for ProgramVer package.
+Copyright (C) 2017-2026 Dog Face Development Co.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -8,262 +8,532 @@
"""
# pylint: disable=import-error, invalid-name, wrong-import-position, import-outside-toplevel, unused-argument
-# unused-argument is disabled because @patch decorators inject mocked objects as parameters
-# even when not all mocks are used in every test
import unittest
-from unittest.mock import Mock, patch, mock_open
-import os
-import sys
+from unittest.mock import Mock, patch, mock_open, PropertyMock
+import tkinter as tk
+
+
+class TestVersionDialogInit(unittest.TestCase):
+ """Test cases for VersionDialog.__init__."""
+
+ def test_required_params_stored(self):
+ """Test that required parameters are stored correctly."""
+ from programver.dialog import VersionDialog
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="1.0.0",
+ copyright_text="Copyright 2024",
+ )
+ self.assertEqual(dialog.app_name, "TestApp")
+ self.assertEqual(dialog.version, "1.0.0")
+ self.assertEqual(dialog.copyright_text, "Copyright 2024")
+
+ def test_optional_params_default_none(self):
+ """Test that optional parameters default to None or expected values."""
+ from programver.dialog import VersionDialog
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="1.0.0",
+ copyright_text="Copyright 2024",
+ )
+ self.assertIsNone(dialog.license_path)
+ self.assertIsNone(dialog.eula_path)
+ self.assertIsNone(dialog.license_blurb)
+ self.assertIsNone(dialog.logo_path)
+ self.assertTrue(dialog.show_python_powered)
+ self.assertEqual(dialog.window_title, "About TestApp")
+
+ def test_custom_window_title(self):
+ """Test that custom window title is stored."""
+ from programver.dialog import VersionDialog
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="1.0.0",
+ copyright_text="Copyright 2024",
+ window_title="Custom Title",
+ )
+ self.assertEqual(dialog.window_title, "Custom Title")
+
+ def test_all_optional_params(self):
+ """Test that all optional parameters are stored correctly."""
+ from programver.dialog import VersionDialog
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="2.0.0",
+ copyright_text="Copyright 2024",
+ license_path="/path/to/LICENSE",
+ eula_path="/path/to/EULA",
+ license_blurb="MIT License",
+ logo_path="/path/to/logo.gif",
+ show_python_powered=False,
+ window_title="About Test",
+ )
+ self.assertEqual(dialog.license_path, "/path/to/LICENSE")
+ self.assertEqual(dialog.eula_path, "/path/to/EULA")
+ self.assertEqual(dialog.license_blurb, "MIT License")
+ self.assertEqual(dialog.logo_path, "/path/to/logo.gif")
+ self.assertFalse(dialog.show_python_powered)
+ self.assertEqual(dialog.window_title, "About Test")
+
+
+class TestVersionDialogShow(unittest.TestCase):
+ """Test cases for VersionDialog.show method."""
+
+ @patch("programver.dialog.get_bundled_image_path", return_value="fake.gif")
+ @patch("programver.dialog.tk.PhotoImage")
+ @patch("programver.dialog.tk.Button")
+ @patch("programver.dialog.tk.Label")
+ @patch("programver.dialog.get_or_create_root")
+ def test_show_standalone_calls_mainloop(
+ self, mock_root, mock_label, mock_button, mock_photo, mock_path
+ ):
+ """Test that show() calls mainloop in standalone mode."""
+ from programver.dialog import VersionDialog
-# Add parent directory to path for imports
-sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
+ mock_window = Mock()
+ mock_root.return_value = (mock_window, True)
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="1.0.0",
+ copyright_text="Copyright 2024",
+ show_python_powered=False,
+ )
+ dialog.show()
+
+ mock_window.mainloop.assert_called_once()
+
+ @patch("programver.dialog.get_bundled_image_path", return_value="fake.gif")
+ @patch("programver.dialog.tk.PhotoImage")
+ @patch("programver.dialog.tk.Button")
+ @patch("programver.dialog.tk.Label")
+ @patch("programver.dialog.get_or_create_root")
+ def test_show_embedded_no_mainloop(
+ self, mock_root, mock_label, mock_button, mock_photo, mock_path
+ ):
+ """Test that show() does not call mainloop in embedded mode."""
+ from programver.dialog import VersionDialog
-from main import openLicense, openEULA, ProgramVer, get_resource_path
+ mock_window = Mock()
+ mock_root.return_value = (mock_window, False)
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="1.0.0",
+ copyright_text="Copyright 2024",
+ show_python_powered=False,
+ )
+ dialog.show()
+
+ mock_window.mainloop.assert_not_called()
+
+ @patch("programver.dialog.get_bundled_image_path", return_value="fake.gif")
+ @patch("programver.dialog.tk.PhotoImage")
+ @patch("programver.dialog.tk.Button")
+ @patch("programver.dialog.tk.Label")
+ @patch("programver.dialog.get_or_create_root")
+ def test_show_sets_window_title(
+ self, mock_root, mock_label, mock_button, mock_photo, mock_path
+ ):
+ """Test that show() sets the window title."""
+ from programver.dialog import VersionDialog
+ mock_window = Mock()
+ mock_root.return_value = (mock_window, True)
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="1.0.0",
+ copyright_text="Copyright 2024",
+ window_title="Custom Title",
+ show_python_powered=False,
+ )
+ dialog.show()
+
+ mock_window.title.assert_called_once_with("Custom Title")
+
+ @patch("programver.dialog.get_bundled_image_path", return_value="fake.gif")
+ @patch("programver.dialog.tk.PhotoImage")
+ @patch("programver.dialog.tk.Label")
+ @patch("programver.dialog.get_or_create_root")
+ def test_show_creates_info_label(self, mock_root, mock_label, mock_photo, mock_path):
+ """Test that show() creates an info label with app name and version."""
+ from programver.dialog import VersionDialog
-class TestGetResourcePath(unittest.TestCase):
- """Test cases for get_resource_path helper function."""
+ mock_window = Mock()
+ mock_root.return_value = (mock_window, True)
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="3.0.0",
+ copyright_text="Copyright 2024",
+ show_python_powered=False,
+ )
+ dialog.show()
+
+ label_calls = mock_label.call_args_list
+ label_texts = [call[1].get("text", "") for call in label_calls]
+ self.assertTrue(
+ any("TestApp" in t and "3.0.0" in t for t in label_texts)
+ )
+
+ @patch("programver.dialog.get_bundled_image_path", return_value="fake.gif")
+ @patch("programver.dialog.tk.PhotoImage")
+ @patch("programver.dialog.tk.Button")
+ @patch("programver.dialog.tk.Label")
+ @patch("programver.dialog.get_or_create_root")
+ def test_show_license_button_when_path_provided(
+ self, mock_root, mock_label, mock_button, mock_photo, mock_path
+ ):
+ """Test that license button appears when license_path is set."""
+ from programver.dialog import VersionDialog
- def test_get_resource_path_returns_absolute_path(self):
- """Test that get_resource_path returns an absolute path."""
- result = get_resource_path("LICENSE.txt")
- self.assertTrue(os.path.isabs(result))
+ mock_window = Mock()
+ mock_root.return_value = (mock_window, True)
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="1.0.0",
+ copyright_text="Copyright 2024",
+ license_path="/path/to/LICENSE",
+ show_python_powered=False,
+ )
+ dialog.show()
+
+ button_texts = [call[1].get("text", "") for call in mock_button.call_args_list]
+ self.assertIn("Open License", button_texts)
+
+ @patch("programver.dialog.get_bundled_image_path", return_value="fake.gif")
+ @patch("programver.dialog.tk.PhotoImage")
+ @patch("programver.dialog.tk.Button")
+ @patch("programver.dialog.tk.Label")
+ @patch("programver.dialog.get_or_create_root")
+ def test_show_no_license_button_when_path_none(
+ self, mock_root, mock_label, mock_button, mock_photo, mock_path
+ ):
+ """Test that license button is hidden when license_path is None."""
+ from programver.dialog import VersionDialog
- def test_get_resource_path_includes_filename(self):
- """Test that get_resource_path includes the filename."""
- result = get_resource_path("LICENSE.txt")
- self.assertTrue(result.endswith("LICENSE.txt"))
+ mock_window = Mock()
+ mock_root.return_value = (mock_window, True)
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="1.0.0",
+ copyright_text="Copyright 2024",
+ show_python_powered=False,
+ )
+ dialog.show()
+
+ button_texts = [call[1].get("text", "") for call in mock_button.call_args_list]
+ self.assertNotIn("Open License", button_texts)
+
+ @patch("programver.dialog.get_bundled_image_path", return_value="fake.gif")
+ @patch("programver.dialog.tk.PhotoImage")
+ @patch("programver.dialog.tk.Button")
+ @patch("programver.dialog.tk.Label")
+ @patch("programver.dialog.get_or_create_root")
+ def test_show_eula_button_when_path_provided(
+ self, mock_root, mock_label, mock_button, mock_photo, mock_path
+ ):
+ """Test that EULA button appears when eula_path is set."""
+ from programver.dialog import VersionDialog
- def test_get_resource_path_handles_subdirectories(self):
- """Test that get_resource_path handles subdirectories correctly."""
- result = get_resource_path("imgs/dfdlogo.gif")
- self.assertTrue("imgs" in result)
- self.assertTrue(result.endswith("dfdlogo.gif"))
+ mock_window = Mock()
+ mock_root.return_value = (mock_window, True)
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="1.0.0",
+ copyright_text="Copyright 2024",
+ eula_path="/path/to/EULA",
+ show_python_powered=False,
+ )
+ dialog.show()
+
+ button_texts = [call[1].get("text", "") for call in mock_button.call_args_list]
+ self.assertIn("Open EULA", button_texts)
+
+ @patch("programver.dialog.get_bundled_image_path", return_value="fake.gif")
+ @patch("programver.dialog.tk.PhotoImage")
+ @patch("programver.dialog.tk.Button")
+ @patch("programver.dialog.tk.Label")
+ @patch("programver.dialog.get_or_create_root")
+ def test_show_no_eula_button_when_path_none(
+ self, mock_root, mock_label, mock_button, mock_photo, mock_path
+ ):
+ """Test that EULA button is hidden when eula_path is None."""
+ from programver.dialog import VersionDialog
+ mock_window = Mock()
+ mock_root.return_value = (mock_window, True)
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="1.0.0",
+ copyright_text="Copyright 2024",
+ show_python_powered=False,
+ )
+ dialog.show()
+
+ button_texts = [call[1].get("text", "") for call in mock_button.call_args_list]
+ self.assertNotIn("Open EULA", button_texts)
+
+ @patch("programver.dialog.get_bundled_image_path", return_value="fake.gif")
+ @patch("programver.dialog.tk.PhotoImage")
+ @patch("programver.dialog.tk.Label")
+ @patch("programver.dialog.get_or_create_root")
+ def test_show_python_powered_badge(self, mock_root, mock_label, mock_photo, mock_path):
+ """Test that Python Powered badge is shown when enabled."""
+ from programver.dialog import VersionDialog
-class TestOpenLicense(unittest.TestCase):
- """Test cases for openLicense function."""
+ mock_window = Mock()
+ mock_root.return_value = (mock_window, True)
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="1.0.0",
+ copyright_text="Copyright 2024",
+ show_python_powered=True,
+ )
+ dialog.show()
+
+ mock_path.assert_called_once_with("pythonpoweredlengthgif.gif")
+
+ @patch("programver.dialog.get_bundled_image_path", return_value="fake.gif")
+ @patch("programver.dialog.tk.PhotoImage")
+ @patch("programver.dialog.tk.Label")
+ @patch("programver.dialog.get_or_create_root")
+ def test_show_no_python_powered_badge(self, mock_root, mock_label, mock_photo, mock_path):
+ """Test that Python Powered badge is hidden when disabled."""
+ from programver.dialog import VersionDialog
- @patch("main.Text")
- @patch("main.Tk")
- @patch(
- "builtins.open", new_callable=mock_open, read_data="GNU GENERAL PUBLIC LICENSE"
- )
- def test_openLicense_creates_window(self, mock_file, mock_tk, mock_text):
- """Test that openLicense creates a window and reads LICENSE.txt."""
mock_window = Mock()
- mock_tk.return_value = mock_window
- mock_text_widget = Mock()
- mock_text.return_value = mock_text_widget
+ mock_root.return_value = (mock_window, True)
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="1.0.0",
+ copyright_text="Copyright 2024",
+ show_python_powered=False,
+ )
+ dialog.show()
+
+ mock_path.assert_not_called()
+
+ @patch("programver.dialog.get_bundled_image_path", return_value="fake.gif")
+ @patch("programver.dialog.tk.PhotoImage")
+ @patch("programver.dialog.tk.Label")
+ @patch("programver.dialog.get_or_create_root")
+ def test_show_logo_when_provided(self, mock_root, mock_label, mock_photo, mock_path):
+ """Test that logo is displayed when logo_path is provided."""
+ from programver.dialog import VersionDialog
- openLicense()
+ mock_window = Mock()
+ mock_root.return_value = (mock_window, True)
+
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="1.0.0",
+ copyright_text="Copyright 2024",
+ logo_path="/path/to/logo.gif",
+ show_python_powered=False,
+ )
+ dialog.show()
+
+ photo_calls = mock_photo.call_args_list
+ logo_files = [call[1].get("file", "") for call in photo_calls]
+ self.assertIn("/path/to/logo.gif", logo_files)
+
+ @patch("programver.dialog.get_bundled_image_path", return_value="fake.gif")
+ @patch("programver.dialog.tk.PhotoImage")
+ @patch("programver.dialog.tk.Label")
+ @patch("programver.dialog.get_or_create_root")
+ def test_show_no_logo_when_not_provided(self, mock_root, mock_label, mock_photo, mock_path):
+ """Test that no logo image is loaded when logo_path is None."""
+ from programver.dialog import VersionDialog
- # Verify window was created
- mock_tk.assert_called_once()
- # Verify file was opened with absolute path
- mock_file.assert_called_once()
- call_args = mock_file.call_args[0]
- self.assertTrue(call_args[0].endswith("LICENSE.txt"))
- # Verify window title was set
- mock_window.title.assert_called_once_with("License")
-
- @patch("main.Tk")
- @patch("builtins.open", new_callable=mock_open, read_data="Test License Content")
- @patch("main.Text")
- def test_openLicense_displays_content(self, mock_text, mock_file, mock_tk):
- """Test that openLicense displays license content."""
mock_window = Mock()
- mock_tk.return_value = mock_window
- mock_text_widget = Mock()
- mock_text.return_value = mock_text_widget
+ mock_root.return_value = (mock_window, True)
- openLicense()
+ dialog = VersionDialog(
+ app_name="TestApp",
+ version="1.0.0",
+ copyright_text="Copyright 2024",
+ show_python_powered=False,
+ )
+ dialog.show()
- # Verify text widget was created with window
- mock_text.assert_called_once_with(mock_window)
- # Verify content was inserted
- mock_text_widget.insert.assert_called_once()
- # Verify widget was packed
- mock_text_widget.pack.assert_called_once()
+ mock_photo.assert_not_called()
-class TestOpenEULA(unittest.TestCase):
- """Test cases for openEULA function."""
+class TestTextViewer(unittest.TestCase):
+ """Test cases for TextViewer read-only text window."""
+
+ @patch("programver._text_viewer.tk.Text")
+ @patch("programver._text_viewer.tk.Scrollbar")
+ @patch("programver._text_viewer.tk.Frame")
+ @patch("programver._text_viewer.tk.Toplevel")
+ @patch("builtins.open", new_callable=mock_open, read_data="Test license content")
+ def test_uses_toplevel_not_tk(
+ self, mock_file, mock_toplevel, mock_frame, mock_scrollbar, mock_text
+ ):
+ """Test that TextViewer uses Toplevel, not Tk."""
+ from programver._text_viewer import TextViewer
- @patch("main.Text")
- @patch("main.Tk")
- @patch(
- "builtins.open", new_callable=mock_open, read_data="END USER LICENSE AGREEMENT"
- )
- def test_openEULA_creates_window(self, mock_file, mock_tk, mock_text):
- """Test that openEULA creates a window and reads EULA.txt."""
+ mock_parent = Mock()
mock_window = Mock()
- mock_tk.return_value = mock_window
- mock_text_widget = Mock()
- mock_text.return_value = mock_text_widget
+ mock_toplevel.return_value = mock_window
- openEULA()
+ TextViewer(mock_parent, "License", "/path/to/file")
- # Verify window was created
- mock_tk.assert_called_once()
- # Verify file was opened with absolute path
- mock_file.assert_called_once()
- call_args = mock_file.call_args[0]
- self.assertTrue(call_args[0].endswith("EULA.txt"))
- # Verify window title was set
- mock_window.title.assert_called_once_with("EULA")
+ mock_toplevel.assert_called_once_with(mock_parent)
+
+ @patch("programver._text_viewer.tk.Text")
+ @patch("programver._text_viewer.tk.Scrollbar")
+ @patch("programver._text_viewer.tk.Frame")
+ @patch("programver._text_viewer.tk.Toplevel")
+ @patch("builtins.open", new_callable=mock_open, read_data="Test content")
+ def test_sets_window_title(
+ self, mock_file, mock_toplevel, mock_frame, mock_scrollbar, mock_text
+ ):
+ """Test that TextViewer sets the window title."""
+ from programver._text_viewer import TextViewer
- @patch("main.Tk")
- @patch("builtins.open", new_callable=mock_open, read_data="Test EULA Content")
- @patch("main.Text")
- def test_openEULA_displays_content(self, mock_text, mock_file, mock_tk):
- """Test that openEULA displays EULA content."""
mock_window = Mock()
- mock_tk.return_value = mock_window
+ mock_toplevel.return_value = mock_window
+
+ TextViewer(Mock(), "EULA", "/path/to/file")
+
+ mock_window.title.assert_called_once_with("EULA")
+
+ @patch("programver._text_viewer.tk.Text")
+ @patch("programver._text_viewer.tk.Scrollbar")
+ @patch("programver._text_viewer.tk.Frame")
+ @patch("programver._text_viewer.tk.Toplevel")
+ @patch("builtins.open", new_callable=mock_open, read_data="File contents here")
+ def test_inserts_file_content(
+ self, mock_file, mock_toplevel, mock_frame, mock_scrollbar, mock_text
+ ):
+ """Test that TextViewer inserts the file contents."""
+ from programver._text_viewer import TextViewer
+
mock_text_widget = Mock()
mock_text.return_value = mock_text_widget
- openEULA()
+ TextViewer(Mock(), "License", "/path/to/file")
- # Verify text widget was created with window
- mock_text.assert_called_once_with(mock_window)
- # Verify content was inserted
- mock_text_widget.insert.assert_called_once()
- # Verify widget was packed
- mock_text_widget.pack.assert_called_once()
+ mock_text_widget.insert.assert_called_once_with(tk.INSERT, "File contents here")
+ @patch("programver._text_viewer.tk.Text")
+ @patch("programver._text_viewer.tk.Scrollbar")
+ @patch("programver._text_viewer.tk.Frame")
+ @patch("programver._text_viewer.tk.Toplevel")
+ @patch("builtins.open", new_callable=mock_open, read_data="Content")
+ def test_text_is_disabled(
+ self, mock_file, mock_toplevel, mock_frame, mock_scrollbar, mock_text
+ ):
+ """Test that TextViewer sets text widget to DISABLED (read-only)."""
+ from programver._text_viewer import TextViewer
-class TestProgramVer(unittest.TestCase):
- """Test cases for ProgramVer function."""
+ mock_text_widget = Mock()
+ mock_text.return_value = mock_text_widget
- @patch("main.Tk")
- @patch("main.PhotoImage")
- @patch("main.Label")
- @patch("main.Button")
- def test_programver_creates_window(
- self, mock_button, mock_label, mock_photoimage, mock_tk
+ TextViewer(Mock(), "License", "/path/to/file")
+
+ mock_text_widget.config.assert_called_once_with(state=tk.DISABLED)
+
+ @patch("programver._text_viewer.tk.Text")
+ @patch("programver._text_viewer.tk.Scrollbar")
+ @patch("programver._text_viewer.tk.Frame")
+ @patch("programver._text_viewer.tk.Toplevel")
+ @patch("builtins.open", new_callable=mock_open, read_data="Content")
+ def test_scrollbar_is_attached(
+ self, mock_file, mock_toplevel, mock_frame, mock_scrollbar, mock_text
):
- """Test that ProgramVer creates main window with all components."""
- mock_window = Mock()
- mock_tk.return_value = mock_window
- mock_img = Mock()
- mock_photoimage.return_value = mock_img
+ """Test that TextViewer creates and attaches a scrollbar."""
+ from programver._text_viewer import TextViewer
- # Mock mainloop to prevent blocking
- mock_window.mainloop = Mock()
+ mock_sb = Mock()
+ mock_scrollbar.return_value = mock_sb
- ProgramVer()
+ TextViewer(Mock(), "License", "/path/to/file")
- # Verify window was created
- mock_tk.assert_called_once()
- # Verify window title was set
- mock_window.title.assert_called_once()
- title_text = mock_window.title.call_args[0][0]
- assert "ProgramVer" in title_text
-
- @patch("main.Tk")
- @patch("main.PhotoImage")
- @patch("main.Label")
- @patch("main.Button")
- def test_programver_loads_images(
- self, mock_button, mock_label, mock_photoimage, mock_tk
- ):
- """Test that ProgramVer loads required images."""
- mock_window = Mock()
- mock_tk.return_value = mock_window
- mock_window.mainloop = Mock()
-
- ProgramVer()
-
- # Verify PhotoImage was called to load images
- assert mock_photoimage.call_count == 2
- # Check that both images are loaded with absolute paths
- calls = mock_photoimage.call_args_list
- image_files = [call[1]["file"] for call in calls]
- assert any("dfdlogo.gif" in img for img in image_files)
- assert any("pythonpoweredlengthgif.gif" in img for img in image_files)
-
- @patch("main.Tk")
- @patch("main.PhotoImage")
- @patch("main.Label")
- @patch("main.Button")
- def test_programver_creates_labels(
- self, mock_button, mock_label, mock_photoimage, mock_tk
- ):
- """Test that ProgramVer creates appropriate labels."""
- mock_window = Mock()
- mock_tk.return_value = mock_window
- mock_window.mainloop = Mock()
+ mock_sb.pack.assert_called_once()
+ mock_sb.config.assert_called_once()
- ProgramVer()
- # Verify Label was called multiple times to create all labels
- assert mock_label.call_count >= 5
+class TestGetOrCreateRoot(unittest.TestCase):
+ """Test cases for get_or_create_root utility."""
+
+ @patch("programver._utils.tk.Tk")
+ @patch("programver._utils.tk._default_root", None)
+ def test_creates_tk_when_no_root(self, mock_tk):
+ """Test that a new Tk root is created when none exists."""
+ from programver._utils import get_or_create_root
- @patch("main.Tk")
- @patch("main.PhotoImage")
- @patch("main.Label")
- @patch("main.Button")
- def test_programver_creates_buttons(
- self, mock_button, mock_label, mock_photoimage, mock_tk
- ):
- """Test that ProgramVer creates license and EULA buttons."""
- mock_window = Mock()
- mock_tk.return_value = mock_window
- mock_window.mainloop = Mock()
-
- ProgramVer()
-
- # Verify Button was called for both buttons
- assert mock_button.call_count == 2
- # Verify buttons have correct text and commands
- calls = mock_button.call_args_list
- button_texts = [call[1]["text"] for call in calls]
- assert "Open License" in button_texts
- assert "Open EULA" in button_texts
-
- @patch("main.Tk")
- @patch("main.PhotoImage")
- @patch("main.Label")
- @patch("main.Button")
- def test_programver_button_commands(
- self, mock_button, mock_label, mock_photoimage, mock_tk
- ):
- """Test that buttons are linked to correct command functions."""
mock_window = Mock()
mock_tk.return_value = mock_window
- mock_window.mainloop = Mock()
- ProgramVer()
+ window, is_standalone = get_or_create_root()
- calls = mock_button.call_args_list
- commands = [call[1].get("command") for call in calls]
- # Verify that openLicense and openEULA are set as commands
- assert openLicense in commands
- assert openEULA in commands
+ mock_tk.assert_called_once()
+ self.assertTrue(is_standalone)
+
+ @patch("programver._utils.tk.Toplevel")
+ def test_creates_toplevel_when_root_exists(self, mock_toplevel):
+ """Test that Toplevel is created when a root already exists."""
+ from programver._utils import get_or_create_root
+
+ mock_root = Mock()
+ mock_root.winfo_exists.return_value = True
+ mock_toplevel_window = Mock()
+ mock_toplevel.return_value = mock_toplevel_window
+
+ with patch("programver._utils.tk._default_root", mock_root):
+ window, is_standalone = get_or_create_root()
+
+ mock_toplevel.assert_called_once_with(mock_root)
+ self.assertFalse(is_standalone)
class TestModuleIntegration(unittest.TestCase):
- """Integration tests for the module."""
+ """Integration tests for the package."""
+
+ def test_package_imports(self):
+ """Test that the programver package can be imported."""
+ import programver
+
+ self.assertTrue(hasattr(programver, "VersionDialog"))
+ self.assertTrue(hasattr(programver, "__version__"))
+
+ def test_version_dialog_is_class(self):
+ """Test that VersionDialog is a class with a show method."""
+ from programver import VersionDialog
+
+ self.assertTrue(callable(VersionDialog))
+ dialog = VersionDialog(
+ app_name="Test", version="1.0", copyright_text="Copyright"
+ )
+ self.assertTrue(hasattr(dialog, "show"))
+ self.assertTrue(callable(dialog.show))
- def test_module_imports(self):
- """Test that the main module can be imported successfully."""
- import main
+ def test_version_string(self):
+ """Test that __version__ is a string."""
+ import programver
- assert hasattr(main, "ProgramVer")
- assert hasattr(main, "openLicense")
- assert hasattr(main, "openEULA")
- assert hasattr(main, "get_resource_path")
+ self.assertIsInstance(programver.__version__, str)
- def test_functions_are_callable(self):
- """Test that all exported functions are callable."""
- import main
+ def test_main_module_imports(self):
+ """Test that main.py's ProgramVer function is importable."""
+ from main import ProgramVer
- assert callable(main.ProgramVer)
- assert callable(main.openLicense)
- assert callable(main.openEULA)
- assert callable(main.get_resource_path)
+ self.assertTrue(callable(ProgramVer))
if __name__ == "__main__":