Build apps for PyneHub
Everything you need to make a PinePhone app and get it into the hub — the requirements, the app structure, icon specs, ready-to-run templates, a “turn any website into an app” wrapper, and copy-paste prompts if you build with an AI agent.
How PyneHub apps work
A PyneHub app is a small Python 3 program built with GTK4 and libadwaita — the same toolkit the PinePhone’s Phosh shell uses, so your app looks and feels native. You bundle it into a.tar.gz with a manifest.json and an icon. When a user taps Install in the hub, the PyneHub client downloads your package, unpacks it, and adds it to the phone’s app drawer automatically.
There’s no compiling and no app-store review tooling to learn — if it runs with python3 main.py on a PinePhone, it will run in the hub.
Requirements
- Language: Python 3 (standard on PinePhone distros).
- UI toolkit: GTK4 + libadwaita for a native mobile look.
- Web apps: WebKitGTK 6.0 if your app shows web content.
- Entry point: A script that runs with python3 <file>. Name it in the manifest.
- Screen target: Portrait 360 × 720. Design for one narrow column.
- Behaviour: No telemetry, no hidden network calls, no bundled binaries.
Install the tools you need to build and test locally:
# Basic apps (GTK4 + libadwaita):
sudo pacman -S python python-gobject gtk4 libadwaita
# Web apps also need WebKitGTK:
sudo pacman -S webkitgtk-6.0App package structure
Every app is just three files in a folder:
my-app/
├─ main.py # your app code (the entry_point)
├─ manifest.json # metadata the hub reads
└─ icon.png # 512 x 512 app iconThe manifest.json is what the hub reads to list your app. Here are the fields:
| Field | Meaning |
|---|---|
| name | App name shown in the hub and app drawer. |
| version | Semantic version string, e.g. "1.0.0". |
| description | What the app does. First line shows as the launcher tooltip. |
| author | Your name or team. |
| category | e.g. Utilities, Internet, Games, Productivity. |
| entry_point | The script to run, e.g. "main.py". |
| dependencies | List of system packages, e.g. ["gtk4", "libadwaita"]. |
{
"name": "My App",
"version": "1.0.0",
"description": "A short, clear description of what your app does. This is shown on your app's page in the hub. Keep the first line under ~120 characters.",
"author": "Your Name",
"category": "Utilities",
"entry_point": "main.py",
"dependencies": ["gtk4", "libadwaita"]
}
Icon guidelines
- Size: 512 × 512 pixels, square.
- Format: PNG (transparency is fine).
- Shape: Fill the full square — the hub rounds the corners for you.
- Style: One bold, simple symbol. High contrast. No text.
- Legibility: Must stay clear at 48 × 48 in the app drawer.
- File size: Keep it under ~200 KB.
A clean way to match the look of the built-in apps: a flat vertical gradient in a rounded square with a single white line-art glyph on top.
Starter template
A complete, working app you can run right now and build on. Download the full package (code + manifest + icon) or copy the code below.
#!/usr/bin/env python3
"""My PyneHub App — starter template for the PinePhone.
A minimal, touch-friendly GTK4 + libadwaita app sized for the PinePhone
(360 x 720 portrait). Replace the UI in build_content() with your own.
Run locally to test: python3 main.py
"""
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, Adw
class MainWindow(Adw.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_title("My App")
# PinePhone portrait resolution. Keep this so you design and test at
# the real device size — everything should fit within 360 px wide.
self.set_default_size(360, 720)
# ToolbarView gives you a proper mobile header bar.
toolbar = Adw.ToolbarView()
header = Adw.HeaderBar()
toolbar.add_top_bar(header)
toolbar.set_content(self.build_content())
self.set_content(toolbar)
# ------------------------------------------------------------------
# Build your screen here.
# ------------------------------------------------------------------
def build_content(self):
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=16)
box.set_valign(Gtk.Align.CENTER)
box.set_halign(Gtk.Align.CENTER)
box.set_vexpand(True)
box.set_margin_start(24)
box.set_margin_end(24)
title = Gtk.Label(label="Hello, PinePhone!")
title.add_css_class("title-1")
box.append(title)
subtitle = Gtk.Label(label="Edit build_content() in main.py to make it yours.")
subtitle.add_css_class("dim-label")
subtitle.set_wrap(True)
subtitle.set_justify(Gtk.Justification.CENTER)
box.append(subtitle)
# A big, finger-friendly button (>= 48 px tall is the touch target rule).
button = Gtk.Button(label="Tap me")
button.add_css_class("suggested-action")
button.add_css_class("pill")
button.set_halign(Gtk.Align.CENTER)
button.set_size_request(160, 48)
button.connect("clicked", self.on_button_clicked)
box.append(button)
self.status = Gtk.Label(label="")
self.status.add_css_class("dim-label")
box.append(self.status)
return box
def on_button_clicked(self, _button):
self.status.set_label("You tapped the button! \U0001F389")
class MyApp(Adw.Application):
def __init__(self):
# Use your own reverse-DNS id, e.g. com.yourname.yourapp
super().__init__(application_id="com.example.myapp")
def do_activate(self):
win = MainWindow(application=self)
win.present()
def main():
app = MyApp()
return app.run(None)
if __name__ == "__main__":
main()
Run it locally to see it on screen:
python3 main.pyTurn any website into an app
Want an app for a website you already have? Use this wrapper. Change two lines — the app name and the URL — and you have a native-feeling PinePhone app that shows your site full-screen. This is the fastest way to publish something useful on the hub.
The only edit you need:
APP_NAME = "My Web App"
WEBSITE_URL = "https://example.com"#!/usr/bin/env python3
"""Web App wrapper for PyneHub — turn any website into a PinePhone app.
Change the two lines in the CONFIG block below and you have a native-feeling
app that shows your website full-screen. Built with GTK4 + WebKitGTK 6.0.
Run locally to test: python3 main.py
"""
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
gi.require_version("WebKit", "6.0")
from gi.repository import Gtk, Adw, WebKit
# ============================================================
# CONFIG — edit these two lines. That is all you need to change.
# ============================================================
APP_NAME = "My Web App"
WEBSITE_URL = "https://example.com"
# Show the address/title bar at the top? Set False for a full-screen,
# app-like feel with no visible browser chrome.
SHOW_HEADER = True
# ============================================================
class WebAppWindow(Adw.ApplicationWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_title(APP_NAME)
self.set_default_size(360, 720) # PinePhone portrait
toolbar = Adw.ToolbarView()
if SHOW_HEADER:
header = Adw.HeaderBar()
title = Gtk.Label(label=APP_NAME)
title.add_css_class("title")
header.set_title_widget(title)
# A reload button is handy for web apps.
reload_btn = Gtk.Button(icon_name="view-refresh-symbolic")
reload_btn.set_tooltip_text("Reload")
reload_btn.connect("clicked", lambda _b: self.webview.reload())
header.pack_start(reload_btn)
toolbar.add_top_bar(header)
# Thin loading progress bar under the header.
self.progress = Gtk.ProgressBar()
self.progress.set_visible(False)
# The web view fills the rest of the screen.
self.webview = WebKit.WebView()
settings = self.webview.get_settings()
settings.set_enable_javascript(True)
# A modern mobile user agent so sites serve their phone layout,
# which fits the narrow PinePhone screen best.
settings.set_user_agent(
"Mozilla/5.0 (Linux; Android 13; Mobile) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0 Mobile Safari/537.36"
)
self.webview.set_vexpand(True)
self.webview.connect("notify::estimated-load-progress", self._on_progress)
self.webview.connect("load-changed", self._on_load_changed)
content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
content.append(self.progress)
content.append(self.webview)
toolbar.set_content(content)
self.set_content(toolbar)
self.webview.load_uri(WEBSITE_URL)
def _on_progress(self, webview, _param):
self.progress.set_fraction(webview.get_estimated_load_progress())
def _on_load_changed(self, webview, event):
if event == WebKit.LoadEvent.STARTED:
self.progress.set_visible(True)
elif event == WebKit.LoadEvent.FINISHED:
self.progress.set_visible(False)
self.progress.set_fraction(0.0)
class WebApp(Adw.Application):
def __init__(self):
# Use your own reverse-DNS id, e.g. com.yourname.yourwebapp
super().__init__(application_id="com.example.webapp")
def do_activate(self):
win = WebAppWindow(application=self)
win.present()
def main():
app = WebApp()
return app.run(None)
if __name__ == "__main__":
main()
{
"name": "My Web App",
"version": "1.0.0",
"description": "A web app that opens my website on the PinePhone. Replace this with a description of your site/app.",
"author": "Your Name",
"category": "Internet",
"entry_point": "main.py",
"dependencies": ["gtk4", "libadwaita", "webkitgtk-6.0"]
}
Designing for the PinePhone
The PinePhone screen is tall and narrow. Design for 360 × 720 in portrait and everything else follows:
- One column only — no side-by-side desktop layouts.
- Tap targets at least 48px tall; add spacing so fingers don't mis-tap.
- Use large, readable text; avoid tiny labels.
- Put primary actions within easy thumb reach (lower half of the screen).
- Use an Adw.HeaderBar on top; a bottom switcher bar works well for tabs.
- Let content scroll vertically instead of cramming it in.
- Test at 360px wide before you submit.
Package & publish
When your app runs cleanly, bundle the three files into a tarball:
# From inside your app folder:
tar czf my-app-1.0.0.tar.gz main.py manifest.json icon.pngThen send the .tar.gz to the PyneHub maintainers to have it reviewed and published. Publishing is maintainer-controlled to keep the hub safe, so accounts are issued by the team rather than through open sign-up. Reach out at [email protected]. You can review the rules on the Developer & Contributor Agreement and onboarding pages.
Building with an AI agent (no code required)
Not a programmer? You can have an AI coding assistant build your app. Copy one of the prompts below, fill in the bracketed parts, and paste it into your AI agent. Ask it to return main.py and manifest.json, then package them with the command in the Publish section.
Prompt 1 — a normal app
You are building a native app for the PinePhone (a Linux phone running
the Phosh mobile shell). Build it in Python 3 using GTK4 and libadwaita.
Hard requirements:
- Single file called main.py that runs with: python3 main.py
- Use Adw.Application + Adw.ApplicationWindow with an Adw.ToolbarView and
an Adw.HeaderBar at the top.
- Design for a PORTRAIT phone screen only: 360 x 720 pixels. Everything
must fit within 360px wide. No desktop/multi-column layouts.
- Touch-friendly: buttons and tap targets at least 48px tall, generous
spacing, large readable text.
- No network calls unless I ask for them. No telemetry.
The app should do the following:
[DESCRIBE YOUR APP HERE — e.g. "a tip calculator with a bill amount entry,
a tip % slider, and a large total at the bottom"].
Also give me a manifest.json with these fields: name, version ("1.0.0"),
description, author, category, entry_point ("main.py"), and dependencies
(a list like ["gtk4", "libadwaita"]).Prompt 2 — a website-to-app wrapper
You are building a native "web app" wrapper for the PinePhone (Linux phone,
Phosh shell) in Python 3 with GTK4, libadwaita and WebKitGTK 6.0.
It should open a single website full-screen so it feels like a native app.
Requirements:
- Single file main.py, runs with: python3 main.py
- Two config constants at the top: APP_NAME and WEBSITE_URL.
- Window size 360 x 720 (PinePhone portrait).
- Use a mobile user agent so sites serve their phone layout.
- Show a thin loading progress bar and a reload button in the header.
Use this website: [PUT YOUR WEBSITE URL HERE]
App name: [PUT YOUR APP NAME HERE]
Also give me a matching manifest.json (dependencies must include
"gtk4", "libadwaita" and "webkitgtk-6.0").Prompt 3 — generate a matching icon
Design a simple, modern app icon, 512 x 512 pixels, PNG.
Style: a flat vertical color gradient background in a rounded square, with
a single clean white line-art symbol centered on top. High contrast, no
small details, no text. It must stay recognizable at 48x48.
The symbol should represent: [DESCRIBE YOUR APP, e.g. "a tip calculator"].Tip: give the agent the two templates above as a reference (“follow this structure”) so its output drops straight into the hub.
Hardware, firmware & drivers
Building or rebuilding your own PinePhone? This is a reference for the actual chips inside both models and where to get the matching drivers, operating-system images and bootloader. You don’t need any of this to write an app — it’s here for people working on the hardware itself.
PinePhone — components
PinePhone Pro — components
Screen / display drivers
Both screens are driven by open-source panel drivers that ship inside the mainline Linux kernel — there is no separate proprietary driver to install. If you build your own kernel, enable the driver for your panel below (the PinePhone uses the ST7703 controller; the Pro uses the Himax HX8394).
Operating-system images (Arch Linux)
The community-maintained DanctNIX builds are the go-to Arch Linux ARM images for both phones. Default login is user alarm / password 123456 — change it on first boot.
Bootloader (U-Boot)
The PinePhone boots with U-Boot. Use the official PINE64 source if you want to compile it yourself, or Tow-Boot for a ready-made, user-friendly build.
Datasheets & full specs
PINE64 publishes the datasheet for every chip in the phone, plus the complete specification sheets.