---
title: "Multi-App Suites"
vignette: >
  %\VignetteIndexEntry{Multi-App Suites}
  %\VignetteEngine{quarto::html}
  %\VignetteEncoding{UTF-8}
knitr:
  opts_chunk:
    collapse: true
    comment: "#>"
---

```{r}
#| include: false
library(shinyelectron)

# Helper: render a directory tree from a bundled demo, stripped of any ANSI
# colour codes and with the absolute path swapped for the suite name.
print_demo_tree <- function(suite_dir) {
  root <- system.file(paste0("demos/", suite_dir), package = "shinyelectron")
  output <- capture.output(fs::dir_tree(root))
  output <- cli::ansi_strip(output)
  output[1] <- paste0(suite_dir, "/")
  cat(c("```", output, "```"), sep = "\n")
}

# Helper: render a file from a bundled demo as a fenced code block.
print_demo_file <- function(rel_path, lang = "") {
  path <- system.file(paste0("demos/", rel_path), package = "shinyelectron")
  cat(c(paste0("```", lang),
        readLines(path),
        "```"),
      sep = "\n")
}
```

One repo, many apps, one launcher. A suite bundles several Shiny apps, R or Python, into a single Electron binary. Users open the launcher, pick an app, and switch whenever they like through the **Apps** menu.

<figure><picture><source srcset="../man/figures/multi-app-routing-dark.svg" media="(prefers-color-scheme: dark)" /><img src="../man/figures/multi-app-routing.svg" alt="A launcher panel on the left with three app cards. An arrow labelled 'pick an app' leads to an active backend window on the right. A return arrow labelled 'Apps menu: Back' leads from the backend back to the launcher. On the far right, the apps manifest lists dashboard, data-explorer, and about." /></picture><figcaption>The launcher hands off to one backend at a time; picking a different app returns through the launcher.</figcaption></figure>

## Layout

A suite is a directory with a `_shinyelectron.yml` at the root and an `apps/` folder. Each subfolder holds a normal Shiny app (`app.R` for R, `app.py` for Python).

### R suite

The bundled demo at `inst/demos/demo-r-app-suite/`, read live:

```{r}
#| echo: false
#| results: asis
print_demo_tree("demo-r-app-suite")
```

### Python suite

The Python demo at `inst/demos/demo-py-app-suite/` adds one file: a `requirements.txt` at the root that covers every app in the suite.

```{r}
#| echo: false
#| results: asis
print_demo_tree("demo-py-app-suite")
```

## Configuration

A top-level `apps` array switches shinyelectron into suite mode. Each entry needs three fields (`id`, `name`, `path`) and accepts four optional ones (`description`, `type`, `runtime_strategy`, `icon`).

### R suite config

Read live from `inst/demos/demo-r-app-suite/_shinyelectron.yml`:

```{r}
#| echo: false
#| results: asis
print_demo_file("demo-r-app-suite/_shinyelectron.yml", lang = "yaml")
```

### Python suite config

Only `build.type` changes. Read live from `inst/demos/demo-py-app-suite/_shinyelectron.yml`:

```{r}
#| echo: false
#| results: asis
print_demo_file("demo-py-app-suite/_shinyelectron.yml", lang = "yaml")
```

### Fields in an `apps` entry

| Field | Required | Description |
|-------|----------|-------------|
| `id` | Yes | Unique identifier (used in file paths and the apps manifest) |
| `name` | Yes | Display name shown on the launcher card |
| `path` | Yes | Relative path from the suite root to the app directory |
| `description` | No | Short text shown below the name on the launcher card |
| `type` | No | Override the default `build.type` for this specific app |
| `runtime_strategy` | No | Override the default `build.runtime_strategy` for this specific app |
| `icon` | No | Path to an icon image displayed on the launcher card |

Every app inherits `build.type` and `build.runtime_strategy` unless it overrides them. Suites can mix languages (some `r-shiny`, some `py-shiny`) and mix strategies (one app on `shinylive`, another on `system`). Most suites stay uniform, but the overrides are there when you need them.

## Mixed-strategy suites

A single suite can bundle apps with different runtime strategies. Set a suite-wide default in `build.runtime_strategy`, then override it per app where it makes sense. This is handy when one app runs fine in the browser (shinylive) while another needs a real process for packages that do not compile to WebAssembly.

```yaml
# _shinyelectron.yml
app:
  name: "Mixed Suite"
  version: "1.0.0"

build:
  type: "r-shiny"
  runtime_strategy: "shinylive"   # suite default

apps:
  - id: "viewer"
    name: "Data Viewer"
    description: "Lightweight tables and charts"
    path: "./apps/viewer"
    # inherits shinylive from build.runtime_strategy

  - id: "modeler"
    name: "Model Fitter"
    description: "Needs packages that are not in WebR"
    path: "./apps/modeler"
    runtime_strategy: "system"    # overrides the suite default
```

Per-app `runtime_strategy:` overrides the suite default for just that app. The other apps keep inheriting the default. There is one rule on mixing: within a single language, all native apps (`system`, `bundled`, `auto-download`) must use the same strategy. `shinylive` and `container` apps use their own backends and combine freely with anything. So a suite can hold a shinylive R app, a bundled R app, and a system Python app, but it cannot hold both a bundled R app and an auto-download R app, or a bundled R app and a system R app, because that is two native strategies for the same language. Export aborts with a message naming the conflicting apps if you cross that line. The usual platform caveats still apply (for example, bundled R needs a portable R build for the target OS).

## How dependencies are handled

R apps can be scanned for dependencies; Python apps cannot. R lets you skip declaration entirely and rely on the source. Python always asks you to write a list, just in one of two files. Both languages can also use `_shinyelectron.yml` for explicit declarations, and lists from any source merge into one install plan.

### R apps

**Automatic.** shinyelectron runs `renv::dependencies()` over each app directory. It picks up `library()`, `require()`, `pkg::func()`, and `loadNamespace()` references in the source. Base and recommended R packages (`stats`, `utils`, `MASS`, `lattice`, and friends) are excluded automatically.

**Explicit.** Add a `dependencies:` block to the suite-level `_shinyelectron.yml`:

```yaml
dependencies:
  r:
    packages: [shiny, ggplot2, dplyr]
    repos: ["https://cloud.r-project.org"]
```

Source-scan and declared packages merge. Set `dependencies.auto_detect: false` to skip the renv scan and install only the declared list.

### Python apps

shinyelectron does not parse `import` statements: the module-name to package-name mapping is unreliable (`import cv2` is `opencv-python`, `import sklearn` is `scikit-learn`). You declare what gets installed, in one of two places.

**`requirements.txt` or `pyproject.toml`.** A standard Python manifest at the suite root. All apps in the suite share one virtual environment. The demo's `requirements.txt`:

```{r}
#| echo: false
#| results: asis
print_demo_file("demo-py-app-suite/requirements.txt")
```

That file covers every app in the suite.

**`_shinyelectron.yml`.** Add a `python` section to the same `dependencies:` block:

```yaml
dependencies:
  python:
    packages: [shiny, numpy, pandas]
    index_urls: ["https://pypi.org/simple"]
```

Manifest-file and YAML packages merge. Set `dependencies.auto_detect: false` to skip reading the manifest and install only the YAML list.

## Exporting a suite

`export()` reads the config, sees an `apps` array with two or more entries, and switches to suite mode. No extra flags.

```{r}
#| eval: false
library(shinyelectron)

# Export the bundled R demo suite
r_suite <- example_app("suite")
export(appdir = r_suite, destdir = "output-r-suite")

# Export a Python suite from your own project
export(appdir = "path/to/my-py-suite", destdir = "output-py-suite")
```

You do not pass `app_type` or `runtime_strategy`. The YAML owns those choices, through `build.type` and `build.runtime_strategy`.

All five [runtime strategies](runtime-strategies.html) (shinylive, system, bundled, auto-download, container) work with suites, exactly as they do with single apps. Per-app overrides let one suite mix them, subject to the one-native-strategy-per-language rule described above.

## Launcher and app switching

The Electron window opens to a launcher page. Each app gets a card with its name, description, type badge, and icon (your image, or a generated initial).

Click a card: the backend for that app starts, and the window navigates to it. The **Apps** menu offers "Back to Launcher," which stops the backend and returns to the card grid.

### One runtime, one active app

A suite runs a single R or Python process. Switching apps follows four steps:

1. Stop the current backend.
2. Show the launcher.
3. User picks the next app.
4. Start a fresh backend for that app.

Only one app runs at a time. No parallel runtimes, no duplicated processes, no extra installs.

## Building your own suite

From scratch:

1. Create a directory with an `apps/` subfolder.
2. Put each Shiny app in its own subdirectory.
3. Write `_shinyelectron.yml` with an `apps` array listing every app.
4. For Python suites, add `requirements.txt` at the root.
5. Run `export()` on the suite directory.

```{r}
#| eval: false
# Minimal example: create suite structure, then export
export(appdir = "my-suite", destdir = "my-suite-output")
```

The config file is the single source of truth. Two or more entries in `apps`, each pointing at a valid app, and shinyelectron builds the launcher, the menu, and the switching logic for you.
