Skip to content
Closed
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
58 changes: 58 additions & 0 deletions .github/workflows/appimage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Build AppImage

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
release:
types: [ published ]
workflow_dispatch:

permissions:
contents: write

jobs:
appimage:
runs-on: ubuntu-22.04

steps:
- name: Install build dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -y \
cmake \
curl \
g++ \
git \
libfuse2 \
libpng-dev \
libsdl2-dev \
ninja-build \
npm \
python3-pil \
rsync
sudo npm install -g lv_font_conv@1.5.2

- name: Checkout source files
uses: actions/checkout@v4
with:
submodules: recursive

- name: Build AppImage
run: |
chmod +x ./packaging/appimage/build-appimage.sh
./packaging/appimage/build-appimage.sh

- name: Upload AppImage
uses: actions/upload-artifact@v4
with:
name: InfiniSim-AppImage
path: ./*.AppImage
if-no-files-found: error

- name: Attach AppImage to release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: ./*.AppImage
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
.idea/
# Python virtual environment for DFU images
.venv/
__pycache__/
scripts/infinisim-launcher-ui.py.bak

# CMake
cmake-build-*
Expand All @@ -22,6 +24,8 @@ tools
!bootloader/bootloader-5.0.4.bin
*.map
*.out
*.AppImage
*.raw
pinetime*.cbp

# InfiniTime's files
Expand Down
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,13 @@ add_subdirectory(img)
add_dependencies(infinisim infinisim_img_background)

install(TARGETS infinisim DESTINATION bin)
install(PROGRAMS scripts/infinisim-launcher.sh DESTINATION bin RENAME infinisim-launcher)
install(PROGRAMS scripts/infinisim-launcher-ui.py DESTINATION bin RENAME infinisim-launcher-ui.py)
install(PROGRAMS scripts/infinisim_launcher_i18n.py DESTINATION bin)

# Install i18n translation files
install(FILES scripts/infinisim-launcher-en.mo DESTINATION share/locale/en/LC_MESSAGES RENAME infinisim-launcher.mo)
install(FILES scripts/infinisim-launcher-es.mo DESTINATION share/locale/es/LC_MESSAGES RENAME infinisim-launcher.mo)

# helper library to manipulate littlefs raw image
add_executable(littlefs-do
Expand Down
100 changes: 100 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,106 @@ Or use it to develop new Watchfaces, new Screens, or quickly iterate on the user

For a history on how this simulator started and the challenges on its way visit the [original PR](https://github.com/InfiniTimeOrg/InfiniTime/pull/743).

## Quick start

The recommended way to distribute InfiniSim is the AppImage built by GitHub Actions.
It contains the launcher, InfiniSim sources, and a prebuilt simulator for the bundled official InfiniTime checkout.
That means the default official path does not require users to install CMake, SDL2 development headers, Node.js, or
Python packages locally.

After downloading the AppImage:

```sh
chmod +x InfiniSim-*.AppImage
./InfiniSim-*.AppImage
```

On first start it shows 3 simple actions:

- start a simulator that is already compiled (the bundled official binary is preferred when present)
- choose a local InfiniTime source directory and compile it
- clone/update the official InfiniTime repository and compile it

The launcher now uses a GTK interface (GNOME-style) when available:

- each action is shown as a card with icon, title, and short description
- after selecting an action, a loading spinner is displayed
- execution logs are available in an expandable details panel at the bottom

If GTK Python bindings are not available, the launcher falls back to the previous text/dialog flow.

When cloning from the launcher, InfiniSim prefers the InfiniTime revision that was validated for that AppImage build.
This keeps clone-and-build behavior reproducible and avoids breakage from upstream API drift.

Build output and cloned sources are kept outside the AppImage:

- official InfiniTime clone: `~/.local/share/infinisim/InfiniTime`
- build cache for custom local sources: `~/.cache/infinisim`
- last selected source path and executable path: `~/.config/infinisim/launcher.conf`

InfiniTime sources are used at compile time. Because of that, choosing a custom local InfiniTime checkout requires a
local rebuild. The AppImage keeps a zero-build path with the bundled binary, and only asks for build tools when you
choose one of the compile actions or set `INFINISIM_FORCE_REBUILD=1`.

Advanced launcher overrides:

- `INFINISIM_BINARY=/path/to/infinisim` starts a precompiled simulator directly
- `INFINITIME_DIR=/path/to/InfiniTime` skips the chooser and compiles that source tree

For development from a source checkout, run the same launcher directly:

```sh
./scripts/infinisim-launcher.sh
```

When a rebuild is needed, these native build tools are required:

- CMake
- Git
- SDL2 development files
- a C++ compiler (`g++` or `clang++`)
- Node.js/npm
- Python 3

On Ubuntu/Debian:

```sh
sudo apt install -y cmake git libsdl2-dev g++ npm python3 python3-venv
```

On Fedora:

```sh
sudo dnf install cmake git SDL2-devel gcc-c++ npm python3 python3-virtualenv
```

The launcher installs `lv_font_conv` and `Pillow` in its own cache when they are not available globally.

### Build an AppImage

An AppImage wrapper can be generated with:

```sh
./packaging/appimage/build-appimage.sh
```

This helper builds the official simulator binary, places it inside the AppImage, packages the launcher and source tree,
and uses `linuxdeploy` to bundle runtime libraries.

Prerequisites for generating the AppImage locally:

- clone with submodules (`git clone --recursive ...`), or run `git submodule update --init --recursive`
- host build tools: `cmake`, `git`, `g++`/`clang++`, `libsdl2-dev`, `npm`, Python 3 + Pillow
- packaging helpers: `curl`, `rsync`, and FUSE runtime support for AppImage tooling

Optional (recommended for the rich launcher UI):

- `python3-gi` and GTK 3 runtime libraries

The resulting file is generated at the repository root as `InfiniSim-<arch>.AppImage`.

GitHub Actions builds the same artifact via `.github/workflows/appimage.yml`.

## Get the Sources

Clone this repository and tell `git` to recursively download the submodules as well
Expand Down
64 changes: 60 additions & 4 deletions main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -429,10 +429,66 @@ class Framework {
// SDL_Init(SDL_INIT_VIDEO); // Initializing SDL as Video
SDL_CreateWindowAndRenderer(width, height, 0, &window, &renderer);
SDL_SetWindowTitle(window, "LV Simulator Status");
{ // move window a bit to the right, to not be over the PineTime Screen
int x, y;
SDL_GetWindowPosition(window, &x, &y);
SDL_SetWindowPosition(window, x + LV_HOR_RES_MAX, y);
{ // keep both simulator windows aligned vertically and separated horizontally
SDL_Window* tftWindow = nullptr;
const Uint32 statusId = SDL_GetWindowID(window);
if (statusId > 1) {
SDL_Window* candidate = SDL_GetWindowFromID(statusId - 1);
if (candidate != nullptr && candidate != window) {
tftWindow = candidate;
}
}

if (tftWindow == nullptr) {
constexpr Uint32 maxWindowSearch = 256;
for (Uint32 offset = 1; offset <= maxWindowSearch; ++offset) {
if (statusId > offset) {
SDL_Window* previous = SDL_GetWindowFromID(statusId - offset);
if (previous != nullptr && previous != window) {
tftWindow = previous;
break;
}
}

SDL_Window* next = SDL_GetWindowFromID(statusId + offset);
if (next != nullptr && next != window) {
tftWindow = next;
break;
}
}
}

// Get display bounds to center windows on screen
SDL_Rect displayBounds;
if (SDL_GetDisplayBounds(0, &displayBounds) != 0) {
displayBounds = {0, 0, 1280, 720};
}

int screenCenterY = displayBounds.y + (displayBounds.h / 2);

if (tftWindow != nullptr) {
int tftX = 0;
int tftW = 0;
int tftH = 0;
int statusH = 0;

SDL_GetWindowPosition(tftWindow, &tftX, nullptr);
SDL_GetWindowSize(tftWindow, &tftW, &tftH);
SDL_GetWindowSize(window, nullptr, &statusH);

// Calculate Y position to center vertically on screen
int centeredY = screenCenterY - (std::max(tftH, statusH) / 2);

// Keep the TFT window untouched and place the status window to its right
SDL_SetWindowPosition(window, tftX + tftW + 28, centeredY);
} else {
int statusH = 0;
SDL_GetWindowSize(window, nullptr, &statusH);

// Center this window vertically on screen
int centeredY = screenCenterY - (statusH / 2);
SDL_SetWindowPosition(window, displayBounds.x + 100, centeredY);
}
}
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); // setting draw color
SDL_RenderClear(renderer); // Clear the newly created window
Expand Down
5 changes: 5 additions & 0 deletions packaging/appimage/AppRun
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail

HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
exec "${HERE}/usr/bin/infinisim-launcher" "$@"
97 changes: 97 additions & 0 deletions packaging/appimage/build-appimage.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
APPDIR="${ROOT_DIR}/build/AppDir"
LINUXDEPLOY="${ROOT_DIR}/build/linuxdeploy-x86_64.AppImage"
OFFICIAL_BUILD_DIR="${ROOT_DIR}/build/appimage-official"

rm -rf "${APPDIR}"

mkdir -p "${APPDIR}/usr/bin" \
"${APPDIR}/usr/share/applications" \
"${APPDIR}/usr/share/icons/hicolor/scalable/apps" \
"${APPDIR}/usr/share/infinisim/resources"

if [[ -d "${ROOT_DIR}/InfiniTime/src/libs/lvgl/src" ]]; then
official_ref="$(git -C "${ROOT_DIR}/InfiniTime" rev-parse HEAD)"

cmake -S "${ROOT_DIR}" -B "${OFFICIAL_BUILD_DIR}" \
-DCMAKE_BUILD_TYPE=Release \
-DWITH_PNG=OFF
cmake --build "${OFFICIAL_BUILD_DIR}" --parallel "$(getconf _NPROCESSORS_ONLN 2>/dev/null || printf '2')"

install -m 0755 "${OFFICIAL_BUILD_DIR}/infinisim" "${APPDIR}/usr/bin/infinisim-official"
install -m 0755 "${OFFICIAL_BUILD_DIR}/littlefs-do" "${APPDIR}/usr/bin/littlefs-do"
if [[ -f "${OFFICIAL_BUILD_DIR}/resources/resource.zip" ]]; then
install -m 0644 "${OFFICIAL_BUILD_DIR}/resources/resource.zip" "${APPDIR}/usr/share/infinisim/resources/resource.zip"
fi
printf '%s\n' "${official_ref}" > "${APPDIR}/usr/share/infinisim/resources/infinitime-ref.txt"
else
printf '%s\n' "InfiniTime submodule is missing; AppImage will not include the official prebuilt binary." >&2
fi

install -m 0755 "${ROOT_DIR}/scripts/infinisim-launcher.sh" "${APPDIR}/usr/bin/infinisim-launcher"
install -m 0755 "${ROOT_DIR}/scripts/infinisim-launcher-ui.py" "${APPDIR}/usr/bin/infinisim-launcher-ui.py"
install -m 0755 "${ROOT_DIR}/scripts/infinisim_launcher_i18n.py" "${APPDIR}/usr/bin/infinisim_launcher_i18n.py"

# Install i18n translation files
mkdir -p "${APPDIR}/usr/share/locale/en/LC_MESSAGES" \
"${APPDIR}/usr/share/locale/es/LC_MESSAGES"
install -m 0644 "${ROOT_DIR}/scripts/infinisim-launcher-en.mo" "${APPDIR}/usr/share/locale/en/LC_MESSAGES/infinisim-launcher.mo"
install -m 0644 "${ROOT_DIR}/scripts/infinisim-launcher-es.mo" "${APPDIR}/usr/share/locale/es/LC_MESSAGES/infinisim-launcher.mo"
install -m 0755 "${ROOT_DIR}/packaging/appimage/AppRun" "${APPDIR}/AppRun"
install -m 0644 "${ROOT_DIR}/packaging/appimage/infinisim.desktop" "${APPDIR}/usr/share/applications/infinisim.desktop"
install -m 0644 "${ROOT_DIR}/packaging/appimage/infinisim.svg" "${APPDIR}/usr/share/icons/hicolor/scalable/apps/infinisim.svg"

if command -v git >/dev/null 2>&1; then
install -m 0755 "$(command -v git)" "${APPDIR}/usr/bin/git"
if [[ -d /usr/lib/git-core ]]; then
mkdir -p "${APPDIR}/usr/lib"
rsync -a /usr/lib/git-core "${APPDIR}/usr/lib/"
fi
if [[ -d /usr/share/git-core ]]; then
mkdir -p "${APPDIR}/usr/share"
rsync -a /usr/share/git-core "${APPDIR}/usr/share/"
fi
fi

rsync -a --delete \
--exclude '/.git' \
--exclude '/build' \
--exclude '/InfiniTime' \
--exclude '/node_modules' \
--exclude '/.venv' \
"${ROOT_DIR}/" "${APPDIR}/usr/share/infinisim/source/"

if [[ ! -x "${LINUXDEPLOY}" ]]; then
curl -L \
https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage \
-o "${LINUXDEPLOY}"
chmod +x "${LINUXDEPLOY}"
fi

linuxdeploy_args=(
--appdir "${APPDIR}"
--desktop-file "${APPDIR}/usr/share/applications/infinisim.desktop"
--icon-file "${APPDIR}/usr/share/icons/hicolor/scalable/apps/infinisim.svg"
)

if [[ -x "${APPDIR}/usr/bin/infinisim-official" ]]; then
linuxdeploy_args+=(--executable "${APPDIR}/usr/bin/infinisim-official")
fi

if [[ -x "${APPDIR}/usr/bin/littlefs-do" ]]; then
linuxdeploy_args+=(--executable "${APPDIR}/usr/bin/littlefs-do")
fi

if [[ -x "${APPDIR}/usr/bin/git" ]]; then
linuxdeploy_args+=(--executable "${APPDIR}/usr/bin/git")
while IFS= read -r helper; do
if readelf -h "${helper}" >/dev/null 2>&1; then
linuxdeploy_args+=(--executable "${helper}")
fi
done < <(find "${APPDIR}/usr/lib/git-core" -maxdepth 1 -type f -executable 2>/dev/null)
fi

NO_STRIP=1 ARCH=x86_64 "${LINUXDEPLOY}" "${linuxdeploy_args[@]}" --output appimage
8 changes: 8 additions & 0 deletions packaging/appimage/infinisim.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[Desktop Entry]
Type=Application
Name=InfiniSim
Comment=InfiniTime simulator launcher
Exec=infinisim-launcher
Icon=infinisim
Categories=Development;Emulator;
Terminal=false
7 changes: 7 additions & 0 deletions packaging/appimage/infinisim.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added scripts/infinisim-launcher-en.mo
Binary file not shown.
Loading
Loading