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
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
- name: Set up Python
run: uv python install ${{ matrix.python-version }}

- name: Create virtual environment
run: uv venv --python ${{ matrix.python-version }}

- name: Install dependencies
run: uv pip install -e ".[dev,images]"

Expand Down
62 changes: 50 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Presentation decks written as JSON, driven from your phone, and built into a
single HTML file that works when the Wi-Fi doesn't.

```bash
uv tool install glissade # or: pipx install glissade
uv tool install --from git+https://github.com/techmuch/glissade glissade

mkdir my-talk && cd my-talk
glissade init # scaffold a deck and an AI-agent guide
Expand All @@ -16,18 +16,26 @@ anywhere, and they document the whole system.

## Installing

Glissade is not on PyPI yet, so install it from GitHub for now.

| | |
| --- | --- |
| **uv** (recommended) | `uv tool install glissade` — installs Python too if you don't have it |
| **pipx** | `pipx install glissade` |
| **pip** | `pip install glissade` |
| **Try without installing** | `uvx glissade demo` |
| **uv** (recommended) | `uv tool install --from git+https://github.com/techmuch/glissade glissade` — installs Python too if you don't have it |
| **pipx** | `pipx install git+https://github.com/techmuch/glissade.git` |
| **pip** | `pip install git+https://github.com/techmuch/glissade.git` |
| **Try without installing** | `uvx --from git+https://github.com/techmuch/glissade glissade demo` |

One universal wheel covers Windows, macOS and Linux — every dependency is pure
Python, so installation never needs a compiler. Python 3.10 or newer.

Add `glissade[images]` to pull in Pillow, which downscales oversized images
before embedding them. Without it images embed at full size; nothing breaks.
If you want Pillow for image downscaling, install the `images` extra from Git
instead, for example:

```bash
pip install 'git+https://github.com/techmuch/glissade.git#egg=glissade[images]'
```

Without Pillow, images embed at full size; nothing breaks.

## Commands

Expand Down Expand Up @@ -270,12 +278,12 @@ Pick a `layout` and fill the fields it uses. Nothing is required.

Layouts: `title`, `title-content`, `section`, `title-only`, `two-content`,
`comparison`, `content-caption`, `picture-caption`, `media-right`,
`media-left`, `media-full`, `media-caption`, `grid`, `blank`.
`media-left`, `media-full`, `media-caption`, `grid`, `quad-chart`, `blank`.

`cls` adds modifiers independent of layout: `"ask"` (dark, for questions),
`"story"`, `"center"`.

Run `glissade demo --deck gallery` to see all fourteen.
Run `glissade demo --deck gallery` to see all fifteen.

## Media

Expand Down Expand Up @@ -333,16 +341,46 @@ presentation and an apology.

## Developing

Use `uv` plus the cross-platform task script:

```bash
git clone https://github.com/techmuch/glissade && cd glissade
uv venv && uv pip install -e ".[images]"
glissade demo
uv run python scripts/dev.py install
uv run python scripts/dev.py run
```

That installs Python 3.12 if needed, creates `.venv`, and installs the editable
project with the `dev` and `images` extras. Override the Python version if you
need to:

```bash
uv run python scripts/dev.py install --python 3.11
```

Common tasks:

```bash
uv run python scripts/dev.py install # bootstrap .venv
uv run python scripts/dev.py test # run the full test suite
uv run python scripts/dev.py test -- tests/test_check.py -q
uv run python scripts/dev.py run # defaults to: glissade demo
uv run python scripts/dev.py run -- start
uv run python scripts/dev.py build # build dist/ packages
```

If you prefer the raw `uv` commands:

```bash
uv python install 3.12
uv venv .venv --python 3.12
uv pip install --python .venv/bin/python -e ".[dev,images]" # Windows: .venv\Scripts\python.exe
uv run --python .venv/bin/python pytest -q # Windows: .venv\Scripts\python.exe
```

`src/glissade/` is the package; `templates/` holds the deck and remote HTML;
`data/` holds the themes, JSON schema, `init` scaffold and demo decks — all
shipped inside the wheel so the tool works from any directory.

```bash
python -m build # sdist + universal wheel into dist/
uv build # sdist + universal wheel into dist/
```
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "glissade"
version = "0.10.0"
version = "0.12.0"
description = "Presentation decks written as JSON, driven from your phone, built into a single self-contained HTML file"
readme = "README.md"
requires-python = ">=3.10"
Expand Down Expand Up @@ -46,6 +46,7 @@ images = ["pillow>=10.0"]
dev = [
"pytest>=8.0",
"httpx>=0.27",
"ruff>=0.6.0",
]

[tool.pytest.ini_options]
Expand Down
158 changes: 158 additions & 0 deletions scripts/dev.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import os
import shutil
import subprocess
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent
DEFAULT_PYTHON = "3.12"
DEFAULT_EXTRAS = "dev,images"


def uv() -> str:
found = shutil.which("uv")
if not found:
raise SystemExit("uv is required. Install it from https://docs.astral.sh/uv/")
return found


def venv_python(venv: Path) -> Path:
if os.name == "nt":
return venv / "Scripts" / "python.exe"
return venv / "bin" / "python"


def run(cmd: list[str], *, cwd: Path = ROOT) -> None:
print("+", " ".join(cmd))
subprocess.run(cmd, cwd=cwd, check=True)


def require_venv(venv: Path) -> Path:
python = venv_python(venv)
if not python.exists():
raise SystemExit(
f"{python} not found. Run `uv run python scripts/dev.py install` first."
)
return python


def cmd_install(args: argparse.Namespace) -> None:
venv = ROOT / args.venv
run([uv(), "python", "install", args.python])
run([uv(), "venv", str(venv), "--python", args.python])
run([
uv(),
"pip",
"install",
"--python",
str(venv_python(venv)),
"-e",
f".[{args.extras}]",
])


def passthrough(argv: list[str]) -> list[str]:
return argv[1:] if argv and argv[0] == "--" else argv


def cmd_test(args: argparse.Namespace) -> None:
python = require_venv(ROOT / args.venv)
extra = passthrough(args.args)
cmd = [str(python), "-m", "pytest"]
if not extra:
cmd.append("-q")
cmd.extend(extra)
run(cmd)


def cmd_run(args: argparse.Namespace) -> None:
python = require_venv(ROOT / args.venv)
glissade_args = passthrough(args.args) or ["demo"]
run([str(python), "-m", "glissade", *glissade_args])


def cmd_build(args: argparse.Namespace) -> None:
run([uv(), "build", *passthrough(args.args)])


def parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
description="Cross-platform developer tasks for Glissade."
)
sub = p.add_subparsers(dest="command", required=True)

install = sub.add_parser(
"install",
help="Install Python if needed, create .venv, and install dev dependencies.",
)
install.add_argument(
"--python",
default=DEFAULT_PYTHON,
help=f"Python version for the local venv (default: {DEFAULT_PYTHON})",
)
install.add_argument(
"--venv",
default=".venv",
help="Virtualenv directory (default: .venv)",
)
install.add_argument(
"--extras",
default=DEFAULT_EXTRAS,
help=f"Extras to install from the project (default: {DEFAULT_EXTRAS})",
)
install.set_defaults(func=cmd_install)

test = sub.add_parser("test", help="Run pytest inside the local .venv.")
test.add_argument(
"--venv",
default=".venv",
help="Virtualenv directory (default: .venv)",
)
test.add_argument(
"args",
nargs=argparse.REMAINDER,
help="Arguments passed through to pytest",
)
test.set_defaults(func=cmd_test)

run_p = sub.add_parser(
"run",
help="Run Glissade inside the local .venv. Defaults to `glissade demo`.",
)
run_p.add_argument(
"--venv",
default=".venv",
help="Virtualenv directory (default: .venv)",
)
run_p.add_argument(
"args",
nargs=argparse.REMAINDER,
help="Arguments passed through to `python -m glissade`",
)
run_p.set_defaults(func=cmd_run)

build = sub.add_parser(
"build",
help="Build distributable packages into dist/ using `uv build`.",
)
build.add_argument(
"args",
nargs=argparse.REMAINDER,
help="Arguments passed through to `uv build`",
)
build.set_defaults(func=cmd_build)

return p


def main() -> int:
args = parser().parse_args()
args.func(args)
return 0


if __name__ == "__main__":
raise SystemExit(main())
2 changes: 1 addition & 1 deletion src/glissade/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
glissade check validate decks before you rely on them
"""

__version__ = "0.10.0"
__version__ = "0.12.0"

from .project import ( # noqa: F401
DATA_DIR,
Expand Down
31 changes: 24 additions & 7 deletions src/glissade/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,13 @@ def prepare_slides(
if isinstance(block.get("media"), dict):
_inline_media(block["media"], base, warnings)

for quad in slide.get("quads") or []:
if isinstance(quad, dict):
if isinstance(quad.get("image"), dict):
_inline_image(quad["image"], base, warnings)
if isinstance(quad.get("media"), dict):
_inline_media(quad["media"], base, warnings)

if isinstance(slide.get("media"), dict):
_inline_media(slide["media"], base, warnings)

Expand All @@ -241,14 +248,24 @@ def external_media(slides: list[dict[str, Any]]) -> list[tuple[Any, str]]:
found = []
for position, slide in enumerate(slides, start=1):
slide.setdefault("n", position)
for node in (
left = slide.get("left") if isinstance(slide.get("left"), dict) else {}
right = slide.get("right") if isinstance(slide.get("right"), dict) else {}
nodes = [
slide.get("media"),
(slide.get("left") or {}).get("media") if isinstance(slide.get("left"), dict) else None,
(slide.get("right") or {}).get("media") if isinstance(slide.get("right"), dict) else None,
):
left.get("media"),
right.get("media"),
]
for quad in slide.get("quads") or []:
if isinstance(quad, dict):
nodes.append(quad.get("media"))
for node in nodes:
if isinstance(node, dict) and node.get("external") and node.get("src"):
found.append((slide.get("n", "?"), node["src"]))
img = slide.get("image")
if isinstance(img, dict) and img.get("remote"):
found.append((slide.get("n", "?"), img.get("src", "")))
images = [slide.get("image")]
for quad in slide.get("quads") or []:
if isinstance(quad, dict):
images.append(quad.get("image"))
for img in images:
if isinstance(img, dict) and img.get("remote"):
found.append((slide.get("n", "?"), img.get("src", "")))
return found
Loading
Loading