diff --git a/CLAUDE.md b/CLAUDE.md index 0a14f64..dacc8ed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,13 @@ # CLAUDE.md — todo -Offline-first, CRDT-synced notes app. Flutter, targets **Android + Linux**. +Offline-first, CRDT-synced notes app. Flutter, targets **Android + web**. + +The **desktop app is the web build**, served by a small local wrapper +(`bin/todo_desktop.dart`) and shown in a Chrome `--app` window. Flutter's Linux +embedder was removed: it manages only ~20fps at 4K while the same Dart code in +Chrome sustains ~144fps. See `docs/desktop-performance-findings.md` for the +measurements. Do not re-add `linux/` — `flutter create --platforms linux` would +silently bring the slow path back. Capture an idea instantly (persisted on every keystroke), browse/filter notes, and sync peer-to-peer through a GitHub repo used as dumb storage. @@ -24,7 +31,12 @@ and sync peer-to-peer through a GitHub repo used as dumb storage. - Coverage summary: `lcov --list coverage/lcov.info`. - Static analysis: `flutter analyze` (must be clean before committing). - Format: `dart format lib/ test/`. -- Run the app: `flutter run` (Linux desktop or a connected Android device). +- Run the desktop app: `./run.sh` (builds the web bundle, starts the wrapper, + opens Chrome). Mobile: `flutter run` with a connected Android device. +- The wrapper serves on a **fixed** port 8730 with a **fixed** Chrome profile + dir. Both must stay fixed: the GitHub token (`localStorage`) and the notes + (IndexedDB) are keyed by origin and live in that profile, so changing either + silently logs you out and hides your notes. - Release APK: `flutter build apk --release` (signs with the debug key; debug builds are janky — always measure smoothness on a release build). - Sync smoke test (hits real GitHub, needs a token): `dart run tool/sync_smoke.dart`. diff --git a/bin/todo_desktop.dart b/bin/todo_desktop.dart index a1c5027..0c0b414 100644 --- a/bin/todo_desktop.dart +++ b/bin/todo_desktop.dart @@ -19,10 +19,12 @@ Future main(List args) async { exit(1); } - // Assets sit next to this executable when installed; --web-root overrides for - // development runs straight out of the repo. + // `dart build cli` emits bundle/bin/ alongside bundle/lib/, so the + // installed layout is /opt/todo/bin/todo-desktop with the web assets one + // level up at /opt/todo/web. --web-root overrides for development runs + // straight out of the repo. final webRoot = _argValue(args, '--web-root') ?? - p.join(p.dirname(Platform.resolvedExecutable), 'web'); + p.normalize(p.join(p.dirname(Platform.resolvedExecutable), '..', 'web')); if (!Directory(webRoot).existsSync()) { stderr.writeln('web assets not found at $webRoot'); exit(1); @@ -40,37 +42,67 @@ Future main(List args) async { await server.start(_port); stdout.writeln('todo desktop serving on http://localhost:$_port'); - if (_argValue(args, '--no-browser') == null) { - await _launchBrowser(home); - // The browser owns the session: when its window closes, so does the app. + // A bare flag, not a valued option: requiring a dummy value meant passing a + // stray positional, which the AOT runtime tries to interpret as a snapshot. + if (!args.contains('--no-browser')) { + final ranLongEnough = await _launchBrowser(home); + if (!ranLongEnough) { + // Chrome exits immediately when it hands the URL to an instance that + // already owns the profile directory (or when a stale SingletonLock is + // left behind). Shutting down here would pull the server out from under + // a window that is still open, so keep serving instead. + stdout.writeln( + 'Browser returned immediately (handed off to an existing window). ' + 'Still serving on http://localhost:$_port — Ctrl-C to stop.', + ); + return; + } + // Otherwise the browser owned the session: its window closed, so we exit. await server.stop(); } } /// Launches the app in a Chrome-family browser with a **stable** profile /// directory, since the token and notes live in that profile. -Future _launchBrowser(String home) async { - const candidates = [ +/// Returns true when the browser ran long enough to have owned the session. +Future _launchBrowser(String home) async { + // Ordered by preference, and deliberately broad: this machine runs Thorium + // behind /opt/google/chrome, and has a policy that uninstalls the `chromium` + // package, so assuming any single browser is wrong. TODO_BROWSER overrides. + final candidates = [ + Platform.environment['TODO_BROWSER'] ?? '', '/opt/google/chrome/chrome', - '/usr/bin/chromium', + '/opt/thorium-browser/thorium-browser', '/usr/bin/google-chrome-stable', + '/usr/bin/chromium', + '/usr/bin/brave', ]; final browser = candidates.firstWhere( - (path) => File(path).existsSync(), + (path) => path.isNotEmpty && File(path).existsSync(), orElse: () => '', ); if (browser.isEmpty) { stderr.writeln('No Chrome-family browser found; open ' 'http://localhost:$_port manually.'); - return; + return false; } + // The profile directory must stay stable: the GitHub token (localStorage) + // and the notes (IndexedDB) live inside it, so a changing path silently + // logs the user out and hides their notes. final profile = p.join(home, '.local', 'share', 'todo-desktop', 'profile'); final process = await Process.start(browser, [ '--app=http://localhost:$_port', '--user-data-dir=$profile', + // Sets WM_CLASS, which the .desktop entry matches on via StartupWMClass. + // Without it the window inherits the browser's class and the taskbar shows + // a browser icon instead of todo's. + '--class=todo', '--no-first-run', ]); + + final started = DateTime.now(); await process.exitCode; + return DateTime.now().difference(started) > const Duration(seconds: 5); } String? _argValue(List args, String name) { diff --git a/linux/icons/128/todo.png b/desktop/icons/128/todo.png similarity index 100% rename from linux/icons/128/todo.png rename to desktop/icons/128/todo.png diff --git a/linux/icons/16/todo.png b/desktop/icons/16/todo.png similarity index 100% rename from linux/icons/16/todo.png rename to desktop/icons/16/todo.png diff --git a/linux/icons/24/todo.png b/desktop/icons/24/todo.png similarity index 100% rename from linux/icons/24/todo.png rename to desktop/icons/24/todo.png diff --git a/linux/icons/256/todo.png b/desktop/icons/256/todo.png similarity index 100% rename from linux/icons/256/todo.png rename to desktop/icons/256/todo.png diff --git a/linux/icons/32/todo.png b/desktop/icons/32/todo.png similarity index 100% rename from linux/icons/32/todo.png rename to desktop/icons/32/todo.png diff --git a/linux/icons/48/todo.png b/desktop/icons/48/todo.png similarity index 100% rename from linux/icons/48/todo.png rename to desktop/icons/48/todo.png diff --git a/linux/icons/512/todo.png b/desktop/icons/512/todo.png similarity index 100% rename from linux/icons/512/todo.png rename to desktop/icons/512/todo.png diff --git a/linux/icons/64/todo.png b/desktop/icons/64/todo.png similarity index 100% rename from linux/icons/64/todo.png rename to desktop/icons/64/todo.png diff --git a/linux/install_desktop_entry.sh b/desktop/install_desktop_entry.sh similarity index 70% rename from linux/install_desktop_entry.sh rename to desktop/install_desktop_entry.sh index f10bf5f..869843e 100755 --- a/linux/install_desktop_entry.sh +++ b/desktop/install_desktop_entry.sh @@ -3,32 +3,38 @@ # ============================================================================ # Install the todo launcher icon and .desktop entry for the Linux desktop. # -# Flutter's GTK runner has no icon of its own: my_application.cc calls -# gtk_window_set_icon_name("todo"), which resolves through the hicolor icon -# theme. This script is what puts the icon into that theme, plus a .desktop -# entry so the app appears in application menus. +# The desktop app is a Flutter *web* build shown in a Chrome app window (the +# Linux embedder manages only ~20fps at 4K — see +# docs/desktop-performance-findings.md), so the launcher starts the wrapper, +# which serves the build and opens the browser. # -# Icon PNGs are pre-rendered and committed under linux/icons/, so this script +# Icon PNGs are pre-rendered and committed under desktop/icons/, so this script # needs no image tooling at run time. Regenerate them with: # PYTHONPATH=~/testsAndMisc python3 -m python_pkg.app_icons \ -# generate --app todo --linux-out linux/icons +# generate --app todo --linux-out desktop/icons # ============================================================================ set -euo pipefail SCRIPT_NAME="$(basename "$0")" -LINUX_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_DIR="$(dirname "$LINUX_DIR")" -readonly SCRIPT_NAME LINUX_DIR REPO_DIR +DESKTOP_DIR_SRC="$(cd "$(dirname "$0")" && pwd)" +REPO_DIR="$(dirname "$DESKTOP_DIR_SRC")" +readonly SCRIPT_NAME DESKTOP_DIR_SRC REPO_DIR readonly ICON_NAME="todo" -readonly BINARY_NAME="todo" -# GTK derives WM_CLASS from the GApplication id in linux/CMakeLists.txt, -# not from the binary name; verified with xprop on the running window. -readonly WM_CLASS="dev.kuhy.todo" -readonly ICON_SRC_DIR="$LINUX_DIR/icons" +# Chrome sets WM_CLASS from --class, which the wrapper passes explicitly. +# Without it the window would inherit the browser's own class (e.g. +# "Google-chrome"), and the taskbar would show a browser icon for the app. +readonly WM_CLASS="todo" +readonly ICON_SRC_DIR="$DESKTOP_DIR_SRC/icons" readonly ICON_THEME_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" readonly DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications" -readonly BUNDLE_BIN="$REPO_DIR/build/linux/x64/release/bundle/$BINARY_NAME" +# Prefer the packaged launcher; fall back to running straight from the repo so +# the entry also works for a development checkout that was never installed. +LAUNCH_CMD="/usr/bin/todo" +if [[ ! -x "$LAUNCH_CMD" ]]; then + LAUNCH_CMD="$REPO_DIR/run.sh" +fi +readonly LAUNCH_CMD usage() { echo "Usage: $SCRIPT_NAME" @@ -79,7 +85,7 @@ install_desktop_entry() { Type=Application Name=todo Comment=Offline-first, CRDT-synced notes -Exec=$BUNDLE_BIN +Exec=$LAUNCH_CMD Icon=$ICON_NAME Terminal=false Categories=Utility;TextEditor; @@ -97,9 +103,9 @@ main() { install_desktop_entry echo "Installed $ICON_NAME icon into $ICON_THEME_DIR" echo "Installed $DESKTOP_DIR/$ICON_NAME.desktop" - if [[ ! -x "$BUNDLE_BIN" ]]; then - echo "Note: $BUNDLE_BIN does not exist yet." - echo " Run 'flutter build linux --release' to make the entry launchable." + if [[ ! -x "$LAUNCH_CMD" ]]; then + echo "Note: $LAUNCH_CMD does not exist yet." + echo " Run ./install_arch.sh to build and install the app." fi } diff --git a/install_arch.sh b/install_arch.sh index 08190f8..fb252b6 100755 --- a/install_arch.sh +++ b/install_arch.sh @@ -6,7 +6,12 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -BUNDLE_DIR="$SCRIPT_DIR/build/linux/x64/release/bundle" +# The desktop app is a Flutter *web* build served by a small local wrapper. +# Flutter's Linux embedder only reaches ~20fps at 4K on this hardware, while +# the same Dart code in Chrome sustains ~144fps — see +# docs/desktop-performance-findings.md for the measurements. +WEB_DIR="$SCRIPT_DIR/build/web" +WRAPPER_BUNDLE="$SCRIPT_DIR/build/cli/bundle" WORK_DIR="$(mktemp -d)" cleanup() { rm -rf "$WORK_DIR"; } @@ -15,9 +20,17 @@ trap cleanup EXIT # Parse version from pubspec.yaml (strip build number: 1.0.0+1 → 1.0.0) PKGVER="$(grep '^version:' "$SCRIPT_DIR/pubspec.yaml" | sed 's/^version:[[:space:]]*//' | sed 's/+.*//')" -echo "==> Building todo $PKGVER (Flutter release)..." +echo "==> Building todo $PKGVER (Flutter web release)..." cd "$SCRIPT_DIR" -flutter build linux --release +flutter build web --release + +echo "==> Compiling the desktop wrapper..." +# AOT-compiled so the installed package needs no Dart SDK at run time. +# `dart build cli`, not `dart compile exe`: the package pulls in dependencies +# with native build hooks (sqlite3, objective_c), which `dart compile` refuses +# to handle even though the wrapper itself uses none of them. +rm -rf "$SCRIPT_DIR/build/cli" +dart build cli -o "$SCRIPT_DIR/build/cli" echo "==> Generating PKGBUILD..." # pkgname is 'todo-flutter', NOT 'todo': the AUR already ships an unrelated @@ -33,24 +46,41 @@ pkgdesc='Offline-first notes app' arch=('x86_64') url='https://github.com/kuhyx/todo-app' license=('custom') -depends=('gtk3') +# No hard browser dependency. A Chrome-family browser renders the UI, but +# naming one as a dependency is wrong here: makepkg installs it, and this +# system has a policy that immediately removes 'chromium' again, which then +# fails dependency resolution. The wrapper discovers whatever browser is +# actually present (including Thorium) and reports clearly if none is. +depends=() +optdepends=('chromium: renders the app window' + 'google-chrome: renders the app window') conflicts=('todo') replaces=('todo') # Flutter release binaries are already stripped/AOT-compiled with no # extractable DWARF debug info, so a split -debug package is pointless here # and gdb-add-index just fails noisily on every binary. Skip it, overriding # the system-wide 'debug' OPTIONS setting. -options=('!debug') +# !strip is load-bearing: the wrapper is a Dart AOT executable with its snapshot +# embedded in the ELF, and stripping it discards that snapshot. The stripped +# binary still runs but is just the bare Dart VM, which prints a usage message +# instead of starting the app. +options=('!strip' '!debug') package() { + # Preserve the bundle's bin/ + lib/ layout: the executable loads its + # native libraries from ../lib, and resolves the web assets from ../web. install -dm755 "\$pkgdir/opt/todo" - cp -r "$BUNDLE_DIR/." "\$pkgdir/opt/todo/" - chmod 755 "\$pkgdir/opt/todo/todo" + cp -r "$WRAPPER_BUNDLE/bin" "\$pkgdir/opt/todo/bin" + cp -r "$WRAPPER_BUNDLE/lib" "\$pkgdir/opt/todo/lib" + chmod 755 "\$pkgdir/opt/todo/bin/todo_desktop" + + install -dm755 "\$pkgdir/opt/todo/web" + cp -r "$WEB_DIR/." "\$pkgdir/opt/todo/web/" install -dm755 "\$pkgdir/usr/bin" cat > "\$pkgdir/usr/bin/todo" <<'WRAPPER' #!/bin/bash -exec /opt/todo/todo "\$@" +exec /opt/todo/bin/todo_desktop "\$@" WRAPPER chmod 755 "\$pkgdir/usr/bin/todo" } diff --git a/linux/.gitignore b/linux/.gitignore deleted file mode 100644 index d3896c9..0000000 --- a/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt deleted file mode 100644 index 698edef..0000000 --- a/linux/CMakeLists.txt +++ /dev/null @@ -1,128 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "todo") -# The unique GTK application identifier for this application. See: -# https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "dev.kuhy.todo") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(SET CMP0063 NEW) - -# Load bundled libraries from the lib/ directory relative to the binary. -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Root filesystem for cross-building. -if(FLUTTER_TARGET_PLATFORM_SYSROOT) - set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) - set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -endif() - -# Define build configuration options. -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") -endif() - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_14) - target_compile_options(${TARGET} PRIVATE -Wall -Werror) - target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") - target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) - -# Only the install-generated bundle's copy of the executable will launch -# correctly, since the resources must in the right relative locations. To avoid -# people trying to run the unbundled copy, put it in a subdirectory instead of -# the default top-level location. -set_target_properties(${BINARY_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" -) - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# By default, "installing" just makes a relocatable bundle in the build -# directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) - install(FILES "${bundled_library}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endforeach(bundled_library) - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") - install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt deleted file mode 100644 index d5bd016..0000000 --- a/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 3ccd551..0000000 --- a/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,23 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include -#include - -void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); - file_selector_plugin_register_with_registrar(file_selector_linux_registrar); - g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); - flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); - g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); - url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); -} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47..0000000 --- a/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake deleted file mode 100644 index fbedf4a..0000000 --- a/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,27 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - file_selector_linux - flutter_secure_storage_linux - url_launcher_linux -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST - jni -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt deleted file mode 100644 index e97dabc..0000000 --- a/linux/runner/CMakeLists.txt +++ /dev/null @@ -1,26 +0,0 @@ -cmake_minimum_required(VERSION 3.13) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the application ID. -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Add dependency libraries. Add any application-specific dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) - -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/linux/runner/main.cc b/linux/runner/main.cc deleted file mode 100644 index e7c5c54..0000000 --- a/linux/runner/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc deleted file mode 100644 index 9d02fa3..0000000 --- a/linux/runner/my_application.cc +++ /dev/null @@ -1,153 +0,0 @@ -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication { - GtkApplication parent_instance; - char** dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Called when first Flutter frame received. -static void first_frame_cb(MyApplication* self, FlView* view) { - gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); -} - -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "todo"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "todo"); - } - - gtk_window_set_default_size(window, 1280, 720); - - // Resolved from the hicolor icon theme, which linux/install_desktop_entry.sh - // populates from linux/icons/. Without this the GTK runner shows a generic - // placeholder in the taskbar, since Flutter never sets a window icon itself. - gtk_window_set_icon_name(window, "todo"); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments( - project, self->dart_entrypoint_arguments); - - FlView* view = fl_view_new(project); - GdkRGBA background_color; - // Background defaults to black, override it here if necessary, e.g. #00000000 - // for transparent. - gdk_rgba_parse(&background_color, "#000000"); - fl_view_set_background_color(view, &background_color); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - // Show the window when Flutter renders. - // Requires the view to be realized so we can start rendering. - g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), - self); - gtk_widget_realize(GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, - gchar*** arguments, - int* exit_status) { - MyApplication* self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GApplication::startup. -static void my_application_startup(GApplication* application) { - // MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application startup. - - G_APPLICATION_CLASS(my_application_parent_class)->startup(application); -} - -// Implements GApplication::shutdown. -static void my_application_shutdown(GApplication* application) { - // MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application shutdown. - - G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = - my_application_local_command_line; - G_APPLICATION_CLASS(klass)->startup = my_application_startup; - G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -static void my_application_init(MyApplication* self) {} - -MyApplication* my_application_new() { - // Set the program name to the application ID, which helps various systems - // like GTK and desktop environments map this running application to its - // corresponding .desktop file. This ensures better integration by allowing - // the application to be recognized beyond its binary name. - g_set_prgname(APPLICATION_ID); - - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, "flags", - G_APPLICATION_NON_UNIQUE, nullptr)); -} diff --git a/linux/runner/my_application.h b/linux/runner/my_application.h deleted file mode 100644 index db16367..0000000 --- a/linux/runner/my_application.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, - my_application, - MY, - APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/run.sh b/run.sh index 8cf88a9..4a7d740 100755 --- a/run.sh +++ b/run.sh @@ -119,11 +119,17 @@ main() { command -v flutter >/dev/null 2>&1 || { err "flutter still not on PATH after install"; exit 1; } cd "${SCRIPT_DIR}" - flutter config --enable-linux-desktop >/dev/null + flutter config --enable-web >/dev/null log "fetching pub packages" flutter pub get + + # The desktop app is a web build served by the local wrapper: Flutter's + # Linux embedder manages only ~20fps at 4K, while the same Dart code in + # Chrome sustains ~144fps (docs/desktop-performance-findings.md). + log "building the web bundle" + flutter build web --release log "launching the app" - exec flutter run -d linux + exec dart run bin/todo_desktop.dart --web-root "${SCRIPT_DIR}/build/web" } main "$@"