Skip to content

Latest commit

 

History

History
225 lines (189 loc) · 8.17 KB

File metadata and controls

225 lines (189 loc) · 8.17 KB

Progressive Web App (PWA) Guide for PyScript

Build and deploy installable, offline-capable Python applications in the browser using PyDevices and PyScript.

Overview

This template provides a 100% standalone Progressive Web App (PWA) setup that allows your PyScript application to:

  1. Install natively to the user's home screen or desktop application menu.
  2. Work offline by caching the application shell, Python runtime, and assets.
  3. Run in standalone window mode without browser address bars or navigation controls.

Where PWAs Run

Host Platform Install UX Result
Desktop Chrome / Edge (Windows, macOS, Linux) Address bar install icon or Install button Standalone window with dedicated desktop icon
Android Chrome Install app prompt or browser menu Add to Home screen Standalone Android application window
Chromebook (ChromeOS) Address bar install icon or launcher install prompt Dedicated application window pinned to the shelf
iOS / iPadOS Safari Share → Add to Home Screen Fullscreen standalone WebKit app container
Smart TVs (webOS, Tizen) Direct Chromium browser access Runs directly in the TV web runtime

Note

Safari on iOS does not fire the beforeinstallprompt event. Users install the app via Share → Add to Home Screen.

Template Architecture

This template includes everything required for an offline PWA at the repository root:

pyscript-template/
├── index.html         # Application shell loading PyScript, Canvas, and PWA scripts
├── main.py            # Application entry point using pydevices / displaydev / appdev
├── pyscript.json      # PyScript configuration: pinned PyDevices source files
├── manifest.json      # Web App Manifest defining app name, icons, and theme
├── sw.js              # Service Worker for caching and offline execution
├── pwa.js             # Service Worker registration and install prompt handler
├── style.css          # Application and UI styling
├── icon-192.png       # 192x192 PNG application icon
├── icon-512.png       # 512x512 PNG maskable application icon
├── vendor/pyscript/   # Vendored PyScript interpreter — generated by
│                       # scripts/vendor_pyscript.sh, .gitignore'd locally,
│                       # but present in every deploy (the workflow below
│                       # vendors it before publishing)
├── scripts/
│   └── vendor_pyscript.sh   # Downloads and unpacks the pinned PyScript release
├── tests/
│   └── test_template.py     # unittest suite: config validity, asset presence, pins
├── docs/
│   ├── README.md
│   └── pwa-guide.md          # this file
└── .github/workflows/
    ├── deploy.yml     # Vendors PyScript, stamps the SW cache version, deploys to Pages
    └── tests.yml      # Runs the unittest suite on push/PR

Key Components

1. Web App Manifest (manifest.json)

manifest.json tells the browser how your application should appear when installed:

{
  "name": "My PyDevices App",
  "short_name": "PyDevices App",
  "description": "A cross-platform Python display application built with PyDevices and PyScript.",
  "start_url": "./index.html",
  "scope": "./",
  "display": "standalone",
  "background_color": "#100e0b",
  "theme_color": "#f54e00",
  "icons": [
    {
      "src": "./icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "./icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}
  • display: "standalone": Launches the app in its own window without browser chrome.
  • theme_color / background_color: Sets the OS title bar and splash screen background.
  • icons: Specifies standard and maskable icons for device home screens and app launchers.

2. Service Worker (sw.js)

sw.js caches all application shell resources on installation and serves cached assets when offline:

const VERSION = '__DEPLOY_VERSION__';
const CACHE_NAME = 'pyscript-template-' + (VERSION.indexOf('DEPLOY_VERSION') !== -1 ? 'dev' : VERSION);
const SHELL = [
  './',
  './index.html',
  './main.py',
  './pyscript.json',
  './manifest.json',
  './style.css',
  './pwa.js',
  './icon-192.png',
  './icon-512.png',
  './vendor/pyscript/core.css',
  './vendor/pyscript/core.js',
];

self.addEventListener('install', function (event) {
  event.waitUntil(caches.open(CACHE_NAME).then(function (cache) {
    return cache.addAll(SHELL);
  }).then(function () {
    return self.skipWaiting();
  }));
});

self.addEventListener('activate', function (event) {
  event.waitUntil(caches.keys().then(function (names) {
    return Promise.all(names.filter(function (name) {
      return name !== CACHE_NAME;
    }).map(function (name) {
      return caches.delete(name);
    }));
  }).then(function () {
    return self.clients.claim();
  }));
});

self.addEventListener('fetch', function (event) {
  if (event.request.method !== 'GET') return;
  event.respondWith(caches.match(event.request).then(function (cached) {
    if (cached) return cached;
    return fetch(event.request).then(function (response) {
      if (!response || response.status !== 200) return response;
      var copy = response.clone();
      caches.open(CACHE_NAME).then(function (cache) {
        cache.put(event.request, copy);
      });
      return response;
    });
  }));
});

CACHE_NAME includes a version so a new cache is created whenever VERSION changes, and the activate handler deletes every cache that doesn't match. .github/workflows/deploy.yml replaces __DEPLOY_VERSION__ with the commit SHA at deploy time, so every deploy automatically busts stale caches for visitors. Locally, VERSION stays __DEPLOY_VERSION__ (rendered as dev) across edits, so it does not change from commit to commit while you're iterating — if you edit app files and the browser still serves the old version, hard-refresh (or unregister the service worker in devtools) rather than waiting for a cache change that won't happen locally.


3. PWA Installer & Lifecycle (pwa.js)

pwa.js handles registering the Service Worker and managing the custom install prompt button:

(function () {
  var installButton = document.getElementById('install');
  var deferredPrompt = null;

  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('./sw.js', {scope: './'}).catch(console.error);
  }

  window.addEventListener('beforeinstallprompt', function (event) {
    event.preventDefault();
    deferredPrompt = event;
    if (installButton) installButton.hidden = false;
  });

  if (installButton) {
    installButton.addEventListener('click', function () {
      if (!deferredPrompt) return;
      deferredPrompt.prompt();
      deferredPrompt.userChoice.finally(function () {
        deferredPrompt = null;
        installButton.hidden = true;
      });
    });
  }

  window.addEventListener('appinstalled', function () {
    if (installButton) installButton.hidden = true;
  });
})();

Deployment to GitHub Pages

  1. In your GitHub repository, navigate to Settings → Pages.

  2. Under Build and deployment → Source, select GitHub Actions.

  3. Push to main, or trigger it manually from the Actions tab (workflow_dispatch). The included .github/workflows/deploy.yml workflow:

    • checks out the repository,
    • runs ./scripts/vendor_pyscript.sh to populate vendor/pyscript/ (not checked in — this step is what makes it exist in the deploy),
    • stamps sw.js's __DEPLOY_VERSION__ placeholder with the commit SHA,
    • uploads the repository root as a Pages artifact via actions/upload-pages-artifact, and
    • publishes it with actions/deploy-pages.

    The application is published from the repository root — there is no separate pwa/ subdirectory to keep in sync.

Once published:

  • Visit the site on desktop Chrome or Android to test the Install button.
  • Visit on iOS Safari, tap the Share button, and select Add to Home Screen.
  • Disconnect network connectivity to verify offline execution.