Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86da224448 | ||
|
|
29d2b75209 | ||
|
|
e000b296b5 | ||
|
|
cbce35441d | ||
|
|
11ae0e2f7d | ||
|
|
4d9058b9b2 |
No files matched your search
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
# No `submodules:` on the checkout above, so the tap is empty here and the
|
# No `submodules:` on the checkout above, so the tap is empty here and the
|
||||||
# sdist below differs from a local `uv build` with the submodule populated.
|
# sdist below differs from a local `uv build` with the submodule populated.
|
||||||
# The published asset is therefore the authority: tools/bump_packaging.py
|
# The published asset is therefore the authority: bump_packaging.py
|
||||||
# hashes the download, never a rebuild.
|
# hashes the download, never a rebuild.
|
||||||
- name: Build the sdist and wheel
|
- name: Build the sdist and wheel
|
||||||
run: |
|
run: |
|
||||||
@@ -64,6 +64,36 @@ jobs:
|
|||||||
uses: actions/forgejo-release@v2
|
uses: actions/forgejo-release@v2
|
||||||
with:
|
with:
|
||||||
direction: upload
|
direction: upload
|
||||||
|
# A re-run finds the release it made last time and would otherwise die
|
||||||
|
# on `curl: (22) ... 409` when it tries to create it again. Override
|
||||||
|
# deletes and recreates the release (the tag survives, since it still
|
||||||
|
# matches the triggered SHA), so re-running this workflow is safe.
|
||||||
|
override: true
|
||||||
|
tag: ${{ forgejo.ref_name }}
|
||||||
|
title: ${{ forgejo.ref_name }}
|
||||||
|
release-notes-file: dist/notes.md
|
||||||
|
|
||||||
|
# Every packaging channel (AUR, the Homebrew tap and the flake) downloads
|
||||||
|
# its source from tildeforge, so the release is published there as well.
|
||||||
|
# That repository is not a mirror of this one: nothing reaches it unless
|
||||||
|
# it is pushed, hence the tag push before the upload. Pushing a tag that
|
||||||
|
# is already there and identical is a no-op, so this stays re-runnable.
|
||||||
|
- name: Push the tag to tildeforge
|
||||||
|
env:
|
||||||
|
TILDEFORGE_TOKEN: ${{ secrets.TILDEFORGE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
git push \
|
||||||
|
"https://beefcarver:${TILDEFORGE_TOKEN}@tildeforge.dev/beefcarver/beefy-player.git" \
|
||||||
|
"refs/tags/${FORGEJO_REF_NAME}"
|
||||||
|
|
||||||
|
- name: Publish the release to tildeforge
|
||||||
|
uses: actions/forgejo-release@v2
|
||||||
|
with:
|
||||||
|
direction: upload
|
||||||
|
url: https://tildeforge.dev
|
||||||
|
repo: beefcarver/beefy-player
|
||||||
|
token: ${{ secrets.TILDEFORGE_TOKEN }}
|
||||||
|
override: true
|
||||||
tag: ${{ forgejo.ref_name }}
|
tag: ${{ forgejo.ref_name }}
|
||||||
title: ${{ forgejo.ref_name }}
|
title: ${{ forgejo.ref_name }}
|
||||||
release-notes-file: dist/notes.md
|
release-notes-file: dist/notes.md
|
||||||
@@ -35,7 +35,6 @@ git submodule update --init # contrib/homebrew, the Homebrew tap
|
|||||||
uv run pytest # full test suite
|
uv run pytest # full test suite
|
||||||
uvx ruff check . # lint (ruff is not a project dependency)
|
uvx ruff check . # lint (ruff is not a project dependency)
|
||||||
uv run beefy-player --list # station ids for the current catalogue
|
uv run beefy-player --list # station ids for the current catalogue
|
||||||
uv run python tools/ansi_art.py ... # regenerate a provider's header art
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`uvx ruff check .` is the enforced lint and must pass. `ruff format` is **not**
|
`uvx ruff check .` is the enforced lint and must pass. `ruff format` is **not**
|
||||||
@@ -61,7 +60,7 @@ repo-wide reformat as a side effect of an unrelated change.
|
|||||||
`.forgejo/workflows/release.yml` fires on a `v*` tag: it asserts that the tag,
|
`.forgejo/workflows/release.yml` fires on a `v*` tag: it asserts that the tag,
|
||||||
the `pyproject.toml` version and `beefy_player.__version__` all agree, then
|
the `pyproject.toml` version and `beefy_player.__version__` all agree, then
|
||||||
publishes release notes. It does **not** touch packaging, so run
|
publishes release notes. It does **not** touch packaging, so run
|
||||||
`tools/bump_packaging.py <version>` after publishing: it rewrites
|
`uv run python bump_packaging.py <version>` after publishing: it rewrites
|
||||||
`contrib/aur/PKGBUILD` and regenerates its `.SRCINFO` from it, rewrites the tap
|
`contrib/aur/PKGBUILD` and regenerates its `.SRCINFO` from it, rewrites the tap
|
||||||
formula in `contrib/homebrew`, and with `--commit` commits the tap first and
|
formula in `contrib/homebrew`, and with `--commit` commits the tap first and
|
||||||
then this repository. Both channels build the sdist attached to the release,
|
then this repository. Both channels build the sdist attached to the release,
|
||||||
|
|||||||
@@ -330,15 +330,12 @@ Each catalogue is a self-contained provider module under
|
|||||||
`beefy_player/providers/`; `base.py` documents the interface. Every catalogue's
|
`beefy_player/providers/`; `base.py` documents the interface. Every catalogue's
|
||||||
header is a wordmark from the shared half-block font, and the header paints it
|
header is a wordmark from the shared half-block font, and the header paints it
|
||||||
with the active palette gradient (`hero_gradient`, on by default) so all
|
with the active palette gradient (`hero_gradient`, on by default) so all
|
||||||
thirteen follow the terminal's colours. `tools/ansi_art.py` converts a PNG logo
|
thirteen follow the terminal's colours.
|
||||||
into half-block ANSI art that carries colours of its own; a provider using that
|
|
||||||
art must set `hero_gradient = False`, because the header would otherwise
|
|
||||||
repaint it with the gradient.
|
|
||||||
|
|
||||||
`uvx ruff check .` is the enforced lint. The tree is not `ruff format`-clean (54
|
`uvx ruff check .` is the enforced lint. The tree is not `ruff format`-clean (54
|
||||||
files would change), so do not reformat it wholesale.
|
files would change), so do not reformat it wholesale.
|
||||||
|
|
||||||
`tools/bump_packaging.py <version>` keeps the AUR package and the Homebrew tap
|
`uv run python bump_packaging.py <version>` keeps the AUR package and the Homebrew tap
|
||||||
formula in step with a release — it rewrites `PKGBUILD`, regenerates
|
formula in step with a release — it rewrites `PKGBUILD`, regenerates
|
||||||
`.SRCINFO` from it (no makepkg needed), rewrites the formula, and `--commit`s
|
`.SRCINFO` from it (no makepkg needed), rewrites the formula, and `--commit`s
|
||||||
both. `--check` reports drift without writing.
|
both. `--check` reports drift without writing.
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""Beefy Player — a flashy terminal player for internet radio."""
|
"""Beefy Player — a flashy terminal player for internet radio."""
|
||||||
|
|
||||||
__version__ = "1.2.3"
|
__version__ = "1.2.5"
|
||||||
@@ -15,10 +15,10 @@ generated on the fly. The sdist's root directory is hatchling's PEP 503 name
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
python tools/bump_packaging.py 1.2.3 # download the release asset
|
python bump_packaging.py 1.2.3 # download the release asset
|
||||||
python tools/bump_packaging.py 1.2.3 --asset dist/beefy_player-1.2.3.tar.gz
|
python bump_packaging.py 1.2.3 --asset dist/beefy_player-1.2.3.tar.gz
|
||||||
python tools/bump_packaging.py 1.2.3 --commit # also commit both repos
|
python bump_packaging.py 1.2.3 --commit # also commit both repos
|
||||||
python tools/bump_packaging.py 1.2.3 --check # verify, write nothing
|
python bump_packaging.py 1.2.3 --check # verify, write nothing
|
||||||
|
|
||||||
`.SRCINFO` is never edited directly; it is regenerated by evaluating
|
`.SRCINFO` is never edited directly; it is regenerated by evaluating
|
||||||
`PKGBUILD` in bash and serialising it in makepkg's field order. That keeps it
|
`PKGBUILD` in bash and serialising it in makepkg's field order. That keeps it
|
||||||
@@ -106,7 +106,7 @@ done
|
|||||||
|
|
||||||
#: Replaces the `_commit`-pinned git-archive source with the release asset.
|
#: Replaces the `_commit`-pinned git-archive source with the release asset.
|
||||||
ASSET_SOURCE = (
|
ASSET_SOURCE = (
|
||||||
"# The sdist attached to the v$pkgver release. tools/bump_packaging.py keeps\n"
|
"# The sdist attached to the v$pkgver release. bump_packaging.py keeps\n"
|
||||||
"# this and sha256sums in step with the tag; build/check/package use\n"
|
"# this and sha256sums in step with the tag; build/check/package use\n"
|
||||||
"# $_srcname because the sdist unpacks to hatchling's PEP 503 name.\n"
|
"# $_srcname because the sdist unpacks to hatchling's PEP 503 name.\n"
|
||||||
'_srcname="beefy_player-$pkgver"\n'
|
'_srcname="beefy_player-$pkgver"\n'
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
in
|
in
|
||||||
py.buildPythonApplication {
|
py.buildPythonApplication {
|
||||||
pname = "beefy-player";
|
pname = "beefy-player";
|
||||||
version = "1.2.3";
|
version = "1.2.5";
|
||||||
pyproject = true;
|
pyproject = true;
|
||||||
src = self;
|
src = self;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "beefy-player"
|
name = "beefy-player"
|
||||||
version = "1.2.3"
|
version = "1.2.5"
|
||||||
description = "A flashy terminal player for internet radio — live spectrum analyser, MPRIS / macOS Now Playing support, song history and pluggable station catalogues (SomaFM, Radio Paradise, dublab, Rinse FM and more)"
|
description = "A flashy terminal player for internet radio — live spectrum analyser, MPRIS / macOS Now Playing support, song history and pluggable station catalogues (SomaFM, Radio Paradise, dublab, Rinse FM and more)"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "WTFPL"
|
license = "WTFPL"
|
||||||
|
|||||||
@@ -1,275 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Turn a logo into half-block ANSI art for the header hero.
|
|
||||||
|
|
||||||
The header hero is a handful of text rows, so an image has to survive a brutal
|
|
||||||
downsample: two pixels per cell (upper/lower half block), one ANSI colour per
|
|
||||||
half. Colours are quantised to the stock 16-colour ANSI palette on purpose —
|
|
||||||
the rest of the UI never emits RGB, and a hero built from palette names still
|
|
||||||
reads as the station's art in any terminal theme.
|
|
||||||
|
|
||||||
Shipped catalogues no longer use image art: each one renders a wordmark from
|
|
||||||
beefy_player/providers/wordmark.py, which the header repaints with the active
|
|
||||||
palette gradient (RadioProvider.hero_gradient, on by default). Art from this
|
|
||||||
tool carries colours of its own, so a provider that uses it must set
|
|
||||||
hero_gradient = False — otherwise the header repaints the art with the gradient
|
|
||||||
and the palette names below are wasted.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
python tools/ansi_art.py logo.png --cols 22 --rows 7
|
|
||||||
python tools/ansi_art.py --url https://example.org/logo.png \\
|
|
||||||
--cols 22 --rows 7 --module beefy_player/providers/<slug>/hero.py
|
|
||||||
|
|
||||||
``--preview out.png`` draws what the cells will look like, which is much easier
|
|
||||||
to judge than a wall of ▀ and ▄.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
|
|
||||||
#: The stock xterm colours the names below resolve to in a terminal.
|
|
||||||
ANSI16: dict[str, tuple[int, int, int]] = {
|
|
||||||
"black": (0, 0, 0),
|
|
||||||
"red": (170, 0, 0),
|
|
||||||
"green": (0, 170, 0),
|
|
||||||
"yellow": (170, 85, 0),
|
|
||||||
"blue": (0, 0, 170),
|
|
||||||
"magenta": (170, 0, 170),
|
|
||||||
"cyan": (0, 170, 170),
|
|
||||||
"white": (170, 170, 170),
|
|
||||||
"bright_black": (85, 85, 85),
|
|
||||||
"bright_red": (255, 85, 85),
|
|
||||||
"bright_green": (85, 255, 85),
|
|
||||||
"bright_yellow": (255, 255, 85),
|
|
||||||
"bright_blue": (85, 85, 255),
|
|
||||||
"bright_magenta": (255, 85, 255),
|
|
||||||
"bright_cyan": (85, 255, 255),
|
|
||||||
"bright_white": (255, 255, 255),
|
|
||||||
}
|
|
||||||
|
|
||||||
#: Below this alpha a half-cell counts as empty (terminal background).
|
|
||||||
ALPHA_CUTOFF = 110
|
|
||||||
|
|
||||||
#: Ink weaker than this is treated as fringe when cropping the image.
|
|
||||||
CROP_CUTOFF = 96
|
|
||||||
|
|
||||||
|
|
||||||
def nearest_colour(rgb: tuple[int, int, int]) -> str:
|
|
||||||
best, best_distance = "white", float("inf")
|
|
||||||
for name, reference in ANSI16.items():
|
|
||||||
distance = sum((a - b) ** 2 for a, b in zip(rgb, reference, strict=True))
|
|
||||||
if distance < best_distance:
|
|
||||||
best, best_distance = name, distance
|
|
||||||
return best
|
|
||||||
|
|
||||||
|
|
||||||
def key_white(image: Image.Image, tolerance: int) -> Image.Image:
|
|
||||||
"""Make near-white pixels transparent.
|
|
||||||
|
|
||||||
A logo exported on a white background (or one whose anti-aliasing bleeds
|
|
||||||
white) otherwise survives the downsample as a glaring box.
|
|
||||||
"""
|
|
||||||
image = image.copy()
|
|
||||||
pixels = image.load()
|
|
||||||
width, height = image.size
|
|
||||||
threshold = 255 - max(0, tolerance)
|
|
||||||
for y in range(height):
|
|
||||||
for x in range(width):
|
|
||||||
red, green, blue, alpha = pixels[x, y]
|
|
||||||
if alpha and min(red, green, blue) >= threshold:
|
|
||||||
pixels[x, y] = (red, green, blue, 0)
|
|
||||||
return image
|
|
||||||
|
|
||||||
|
|
||||||
def resize_premultiplied(image: Image.Image, size: tuple[int, int]) -> Image.Image:
|
|
||||||
"""Resize in premultiplied alpha, so transparent pixels cannot bleed in.
|
|
||||||
|
|
||||||
Plain ``Image.resize`` averages the RGB of fully transparent pixels (often
|
|
||||||
white) with the surrounding ink, which is how a clean logo turns into a
|
|
||||||
grey haze at 20x14.
|
|
||||||
"""
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
data = np.asarray(image.convert("RGBA"), dtype=np.float32)
|
|
||||||
alpha = data[..., 3:4] / 255.0
|
|
||||||
premultiplied = np.concatenate([data[..., :3] * alpha, data[..., 3:4]], axis=-1)
|
|
||||||
small = Image.fromarray(premultiplied.astype(np.uint8), "RGBA").resize(
|
|
||||||
size, Image.LANCZOS
|
|
||||||
)
|
|
||||||
out = np.asarray(small, dtype=np.float32)
|
|
||||||
out_alpha = out[..., 3:4] / 255.0
|
|
||||||
rgb = out[..., :3] / np.maximum(out_alpha, 1e-6)
|
|
||||||
return Image.fromarray(
|
|
||||||
np.concatenate([np.clip(rgb, 0, 255), out[..., 3:4]], axis=-1).astype(np.uint8),
|
|
||||||
"RGBA",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def load_image(
|
|
||||||
path: Path | None, url: str | None, key_white_background: bool = False
|
|
||||||
) -> Image.Image:
|
|
||||||
if path is None and url:
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
response = httpx.get(url, follow_redirects=True, timeout=30.0)
|
|
||||||
response.raise_for_status()
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
image = Image.open(BytesIO(response.content))
|
|
||||||
elif path is not None:
|
|
||||||
image = Image.open(path)
|
|
||||||
else: # pragma: no cover - argparse enforces one of the two
|
|
||||||
raise SystemExit("pass an image path or --url")
|
|
||||||
|
|
||||||
image = image.convert("RGBA")
|
|
||||||
if key_white_background:
|
|
||||||
image = key_white(image, 24)
|
|
||||||
# Trim the transparent margin: the art should fill the box it is given.
|
|
||||||
opaque = image.getchannel("A").point(lambda value: 255 if value > CROP_CUTOFF else 0)
|
|
||||||
box = opaque.getbbox()
|
|
||||||
return image.crop(box) if box else image
|
|
||||||
|
|
||||||
|
|
||||||
def art_rows(image: Image.Image, cols: int, rows: int) -> list[list[tuple[str, str, str]]]:
|
|
||||||
"""(glyph, foreground, background) per cell, two source pixels per cell."""
|
|
||||||
small = resize_premultiplied(image, (cols, rows * 2))
|
|
||||||
pixels = small.load()
|
|
||||||
grid: list[list[tuple[str, str, str]]] = []
|
|
||||||
for row in range(rows):
|
|
||||||
cells: list[tuple[str, str, str]] = []
|
|
||||||
for column in range(cols):
|
|
||||||
top = pixels[column, row * 2]
|
|
||||||
bottom = pixels[column, row * 2 + 1]
|
|
||||||
top_ink = top[3] >= ALPHA_CUTOFF
|
|
||||||
bottom_ink = bottom[3] >= ALPHA_CUTOFF
|
|
||||||
if not top_ink and not bottom_ink:
|
|
||||||
cells.append((" ", "", ""))
|
|
||||||
elif top_ink and bottom_ink:
|
|
||||||
cells.append(("▀", nearest_colour(top[:3]), nearest_colour(bottom[:3])))
|
|
||||||
elif top_ink:
|
|
||||||
cells.append(("▀", nearest_colour(top[:3]), ""))
|
|
||||||
else:
|
|
||||||
cells.append(("▄", nearest_colour(bottom[:3]), ""))
|
|
||||||
grid.append(cells)
|
|
||||||
return grid
|
|
||||||
|
|
||||||
|
|
||||||
def markup(grid: list[list[tuple[str, str, str]]]) -> list[str]:
|
|
||||||
"""One rich-markup string per row, run-length encoded by style."""
|
|
||||||
lines: list[str] = []
|
|
||||||
for cells in grid:
|
|
||||||
line: list[str] = []
|
|
||||||
index = 0
|
|
||||||
while index < len(cells):
|
|
||||||
glyph, foreground, background = cells[index]
|
|
||||||
if not background and glyph == " ":
|
|
||||||
line.append(" ")
|
|
||||||
index += 1
|
|
||||||
continue
|
|
||||||
run = [glyph]
|
|
||||||
index += 1
|
|
||||||
while index < len(cells) and cells[index] == (glyph, foreground, background):
|
|
||||||
run.append(cells[index][0])
|
|
||||||
index += 1
|
|
||||||
style = " ".join(
|
|
||||||
part for part in (foreground, f"on {background}" if background else "") if part
|
|
||||||
)
|
|
||||||
body = "".join(run)
|
|
||||||
line.append(f"[{style}]{body}[/]" if style else body)
|
|
||||||
lines.append("".join(line).rstrip())
|
|
||||||
return lines
|
|
||||||
|
|
||||||
|
|
||||||
def preview(grid: list[list[tuple[str, str, str]]], path: Path, cell: tuple[int, int]) -> None:
|
|
||||||
width, height = cell
|
|
||||||
image = Image.new("RGB", (len(grid[0]) * width, len(grid) * height), (24, 24, 24))
|
|
||||||
draw = ImageDraw.Draw(image)
|
|
||||||
for row, cells in enumerate(grid):
|
|
||||||
for column, (glyph, foreground, background) in enumerate(cells):
|
|
||||||
x, y = column * width, row * height
|
|
||||||
if background:
|
|
||||||
draw.rectangle([x, y, x + width - 1, y + height - 1], fill=ANSI16[background])
|
|
||||||
if glyph == " ":
|
|
||||||
continue
|
|
||||||
colour = ANSI16.get(foreground, (140, 140, 140))
|
|
||||||
if glyph == "▀":
|
|
||||||
box = [x, y, x + width - 1, y + height // 2 - 1]
|
|
||||||
else:
|
|
||||||
box = [x, y + height // 2, x + width - 1, y + height - 1]
|
|
||||||
draw.rectangle(box, fill=colour)
|
|
||||||
image.resize((image.width * 3, image.height * 3), Image.NEAREST).save(path)
|
|
||||||
|
|
||||||
|
|
||||||
MODULE_TEMPLATE = '''"""Generated header art — do not edit by hand.
|
|
||||||
|
|
||||||
{provenance}
|
|
||||||
|
|
||||||
Regenerate with::
|
|
||||||
|
|
||||||
{command}
|
|
||||||
|
|
||||||
Each row is rich markup; ``▀`` is a half block whose foreground is the top
|
|
||||||
pixel and whose background is the pixel below it. Colours are stock ANSI
|
|
||||||
palette names, so the art keeps the terminal's own palette.
|
|
||||||
|
|
||||||
The header paints every hero with the active palette gradient unless the
|
|
||||||
provider opts out, so the class using this art must set
|
|
||||||
``hero_gradient = False``.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
HERO: tuple[str, ...] = (
|
|
||||||
{rows}
|
|
||||||
)
|
|
||||||
'''
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv: list[str] | None = None) -> int:
|
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
|
||||||
parser.add_argument("image", nargs="?", type=Path, help="local image file")
|
|
||||||
parser.add_argument("--url", help="fetch the image from this URL instead")
|
|
||||||
parser.add_argument("--cols", type=int, default=22, help="terminal columns (default 22)")
|
|
||||||
parser.add_argument("--rows", type=int, default=7, help="terminal rows (default 7)")
|
|
||||||
parser.add_argument("--module", type=Path, help="write a Python module with HERO")
|
|
||||||
parser.add_argument("--preview", type=Path, help="write a PNG preview of the cells")
|
|
||||||
parser.add_argument("--provenance", default="", help="source note for the module header")
|
|
||||||
parser.add_argument(
|
|
||||||
"--key-white",
|
|
||||||
action="store_true",
|
|
||||||
help="treat near-white pixels as transparent (logos on white)",
|
|
||||||
)
|
|
||||||
args = parser.parse_args(argv)
|
|
||||||
|
|
||||||
image = load_image(args.image, args.url, args.key_white)
|
|
||||||
grid = art_rows(image, args.cols, args.rows)
|
|
||||||
rows = markup(grid)
|
|
||||||
|
|
||||||
if args.preview:
|
|
||||||
preview(grid, args.preview, (10, 20))
|
|
||||||
|
|
||||||
if args.module:
|
|
||||||
source = args.provenance or args.url or str(args.image)
|
|
||||||
command = "uv run python tools/ansi_art.py " + " ".join(sys.argv[1:])
|
|
||||||
body = "\n".join(f" {row!r}," for row in rows)
|
|
||||||
args.module.write_text(
|
|
||||||
MODULE_TEMPLATE.format(
|
|
||||||
provenance=f"Source: {source}",
|
|
||||||
command=command,
|
|
||||||
rows=body,
|
|
||||||
),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
print(f"wrote {args.module} ({len(rows)} rows x {args.cols} cols)")
|
|
||||||
else:
|
|
||||||
print("\n".join(rows))
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -32,7 +32,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "beefy-player"
|
name = "beefy-player"
|
||||||
version = "1.2.2"
|
version = "1.2.5"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "dbus-next", marker = "sys_platform == 'linux'" },
|
{ name = "dbus-next", marker = "sys_platform == 'linux'" },
|
||||||
|
|||||||
Reference in new issue
Block a user