diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index ace1d87..021e34c 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,2 @@ --- -github: ClaudiuSchuster +github: oss-singularity diff --git a/.github/linters/.markdown-lint.yml b/.github/linters/.markdown-lint.yml new file mode 100644 index 0000000..eb0ddab --- /dev/null +++ b/.github/linters/.markdown-lint.yml @@ -0,0 +1,7 @@ +--- +default: true + +# Product pages intentionally use centered HTML for the logo and badges. +MD013: false +MD033: false +MD041: false diff --git a/.github/linters/.yaml-lint.yml b/.github/linters/.yaml-lint.yml new file mode 100644 index 0000000..526bf7a --- /dev/null +++ b/.github/linters/.yaml-lint.yml @@ -0,0 +1,9 @@ +--- +extends: default + +rules: + comments: + min-spaces-from-content: 1 + line-length: disable + truthy: + check-keys: false diff --git a/.github/social-preview-src/README.md b/.github/social-preview-src/README.md new file mode 100644 index 0000000..4d03d0d --- /dev/null +++ b/.github/social-preview-src/README.md @@ -0,0 +1,29 @@ +# Social preview source + +The repository preview is rendered at 1280×640 from three lossless sources: + +- `background.png`: original 1774×887 Nemo action-bar artwork. +- the existing duplicate-action icon under `icons/`. +- deterministic SVG typography, product copy and layout generated by `render.py`. +- output copy and repository-relative paths from `variants.json`. + +Rendering requires `rsvg-convert` from `librsvg2-bin`. The background, icon and +SVG overlay are rasterized together by Librsvg so local and CI renders use one +rendering path. + +The committed output is `.github/social-preview.png`. Render every configured +variant with: + +```console +make social-preview +``` + +Verify that the committed PNGs still match their sources with: + +```console +make check-social-preview +``` + +The background was generated specifically for Nemo Action Bar with the built-in +ImageGen tool. It is intentionally text-free so typography remains crisp and +fully deterministic in the repository renderer. diff --git a/.github/social-preview-src/background.png b/.github/social-preview-src/background.png new file mode 100644 index 0000000..3bb84d0 Binary files /dev/null and b/.github/social-preview-src/background.png differ diff --git a/.github/social-preview-src/overlay.svg b/.github/social-preview-src/overlay.svg new file mode 100644 index 0000000..255da19 --- /dev/null +++ b/.github/social-preview-src/overlay.svg @@ -0,0 +1,58 @@ + diff --git a/.github/social-preview-src/render-all.py b/.github/social-preview-src/render-all.py new file mode 100644 index 0000000..71ab3d2 --- /dev/null +++ b/.github/social-preview-src/render-all.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Render or verify every configured Nemo Action Bar social-preview variant.""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +import tempfile +from pathlib import Path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--check", + action="store_true", + help="fail if a committed preview differs from a fresh render", + ) + return parser.parse_args() + + +def configured_output(repo_dir: Path, value: object) -> Path: + relative = Path(str(value)) + if relative.is_absolute(): + raise SystemExit(f"Preview output must be repository-relative: {relative}") + + output = (repo_dir / relative).resolve() + try: + output.relative_to(repo_dir) + except ValueError as error: + raise SystemExit(f"Preview output escapes the repository: {relative}") from error + return output + + +def render(render_script: Path, variant: str, output: Path) -> None: + subprocess.run( + [ + sys.executable, + str(render_script), + "--variant", + variant, + "--output", + str(output), + ], + check=True, + ) + + +def main() -> int: + args = parse_args() + source_dir = Path(__file__).resolve().parent + repo_dir = source_dir.parents[1].resolve() + render_script = source_dir / "render.py" + variants = json.loads((source_dir / "variants.json").read_text(encoding="utf-8")) + + if not isinstance(variants, dict) or not variants: + raise SystemExit("At least one social-preview variant must be configured.") + + if not args.check: + for name in sorted(variants): + output = configured_output(repo_dir, variants[name]["output"]) + render(render_script, name, output) + return 0 + + stale: list[Path] = [] + with tempfile.TemporaryDirectory(prefix="nemo-action-bar-social-preview-check-") as temporary: + check_dir = Path(temporary) + for name in sorted(variants): + committed = configured_output(repo_dir, variants[name]["output"]) + candidate = check_dir / f"{name}.png" + render(render_script, name, candidate) + if not committed.is_file() or committed.read_bytes() != candidate.read_bytes(): + stale.append(committed) + + if stale: + for output in stale: + print(f"Stale social preview: {output.relative_to(repo_dir)}", file=sys.stderr) + print("Run make social-preview and commit the results.", file=sys.stderr) + return 1 + + print(f"All {len(variants)} social-preview variants are current.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/social-preview-src/render.py b/.github/social-preview-src/render.py new file mode 100644 index 0000000..67c8ac6 --- /dev/null +++ b/.github/social-preview-src/render.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Render a deterministic Nemo Action Bar social-preview variant.""" + +from __future__ import annotations + +import argparse +import html +import json +import shutil +import subprocess +import tempfile +from pathlib import Path + +WIDTH = 1280 +HEIGHT = 640 + + +def parse_args(variants: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--variant", choices=variants, required=True) + parser.add_argument("--output", type=Path, required=True) + return parser.parse_args() + + +def rsvg_convert() -> str: + executable = shutil.which("rsvg-convert") + if not executable: + raise SystemExit("librsvg2-bin is required (rsvg-convert).") + return executable + + +def main() -> int: + source_dir = Path(__file__).resolve().parent + repo_dir = source_dir.parents[1] + variants = json.loads((source_dir / "variants.json").read_text(encoding="utf-8")) + args = parse_args(sorted(variants)) + variant = variants[args.variant] + + background = source_dir / "background.png" + icon = repo_dir / "icons/nemo-action-bar-duplicate-symbolic.svg" + if not background.is_file() or not icon.is_file(): + raise SystemExit("Social-preview background or Nemo Action Bar icon is missing.") + + replacements = { + "{{TAGLINE}}": html.escape(str(variant["tagline"])), + "{{DETAIL}}": html.escape(str(variant["detail"])), + } + overlay_text = (source_dir / "overlay.svg").read_text(encoding="utf-8") + for marker, value in replacements.items(): + overlay_text = overlay_text.replace(marker, value) + + svg_renderer = rsvg_convert() + output = args.output.resolve() + output.parent.mkdir(parents=True, exist_ok=True) + with tempfile.TemporaryDirectory(prefix="nemo-action-bar-social-preview-") as temporary: + work_dir = Path(temporary) + overlay_svg = work_dir / "overlay.svg" + shutil.copy2(background, work_dir / "background.png") + shutil.copy2(icon, work_dir / "icon.svg") + overlay_svg.write_text(overlay_text, encoding="utf-8") + + subprocess.run( + [ + svg_renderer, + "--width", + str(WIDTH), + "--height", + str(HEIGHT), + "--output", + str(output), + str(overlay_svg), + ], + check=True, + ) + + print(f"Rendered {args.variant}: {output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/social-preview-src/variants.json b/.github/social-preview-src/variants.json new file mode 100644 index 0000000..ceb5d7b --- /dev/null +++ b/.github/social-preview-src/variants.json @@ -0,0 +1,7 @@ +{ + "generic": { + "output": ".github/social-preview.png", + "tagline": "Native actions, exactly where you browse.", + "detail": "18 configurable GTK actions. Live reload. No shell commands." + } +} diff --git a/.github/social-preview.png b/.github/social-preview.png new file mode 100644 index 0000000..552beed Binary files /dev/null and b/.github/social-preview.png differ diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 39889e3..b78efeb 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -23,8 +23,10 @@ jobs: run: | sudo apt-get update sudo apt-get install -y \ + fonts-noto-core \ gir1.2-gtk-3.0 \ gir1.2-nemo-3.0 \ + librsvg2-bin \ python3-gi \ shellcheck - name: Validate repository diff --git a/CHANGELOG.md b/CHANGELOG.md index c46bd34..a941256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.2.1 — 2026-09-01 + +- Add a polished 1280×640 GitHub social-preview card and a reproducible source + workflow for rendering it. +- Refresh the root README hero with the card, release badge and concise product + positioning. + ## 1.2.0 — 2026-08-24 - Use English for default button labels, validation errors and runtime dialogs. diff --git a/Makefile b/Makefile index 1941a87..962a64e 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,14 @@ -.PHONY: check +.PHONY: check social-preview check-social-preview check: python3 -m py_compile nemo_action_bar.py tests/validate_config.py PYTHONPATH=. python3 tests/validate_config.py python3 -m json.tool buttons.json >/dev/null shellcheck install.sh uninstall.sh + $(MAKE) check-social-preview + +social-preview: + python3 .github/social-preview-src/render-all.py + +check-social-preview: + python3 .github/social-preview-src/render-all.py --check diff --git a/README.md b/README.md index 7959c6e..c698b00 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,19 @@
+
+
+
18 native Nemo actions, one focused bar, and a faster path through every folder.
+ The current default layout in a real Nemo window, together with Nemo's original toolbar and the [active-window highlight](https://github.com/ClaudiuSchuster/cinnamon-active-window-highlight): @@ -90,20 +98,20 @@ lines are intentionally not supported. ### Supported actions -| Action ID | Behavior | -| --- | --- | -| `new-folder` | Create and immediately name a folder | -| `cut`, `copy`, `paste` | Nemo's native clipboard operations | -| `duplicate`, `rename`, `trash` | Operate on the current selection | -| `undo`, `redo` | Nemo's file-operation history | -| `properties`, `select-all` | Properties or full selection | -| `show-hidden` | Toggle hidden files for the current window | -| `copy-path` | Put selected local paths/URIs on the clipboard as plain text | -| `open-terminal` | Open Nemo's configured terminal at the selected/current folder | -| `open-admin` | Use Nemo's built-in “Open as Root” action and authentication dialog | -| `favorite-toggle` | Add or remove the selection according to its current state | -| `archive-create` | Open File Roller's archive-creation dialog (`nemo-fileroller`) | -| `archive-extract` | Extract the selected supported archive here (`nemo-fileroller`) | +| Action ID | Behavior | +| ------------------------------ | ------------------------------------------------------------------- | +| `new-folder` | Create and immediately name a folder | +| `cut`, `copy`, `paste` | Nemo's native clipboard operations | +| `duplicate`, `rename`, `trash` | Operate on the current selection | +| `undo`, `redo` | Nemo's file-operation history | +| `properties`, `select-all` | Properties or full selection | +| `show-hidden` | Toggle hidden files for the current window | +| `copy-path` | Put selected local paths/URIs on the clipboard as plain text | +| `open-terminal` | Open Nemo's configured terminal at the selected/current folder | +| `open-admin` | Use Nemo's built-in “Open as Root” action and authentication dialog | +| `favorite-toggle` | Add or remove the selection according to its current state | +| `archive-create` | Open File Roller's archive-creation dialog (`nemo-fileroller`) | +| `archive-extract` | Extract the selected supported archive here (`nemo-fileroller`) | The shipped `open-admin`, `favorite-toggle`, `archive-create` and `archive-extract` entries use `"enabled": false`. Change the desired values to