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
6 changes: 4 additions & 2 deletions MAINTAINERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ When you are ready to cut a new release, follow these steps to generate the
tbump X.Y.Z --no-push
```

_Note: `tbump` will automatically trigger the `before_commit` hook, run
`update-package-metadata.py`, generate the changelog and bundle everything
_Note: `tbump` will automatically trigger the `before_commit` hooks to
regenerate the status screenshot
(`scripts/maintainer/generate-screenshot.py`), run
`update-package-metadata.py`, generate the changelog, and bundle everything
into a single release commit and local tag._

💡 **Tip for Release Candidates:** You can safely use standard SemVer tags
Expand Down
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ updates. It features Snapper-integrated atomic rollbacks, multi-channel alerting
user-level execution. Designed for reliability and ease of use, it ensures your
Flatpak environment remains current and resilient.

<!-- prettier-ignore-start -->
<!-- markdownlint-disable-next-line MD033 -->
<div align="center"><img src="assets/status-screenshot.png" alt="Flatpak Automatic
System Status Overview" width="750"></div>
<!-- prettier-ignore-end -->

## ✨ Features

- **Automated Flatpak Updates:** Keep your flatpak applications up-to-date
Expand Down Expand Up @@ -154,8 +160,8 @@ first run).
To trigger an update manually or use the advanced CLI:

```text
usage: flatpak-automatic [-h] [-d] [-t] [-f] [-s] [-l] [-a] [-c] [-r]
[--desktop-mode] [-e] [-x]
usage: flatpak-automatic [-h] [-d] [-t] [-f] [-s] [-B] [-v] [-V] [-l] [-a] [-c]
[-r] [--desktop-mode] [-e] [-x]

Flatpak Automatic - Advanced Update Automation

Expand All @@ -165,6 +171,9 @@ options:
-t, --test-notify Send a test notification to configured endpoints and exit.
-f, --force Force the update process, ignoring safeguards.
-s, --status Display system monitoring overview and exit.
-B, --hide-banner Hide the header ASCII banner upon execution.
-v, --version Show application version and exit.
-V, --verbose Enable verbose output (e.g. detailed status view with -s/--status).
-l, --history Display recent update history from journalctl and exit.
-a, --apply-schedule Apply systemd timer overrides based on config settings.
-c, --check-config Validate and print the current configuration, then exit.
Expand Down
Binary file added assets/status-screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions config/config.default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ exclusions: []
# Type: string, Default: always
auto_notify: always

# ------------------------------------------------------------------------------
# CLI & Interface Settings
# ------------------------------------------------------------------------------

cli:
# Hide the ASCII header banner when executing from terminal.
# Type: boolean, Default: false
hide_banner: false

# ------------------------------------------------------------------------------
# Systemd Timer Schedule
# ------------------------------------------------------------------------------
Expand Down
8 changes: 8 additions & 0 deletions config/config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ exclusions:
# Notification trigger policy: 'always', 'on-change', 'on-failure', or 'never'.
auto_notify: always

# ------------------------------------------------------------------------------
# CLI & Interface Settings
# ------------------------------------------------------------------------------

cli:
# Hide the ASCII header banner when executing from terminal.
hide_banner: false

# ------------------------------------------------------------------------------
# Systemd Timer Schedule
# ------------------------------------------------------------------------------
Expand Down
9 changes: 9 additions & 0 deletions config/config.user.default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ exclusions: []
# Type: string, Default: always
auto_notify: always

# ------------------------------------------------------------------------------
# CLI & Interface Settings
# ------------------------------------------------------------------------------

cli:
# Hide the ASCII header banner when executing from terminal.
# Type: boolean, Default: false
hide_banner: false

# ------------------------------------------------------------------------------
# Systemd Timer Schedule (User Scope)
# ------------------------------------------------------------------------------
Expand Down
3 changes: 2 additions & 1 deletion config/templates/default_mail_success.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# ✅ Update Report: Success

The `flatpak-automatic` agent has successfully installed **${UPDATE_COUNT}**
The `flatpak-automatic` agent has successfully installed
**${UPDATE_COUNT}**
updates on the host **${HOSTNAME}**.

## Installation Details
Expand Down
14 changes: 14 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ Usage:
[--dry-run]
```

## Screenshot Generation Tool

A `scripts/maintainer/generate-screenshot.py` script automatically captures the
output of `flatpak-automatic -s` (including the full ASCII header banner) and
renders a high-resolution terminal screenshot image saved at
`assets/status-screenshot.png` for display in `README.md`. This script is
automatically invoked during release version bumps via `tbump`.

Usage:

```bash
python3 scripts/maintainer/generate-screenshot.py
```

## Testing Matrix

This project enforces a Shift-Left testing approach.
Expand Down
11 changes: 10 additions & 1 deletion docs/flatpak-automatic.1
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
flatpak-automatic \- Secure, systemd-native automation for Flatpak updates
.SH SYNOPSIS
.B flatpak-automatic
[\fB\-d\fR] [\fB\-t\fR] [\fB\-f\fR] [\fB\-s\fR] [\fB\-l\fR] [\fB\-a\fR] [\fB\-c\fR] [\fB\-r\fR] [\fB\-e\fR] [\fB\-x\fR] [\fB\-\-desktop\-mode\fR]
[\fB\-d\fR] [\fB\-t\fR] [\fB\-f\fR] [\fB\-s\fR] [\fB\-B\fR] [\fB\-v\fR] [\fB\-V\fR] [\fB\-l\fR] [\fB\-a\fR] [\fB\-c\fR] [\fB\-r\fR] [\fB\-e\fR] [\fB\-x\fR] [\fB\-\-desktop\-mode\fR]
.SH DESCRIPTION
.B flatpak-automatic
is a secure, systemd-native automation wrapper for Flatpak updates. It features Snapper-integrated atomic rollbacks, multi-channel alerting (Apprise, Mail, Webhooks, Desktop), and supports both system-wide and rootless user-level execution. Designed for reliability and ease of use, it ensures your Flatpak environment remains current and resilient.
Expand Down Expand Up @@ -32,6 +32,15 @@ Force the update process, bypassing the minimum delay guard and auto_update poli
.BR \-s ", " \-\-status
Display a system monitoring overview of configuration and pending updates.
.TP
.BR \-B ", " \-\-hide\-banner
Hide the header ASCII banner upon execution.
.TP
.BR \-v ", " \-\-version
Display the installed application version and exit.
.TP
.BR \-V ", " \-\-verbose
Enable verbose output (e.g. detailed status view of installed Flatpaks and runtimes with -s/--status).
.TP
.BR \-l ", " \-\-history
Display recent execution history from journalctl.
.TP
Expand Down
174 changes: 174 additions & 0 deletions scripts/maintainer/generate-screenshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
"""Script to generate terminal status screenshot for README using rich, chrome/cairosvg."""

import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
from PIL import Image, ImageChops
from rich.console import Console
from rich.terminal_theme import TerminalTheme
from rich.text import Text


# Authentic terminal color palette matching standard terminal emulators (with rich purple HEADER)
VIBRANT_TERMINAL_THEME = TerminalTheme(
background=(41, 41, 41),
foreground=(248, 248, 242),
normal=[
(0, 0, 0),
(255, 85, 85),
(80, 250, 123),
(241, 250, 140),
(139, 233, 253),
(189, 147, 249), # Purple (#BD93F9)
(0, 229, 255),
(191, 191, 191),
],
bright=[
(98, 114, 164),
(255, 110, 110),
(105, 255, 148),
(255, 255, 165),
(120, 190, 255),
(189, 147, 249), # Bright Purple (#BD93F9)
(0, 229, 255),
(255, 255, 255),
],
)


def crop_margins(image_path: Path) -> None:
"""Crop white outer margins from generated screenshot."""
im = Image.open(image_path)
im_rgb = im.convert("RGB")
bg = Image.new("RGB", im_rgb.size, (255, 255, 255))
diff = ImageChops.difference(im_rgb, bg)
bbox = diff.getbbox()
if bbox:
cropped = im.crop(bbox)
cropped.save(image_path)


def convert_svg_to_png(svg_path: Path, output_png: Path) -> None:
"""Convert SVG to PNG using Google Chrome/Chromium if available, falling back to cairosvg."""
chrome_bin = shutil.which("google-chrome") or shutil.which("chromium")

if chrome_bin:
temp_png = svg_path.parent / "temp_chrome.png"
cmd = [
chrome_bin,
"--headless",
"--disable-gpu",
"--hide-scrollbars",
f"--screenshot={temp_png}",
"--window-size=1000,800",
str(svg_path),
]
subprocess.run(
cmd,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)

if temp_png.exists():
crop_margins(temp_png)
shutil.move(temp_png, output_png)
return

# Fallback to cairosvg
import cairosvg

svg_data = svg_path.read_text(encoding="utf-8")
svg_data = re.sub(
r"font-family:\s*([^;]+);",
r"font-family: \1, 'Noto Color Emoji', 'Noto Emoji', 'Segoe UI Emoji', 'Apple Color Emoji', 'Symbola', monospace;",
svg_data,
)
cairosvg.svg2png(
bytestring=svg_data.encode("utf-8"),
write_to=str(output_png),
)


def generate_screenshot() -> None:
"""Run status command as root (or fallback), capture terminal output with ASCII header banner, and export to PNG image."""
script_path = Path(__file__).resolve()
repo_root = script_path.parent.parent.parent
assets_dir = repo_root / "assets"
assets_dir.mkdir(parents=True, exist_ok=True)
output_png = assets_dir / "status-screenshot.png"
temp_svg = assets_dir / "temp_status.svg"

src_dir = str(repo_root / "src")
if src_dir not in sys.path:
sys.path.insert(0, src_dir)

from flatpak_automatic.cli import banner

cli_script = repo_root / "src" / "flatpak-automatic.py"

output_text = None

# Try non-interactive sudo first, then interactive sudo, then non-root execution
attempts = [
["sudo", "-n", sys.executable, str(cli_script), "-s"],
["sudo", sys.executable, str(cli_script), "-s"],
[sys.executable, str(cli_script), "-s"],
]

if os.geteuid() == 0:
attempts.insert(0, [sys.executable, str(cli_script), "-s"])

for cmd in attempts:
try:
result = subprocess.run(
cmd,
cwd=repo_root,
capture_output=True,
text=True,
check=True,
timeout=5,
)
output_text = result.stdout.strip()
if output_text:
break
except Exception:
continue

if not output_text:
# Ultimate fallback
result = subprocess.run(
[sys.executable, str(cli_script), "-s"],
cwd=repo_root,
capture_output=True,
text=True,
check=True,
)
output_text = result.stdout.strip()

banner_text = banner()
full_output = f"{banner_text}\n{output_text}"

console = Console(record=True, width=100)
rich_text = Text.from_ansi(full_output)
console.print(rich_text)

svg_data = console.export_svg(
title="flatpak-automatic -s", theme=VIBRANT_TERMINAL_THEME
)
temp_svg.write_text(svg_data, encoding="utf-8")

convert_svg_to_png(temp_svg, output_png)

if temp_svg.exists():
temp_svg.unlink()

print(f"Successfully generated screenshot at: {output_png}")


if __name__ == "__main__":
generate_screenshot()
19 changes: 14 additions & 5 deletions src/flatpak_automatic/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import signal
import subprocess
from typing import Any
from . import __version__
from .cli import get_parser, banner
from .config import ConfigManager, StateManager
from .core import AutomationEngine
Expand All @@ -15,18 +16,26 @@ def main() -> None:
# 1. Initialize Parser & Logging
parser = get_parser()
args = parser.parse_args()

if args.version:
print(f"flatpak-automatic {__version__}")
sys.exit(0)

setup_logging()

# Dynamic Flatpak Scope for non-root execution
user_scope = os.geteuid() != 0
flatpak_scope = ["--user"] if user_scope else ["--system"]

if sys.stdout.isatty():
print(banner())

# 2. Load Configuration & State
config = ConfigManager.load()

# Determine if banner should be displayed
hide_banner = args.hide_banner or config.get("cli", {}).get("hide_banner", False)

if sys.stdout.isatty() and not hide_banner:
print(banner())

def sighup_handler(signum: int, frame: Any) -> None:
logging.info("SIGHUP received. Hot-reloading configuration...")
nonlocal config
Expand Down Expand Up @@ -160,8 +169,8 @@ def sighup_handler(signum: int, frame: Any) -> None:
print()
sys.exit(0)

if args.status:
engine.print_status_overview()
if args.status or args.verbose:
engine.print_status_overview(verbose=args.verbose)
sys.exit(0)

if args.history:
Expand Down
18 changes: 18 additions & 0 deletions src/flatpak_automatic/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,24 @@ def get_parser() -> BrandedArgumentParser:
action="store_true",
help="Display system monitoring overview and exit.",
)
parser.add_argument(
"-B",
"--hide-banner",
action="store_true",
help="Hide the header ASCII banner upon execution.",
)
parser.add_argument(
"-v",
"--version",
action="store_true",
help="Show application version and exit.",
)
parser.add_argument(
"-V",
"--verbose",
action="store_true",
help="Enable verbose output (e.g. detailed status view with -s/--status).",
)
parser.add_argument(
"-l",
"--history",
Expand Down
Loading
Loading