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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/scripts/tag_release.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# tag_release.py
import sys
import subprocess
import tomli # Or `tomli` for Python < 3.11

# 1. Read the version from the single source of truth
with open("pyproject.toml", "rb") as f:
pyproject_data = tomli.load(f)
version = pyproject_data["project"]["version"]

# 2. Construct the tag and the git command
tag = f"v{version}"
print(f"Found version {version}. Creating tag: {tag}")

# 3. Run the git command
try:
subprocess.run(["git", "tag", tag], check=True)
except subprocess.CalledProcessError:
print(f"Error: Could not create tag. Does the tag '{tag}' already exist?")
sys.exit(1)
except FileNotFoundError:
print("Error: 'git' command not found. Is Git installed and in your PATH?")
sys.exit(1)
else:
print(f"Successfully created tag '{tag}'.")
sys.exit(0)
30 changes: 30 additions & 0 deletions .github/scripts/validate_main.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail

# Check if on main branch
CURRENT_BRANCH=$(git branch --show-current)
if [ "$CURRENT_BRANCH" != "main" ]; then
echo "Error: You are not on the main branch. Please switch to main."
exit 1
fi

# Check for uncommitted changes
if [ -n "$(git status --porcelain)" ]; then
echo "Error: There are uncommitted changes. Please commit or stash them."
exit 1
fi

# Fetch latest changes from remote
git fetch

# Check if local main is up to date with origin/main
if ! git rev-parse origin/main > /dev/null 2>&1; then
echo "Error: Remote branch origin/main does not exist. Please set up a remote tracking branch."
exit 1
fi
LOCAL_HASH=$(git rev-parse main)
REMOTE_HASH=$(git rev-parse origin/main)
if [ "$LOCAL_HASH" != "$REMOTE_HASH" ]; then
echo "Error: Your local main branch is not up to date with origin/main. Please pull the latest changes."
exit 1
fi
38 changes: 38 additions & 0 deletions .github/workflows/ci-checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# .github/workflows/ci-checks.yml

name: CI Checks

on:
pull_request:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version-file: '.python-version'

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r ci-requirements.txt

- name: Lint code
run: ruff check src

- name: Run type checks
run: |
mypy src
basedpyright src

# add this when tests are ready
# - name: Run tests
# run: pytest
107 changes: 107 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# HOW THIS WORKFLOW WORKS
# This workflow automates creating a GitHub Release and publishing your Python package to PyPI.

# --- Manual Steps ---
# 1. Edit `pyproject.toml` to set the new version (e.g., `version = "1.2.3"` or `version = "1.2.4-rc1"` for pre-releases).
# 2. Commit the change and merge it to the `main` branch (From the feature branch or however you work).
# 3. On your local machine, fetch and pull the latest changes from `main` to ensure you're up to date.
# 4. Use the justfile: `just release` to trigger the process.

# --- Automation ---
# The workflow then automatically:
# - runs .github/scripts/tag_release.py to create a new tag based on the version in `pyproject.toml`.
# - Pushes the new tag to github which triggers this workflow.
# - Checks that the tag matches the version in `pyproject.toml`.
# - Builds the sdist and wheel.
# - Publishes the package to PyPI.
# - Creates a new GitHub Release based on the tag with logic for marking pre-releases.

name: Create Release and Publish to PyPI

on:
push:
tags:
- "v*" # Runs on any tag starting with "v", e.g., v1.2.3
workflow_dispatch:
inputs:
tag_name:
description: 'Tag name to create release for (e.g., v1.2.3)'
required: true
type: string

jobs:
build-and-publish:
name: Build and Publish
runs-on: ubuntu-latest
# These permissions are required for the actions below.
permissions:
id-token: write # Required for Trusted Publishing with PyPI (OIDC).
contents: write # Required to create the GitHub Release.

steps:
- name: Check out code
uses: actions/checkout@v4
with:
# If manually triggered, checkout the specific tag
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag_name || github.ref }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version-file: '.python-version'

- name: Install required python packages
run: python -m pip install --upgrade build tomli

- name: Set tag name
id: tag
# github.ref_name is the name of the tag that triggered this workflow,
# if it was triggered by a tag push and not manually.
# inputs.tag_name is used when the workflow is manually triggered.
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG_NAME="${{ inputs.tag_name }}"
else
TAG_NAME="${{ github.ref_name }}"
fi
echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT
echo "Using tag: $TAG_NAME"

- name: Verify tag matches version
run: |
TAG_VERSION=${{ steps.tag.outputs.tag_name }}
PYPROJECT_VERSION=$(python -c "import tomli; print(tomli.load(open('pyproject.toml', 'rb'))['project']['version'])")
if [ "v$PYPROJECT_VERSION" != "$TAG_VERSION" ]; then
echo "Error: Tag $TAG_VERSION does not match pyproject.toml version v$PYPROJECT_VERSION"
exit 1
fi

- name: Build package
run: python -m build

- name: Publish to PyPI
# This action uses Trusted Publishing, which is configured in your PyPI project settings.
# It avoids the need for storing API tokens as secrets.
uses: pypa/gh-action-pypi-publish@release/v1

- name: Extract changelog for release
id: changelog
run: |
VERSION=${{ steps.tag.outputs.tag_name }}
VERSION=${VERSION#v} # Strip leading "v"
awk "/## \[${VERSION//./\\.}\]/,/^## \[/" CHANGELOG.md | head -n -1 > body.md
# Read the content and set it as output
echo "RELEASE_BODY<<EOF" >> $GITHUB_OUTPUT
cat body.md >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.tag.outputs.tag_name }}
# The release title will be "Release vX.X.X" or "Pre-release vX.X.X-rc1"
# depending on whether a hyphen is present in the tag name.
release_name: ${{ startsWith(steps.tag.outputs.tag_name, 'v') && contains(steps.tag.outputs.tag_name, '-') && format('Pre-release {0}', steps.tag.outputs.tag_name) || format('Release {0}', steps.tag.outputs.tag_name) }}
# Marks the release as a "pre-release" on GitHub if the tag contains a hyphen (e.g., "-rc1").
prerelease: ${{ contains(steps.tag.outputs.tag_name, '-') }}
body: ${{ steps.changelog.outputs.RELEASE_BODY }}
15 changes: 11 additions & 4 deletions .github/workflows/update-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
name: Update Central Docs

on:
push:
branches:
- main
workflow_dispatch:
# Only trigger if the build workflow succeeded
workflow_run:
workflows: ["Create Release and Publish to PyPI"]
types:
- completed
# Manual triggering is allowed as well
# This allows manual updates to the docs without a new release
# To be more professional I should use some kind of 'docs' tag
# But this is fine for now.
workflow_dispatch:


jobs:
create_docs_pr:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
# Step 1: Check out the code of the library that triggered the workflow
- name: Checkout Library Repo
Expand Down
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.13
3.9
24 changes: 23 additions & 1 deletion Changelog.md → CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
# Textual-Window Changelog

## 0.6.0 (2025-07-21)
## [0.7.0] 2025-07-28

### Usage / API changes

- Upgraded to Textual 5.0.0.
- Removed Rich-Pyfiglet dependency, now using the new type-hinted Pyfiglet directly. This probably will not affect anybody.

### Code and project changes

- Renamed Changelog.md to CHANGELOG.md
- Added 2 workflow to .github/workflows:
- ci-checks.yml - runs Ruff, MyPy, BasedPyright (will add Pytest later)
- release.yml - Workflow to publish to PyPI and github releases
- Added 2 scripts to .github/scripts:
- adds .github/scripts/validate_main.sh
- adds .github/scripts/tag_release.py
- Added 1 new file to root: `ci-requirements.txt` - this is used by the ci-checks.yml workflow to install the dev dependencies.
- Added basedpyright as a dev dependency to help with type checking. Made the `just typecheck` command run it after MyPy and set it to 'strict' mode in the config (added [tool.basedpyright] section to pyproject.toml).
- Replaced build and publish commands in the justfile with a single release command that runs the two above scripts and then pushes the new tag to Github
- Workflow `update-docs.yml` now runs only if the `release.yml` workflow is successful, so it will only update the docs if a new release is made (Still possible to manually run it if needed, should add a 'docs' tag in the future for this purpose).
- Changed the `.python-version` file to use `3.9` instead of `3.12`.

## [0.6.0] (2025-07-21)

- Potentially Breaking change: The `WindowManager` class no longer inherits from `textual.dom.DOMNode`. This change was made to simplify the class and remove unnecessary complexity. The window manager is not mounted in the DOM. I was using it to give access to certain Textual widget features, but I just refactored the code to not need them anymore.
- Added EzPubSub to the `WindowManager` class. This is a new cross-framework pub-sub system that I built to facilitate communication between the window manager and other parts of the application that may not be Textual widgets (and thus could not use Textual's built-in pub-sub system). This allows for more flexible communication between the window manager and other parts of the application.
Expand Down
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ MKDOCS-END -->

# Textual-Window

[![badge](https://img.shields.io/badge/linted-Ruff-blue?style=for-the-badge&logo=ruff)](https://astral.sh/ruff)
[![badge](https://img.shields.io/badge/formatted-black-black?style=for-the-badge)](https://github.com/psf/black)
[![badge](https://img.shields.io/badge/type_checked-MyPy_(strict)-blue?style=for-the-badge&logo=python)](https://mypy-lang.org/)
[![badge](https://img.shields.io/badge/Type_checked-Pyright_(strict)-blue?style=for-the-badge&logo=python)](https://microsoft.github.io/pyright/)
[![badge](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](https://opensource.org/license/mit)
[![badge](https://img.shields.io/badge/framework-Textual-blue?style=for-the-badge)](https://textual.textualize.io/)
[![badge](https://img.shields.io/pypi/v/textual-window)](https://pypi.org/project/textual-window/)
[![badge](https://img.shields.io/github/v/release/edward-jazzhands/textual-window)](https://github.com/edward-jazzhands/textual-window/releases/latest)
[![badge](https://img.shields.io/badge/Requires_Python->=3.9-blue&logo=python)](https://python.org)
[![badge](https://img.shields.io/badge/Strictly_Typed-MyPy_&_Pyright-blue&logo=python)](https://mypy-lang.org/)
[![badge](https://img.shields.io/badge/license-MIT-blue)](https://opensource.org/license/mit)

Textual-Window is an extension library for [Textual](https://github.com/Textualize/textual).

Expand All @@ -43,6 +42,8 @@ Window widgets are floating, draggable, resizable, snappable, closable, and you

See the documentation for more details.

Note: This library is under pretty active development and so the API is subject to change. If you find a bug, please report it on the issues page.

## Demo App

If you have [uv](https://docs.astral.sh/uv/) or [pipx](https://pipx.pypa.io/stable/), you can immediately try the demo app:
Expand Down
6 changes: 6 additions & 0 deletions ci-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
textual
ezpubsub
black
ruff
mypy
basedpyright
51 changes: 28 additions & 23 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,45 @@ console:

# Runs ruff, exits with 0 if no issues are found
lint:
uv run ruff check src || (echo "Ruff found issues. Please address them." && exit 1)
@uv run ruff check src || (echo "Ruff found issues. Please address them." && exit 1)

# Runs mypy, exits with 0 if no issues are found
typecheck:
uv run mypy src || (echo "Mypy found issues. Please address them." && exit 1)
@uv run mypy src || (echo "Mypy found issues. Please address them." && exit 1)
@uv run basedpyright src || (echo "BasedPyright found issues. Please address them." && exit 1)

# Runs black
format:
uv run black src
@uv run black src

# Runs ruff, mypy, and black
all-checks: lint typecheck format
echo "All pre-commit checks passed. You're good to publish."

# Build the package, run clean first
build: clean
@echo "Building the package..."
uv build

# Publish the package, run build first
publish: build
@echo "Publishing the package..."
uv publish

# Remove the build and dist directories
echo "All pre-commit checks passed. You're good to PR to main."

# Remove build/dist directories and pyc files
clean:
rm -rf build dist
find . -name "*.pyc" -delete
rm -rf build dist
find . -name "*.pyc" -delete

# Remove tool caches
clean-caches:
rm -rf .mypy_cache
rm -rf .ruff_cache

# Remove the virtual environment and lock file
del-env:
rm -rf .venv
rm -rf uv.lock
rm -rf .venv
rm -rf uv.lock

# Removes all environment and build stuff
reset: clean clean-caches del-env install
@echo "Environment reset."

# Release the kraken
release:
bash .github/scripts/validate_main.sh && \
uv run .github/scripts/tag_release.py && \
git push --tags

reset: clean del-env install
@echo "Resetting the environment..."
#-------------------------------------------------------------------------------
sync-tags:
git fetch --prune origin "+refs/tags/*:refs/tags/*"
Loading