You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #70, revisiting its conclusions in light of what has changed since.
Where things stand at the time of writing
Apple platforms: I have a working build of Grafida for macOS, iOS and iPadOS based on Swift + SwiftUI. One codebase for all Apple targets.
Windows and Linux: the current PHP + Boson codebase.
That is two codebases, which is manageable. This issue is about the two questions that follow from it:
Do I want to support Android?
If I do, how do I do it without maintaining a THIRD codebase?
Question 1 is not settled by this issue. The commercial objection in #70 stands: the Android tablet market is overwhelmingly cheap, underpowered devices on outdated Android versions, and the segment capable of running Grafida is the top couple of percent. Nothing about the architecture below changes that. One further data point I have is that the screen size and aspect ratio for this does not matter. Having to support Stage Manager on iPadOS made this abundantly clear. It’s not very practical to run this software on a postage-stamp display, but it’s possible. This issue answers question 2, so that question 1 can be decided on its own merits rather than being foreclosed by "it's impossible anyway".
What has changed since #70 is that the Swift build exists and works. #70 concluded that a cross-platform mobile Grafida was dead — correct for the constraints at the time, when the alternative was one framework spanning iOS and Android. That is no longer the shape of the problem. Apple is handled. The question now is narrower: can Android join Windows and Linux in a single codebase, keeping the total at two?
The constraint that decides everything
The editor has to be a web engine.
Grafida's value is that it produces Joomla-shaped HTML — <hr id="system-readmore">, accessiblemedia records, #joomlaImage:// fragments, table and figure markup that has to survive a round trip byte-for-byte. tests/corpus/ exists precisely to pin that; it is currently 38 cases.
No native rich-text widget on any platform edits HTML. They all edit their own document model and serialise lossily. Putting article HTML through one of those is a data-loss machine and the corpus would go red on day one.
So the WYSIWYG surface must be HugeRTE (or equivalent) in a web engine. And once a web engine is on screen for the single most important part of the app, a native UI toolkit stops being an asset and becomes a bridge to maintain: every getContent(), image insert, slash-tool trigger, dark-mode skin swap and source-view toggle becomes a message across a language boundary.
Why Flutter is out (restating #70 so it is on the record here)
The primary reason is empirical and was already established in #70: putting an HTML editor in a Flutter WebView is chronically broken in exactly the context Grafida needs it. Hardware keyboards eat most keypresses and hardware arrow keys do not work. Grafida assumes a hardware keyboard — that is the whole premise of authoring on a tablet — so this is not a rough edge, it is a disqualification.
The architectural analysis arrives at the same place from the other direction:
There is no native rich HTML editor in Flutter (flutter_quill edits Deltas, not HTML), so a WebView is mandatory — which walks straight into the defect above.
Desktop WebView embedding is Flutter's weakest area. webview_flutter does not officially cover Windows or Linux; flutter_inappwebview has Windows via WebView2, and Linux is the perennial gap. Linux is a primary target for me.
It would mean rewriting ~19k lines of PHP in Dart and re-bridging ~17.6k lines of existing SPA behaviour, in order to gain a widget toolkit that is then not used for the most important screen.
The same test disqualifies the other native-UI candidates: Compose Multiplatform (desktop WebView means JCEF/KCEF, i.e. a ~100 MB Chromium download at runtime), Avalonia / .NET (immature WebView story), React Native (no Linux at all).
Proposal: TypeScript + Tauri v2
Replace PHP + Boson with a TypeScript codebase on Tauri v2, covering Windows, Linux and Android from one repository, one CLI and one plugin system. tauri build produces the desktop artifacts; tauri android build produces an AAB for Play.
Engine situation: WebView2 (Chromium) on Windows, WebKitGTK on Linux, Android System WebView (Chromium) on Android.
WebKitGTK is worth a note, because it looks like the weak point and is not:
It is already what the Linux build runs on today, so its quirks are known rather than new.
More importantly, the Swift build puts HugeRTE in a WKWebView. The editor glue has to be WebKit-correct regardless. Choosing a Chromium-only desktop shell would not save that testing — it would add a second engine family on top of it.
Electron + Capacitor was the runner-up and was rejected deliberately. It gives better engine parity (Chromium on all three) and needs no Rust at all, but it costs two shells, two dependency trees and three upgrade treadmills (Electron majors roughly every 8 weeks, plus Capacitor, plus AGP churn), and ~150 MB desktop artifacts. For a side project whose entire driver is less maintenance, that is the wrong trade. If the Rust work below turns out to be unacceptable, this is the fallback.
Also considered and rejected: keeping PHP and bundling it on Android. Boson has no Android target, and while PHP can be cross-compiled for Android and exec'd from nativeLibraryDir, it would mean writing and maintaining the entire Android shell myself — Kotlin activity, WebView, process lifecycle under Doze, custom scheme handling — plus 15–30 MB of PHP binary per ABI. That is inventing what Boson currently provides for free, alone, on the platform I have least experience with.
Open risks to scope before committing
SSE streaming. Tauri's HTTP plugin returns whole responses; the AI transport is streaming-first by design. This needs a custom Rust command streaming into a Tauri Channel. Biggest unknown — scope it first. The upside is real: routing AI traffic through Rust permanently kills the CORS / local-model trap and gets the API key out of the renderer, which was a trade-off I took reluctantly.
Secrets.src/Secret/ currently spawns secret-tool and FFIs into crypt32.dll; neither works on Android. Either find plugin coverage for Keystore + DPAPI + libsecret behind one interface, or accept tauri-plugin-stronghold's portable encrypted vault — which is a different security model to an OS keychain and should be chosen deliberately, not by default.
Spell checking in Android WebView.spellcheck="true" behaves less predictably there than on desktop. The current design leans on the per-element attribute as the authoritative gate (see Spell checking #24, Spellchecking Windows #26). Needs a spike.
Willingness to write a few hundred lines of Rust. This is the condition on the whole recommendation. If the answer is no, take Electron + Capacitor instead.
Licensing: an imposed constraint, not a preference
To be explicit, because this will otherwise be misread: I have no ideological objection to the GPL. Grafida is GPLv3 and I would be perfectly content to leave it there.
The constraint comes from distribution through the app stores — the Play Store in the Android case, and the App Store as already noted in #70. GPL-licensed dependencies are untenable under those distribution terms. That is what forces the relicensing, not taste.
Consequences:
TinyMCE → HugeRTE (MIT). TinyMCE is GPL from v7 onwards; HugeRTE is the MIT fork of v6.
The joomla/* dependencies have to go. I audited composer.lock: the GPL surface is exactly five packages, all joomla/* — database, di, language, plus event and string which come in transitively through them. Replacing the three direct ones removes all five.
Everything else is already permissive. Notably the entire Boson stack (17 packages) is MIT, including saucer. Boson was never a licensing blocker.
One footnote for the attribution page: nette/schema and nette/utils (transitive via league/commonmark) are tri-licensed BSD-3-Clause OR GPL-2.0-only OR GPL-3.0-only. I elect BSD-3. Listing them as "GPL" would misstate my own compliance position.
On a TypeScript greenfield the dependency question largely evaporates — everything pulled in has to be permissive from the start. But it does not police itself:
npm is not automatically permissive. AGPL and GPL packages exist there and the transitive tree gets large fast. license-checker with an allowlist in CI from day one.
Tauri means inheriting a Rust crate tree as well — mostly MIT/Apache-2.0, with occasional MPL-2.0 and copyleft. cargo-deny with an allowlist in deny.toml.
The About page should be generated from the dependency tree at build time, not hand-curated. MIT and BSD-3 both require the copyright notice be reproduced in distributions, so that page is the compliance mechanism for nearly every dependency, not just a courtesy. It also covers Font Awesome Free (CC-BY-4.0 icons, MIT code, SIL OFL font) automatically, and cannot drift out of date.
One clarification on \Dom\HTMLDocument: it is not a licensing problem. It is PHP core (ext/dom, PHP License 3.01) and the HTML5 tree builder behind it is Lexbor (Apache-2.0). It is also functionally the best option available on PHP. It only gets replaced because it is a PHP core class and cannot travel if the language does — parse5 (MIT) is the TypeScript equivalent with the same html5lib conformance. If the PHP codebase were staying, \Dom\HTMLDocument would stay with it.
UI adaptation
Already worked out for the Apple build, and it reduces to a handful of primitives:
Sidebars become modals below a screen-width threshold.
HugeRTE toolbar icons raised to 44px for touch, toolbar set to a single scrolling ribbon by default with an option to scroll it. This also helps touchscreen Windows and Linux machines.
HugeRTE menu bar hidden by default; menu bar and submenu heights set to 44px for touch.
Clean-room reimplementation of the slash tools.
Documentation changes explaining how things differ on mobile — in particular that local AI inference requires a third-party app providing that service.
Scope reality
The headline numbers are ~19k lines of PHP plus ~17.6k lines of my own SPA JavaScript, but the real port is smaller:
src/Http/ (~4k lines) largely evaporates. The 83-route internal API exists only because PHP and the webview are separate processes. A single-process TypeScript app turns most of it into direct calls.
The SPA ports rather than being rewritten — existing JavaScript becomes TypeScript with types added.
The genuinely new code is the PHP business logic: Publish, Reference, Field, the Joomla client, Media, Html, Article, Site, Ai. Call it 8–9k lines of actual translation.
What makes two codebases manageable — and the proposed first milestone
This matters more than the framework choice.
Extract the HugeRTE integration layer as a standalone, permissively-licensed npm package consumed by BOTH codebases. It is JavaScript inside WKWebView in the Swift app and JavaScript inside the webview in the Tauri app. It is the fiddliest, most regression-prone code in the project: toolbar config, the 44px touch sizing, the clean-room slash tools, csstheme, codeformats, localmedia, the CodeMirror source view, read-more marker handling. The slash tools have to be rewritten once anyway.
Build it first and ship it into the Swift app, before the Tauri app exists. That way it delivers value to a codebase that already ships, proves the hardest component on a real platform, and — if the rewrite stalls or question 1 is answered "no" — nothing was wasted. A twelve-month rewrite that delivers nothing until month twelve is how side projects die.
Then promote the language-neutral assets to a shared data layer both codebases consume. All of it is mine to relicense:
Asset
Size
tests/corpus/
38 round-trip cases
language/*/*.ini
7 languages × ~490 keys
docs/ + _manifest.json
25 pages
src/Ai/resources/*.json
providers, defaults, voices
storage/migrations/*.sql
10 files
And run tests/corpus/ as a conformance suite in both codebases' CI. That is the guarantee that Swift-Grafida and Tauri-Grafida publish byte-identical HTML — worth more than sharing implementation, because it makes divergence visible in CI instead of arriving as a support ticket six months later.
One thing worth doing regardless
Removing the joomla/* dependencies from the current PHP codebase is decoupled from all of the above. It is roughly 23 files, all behind interfaces I already own (src/Storage/ wraps the database entirely; src/Application/Provider/ wraps the container; the language catalogue can be forked from my own AWF code, which I can relicense freely).
Doing it now:
unblocks non-GPL licensing for Windows and Linux independently of Android;
makes any later port easier, because I would be translating my own abstractions rather than Joomla idioms;
costs nothing if Android never happens.
For the database specifically, plain PDO behind the existing src/Storage/ interfaces is probably a better answer than Doctrine DBAL — it takes the dependency count to zero rather than swapping one for another, and it removes the bind()-by-reference, insert()->set() and loadResult()-returns-null traps, all of which are joomla/database quirks that do not exist in PDO.
Status
Nothing here is decided. This issue establishes that if Android happens, it can happen without a third codebase — which is the precondition for taking question 1 seriously at all.
Follow-up to #70, revisiting its conclusions in light of what has changed since.
Where things stand at the time of writing
That is two codebases, which is manageable. This issue is about the two questions that follow from it:
Question 1 is not settled by this issue. The commercial objection in #70 stands: the Android tablet market is overwhelmingly cheap, underpowered devices on outdated Android versions, and the segment capable of running Grafida is the top couple of percent. Nothing about the architecture below changes that. One further data point I have is that the screen size and aspect ratio for this does not matter. Having to support Stage Manager on iPadOS made this abundantly clear. It’s not very practical to run this software on a postage-stamp display, but it’s possible. This issue answers question 2, so that question 1 can be decided on its own merits rather than being foreclosed by "it's impossible anyway".
What has changed since #70 is that the Swift build exists and works. #70 concluded that a cross-platform mobile Grafida was dead — correct for the constraints at the time, when the alternative was one framework spanning iOS and Android. That is no longer the shape of the problem. Apple is handled. The question now is narrower: can Android join Windows and Linux in a single codebase, keeping the total at two?
The constraint that decides everything
The editor has to be a web engine.
Grafida's value is that it produces Joomla-shaped HTML —
<hr id="system-readmore">,accessiblemediarecords,#joomlaImage://fragments, table and figure markup that has to survive a round trip byte-for-byte.tests/corpus/exists precisely to pin that; it is currently 38 cases.No native rich-text widget on any platform edits HTML. They all edit their own document model and serialise lossily. Putting article HTML through one of those is a data-loss machine and the corpus would go red on day one.
So the WYSIWYG surface must be HugeRTE (or equivalent) in a web engine. And once a web engine is on screen for the single most important part of the app, a native UI toolkit stops being an asset and becomes a bridge to maintain: every
getContent(), image insert, slash-tool trigger, dark-mode skin swap and source-view toggle becomes a message across a language boundary.Why Flutter is out (restating #70 so it is on the record here)
The primary reason is empirical and was already established in #70: putting an HTML editor in a Flutter WebView is chronically broken in exactly the context Grafida needs it. Hardware keyboards eat most keypresses and hardware arrow keys do not work. Grafida assumes a hardware keyboard — that is the whole premise of authoring on a tablet — so this is not a rough edge, it is a disqualification.
The architectural analysis arrives at the same place from the other direction:
flutter_quilledits Deltas, not HTML), so a WebView is mandatory — which walks straight into the defect above.webview_flutterdoes not officially cover Windows or Linux;flutter_inappwebviewhas Windows via WebView2, and Linux is the perennial gap. Linux is a primary target for me.The same test disqualifies the other native-UI candidates: Compose Multiplatform (desktop WebView means JCEF/KCEF, i.e. a ~100 MB Chromium download at runtime), Avalonia / .NET (immature WebView story), React Native (no Linux at all).
Proposal: TypeScript + Tauri v2
Replace PHP + Boson with a TypeScript codebase on Tauri v2, covering Windows, Linux and Android from one repository, one CLI and one plugin system.
tauri buildproduces the desktop artifacts;tauri android buildproduces an AAB for Play.Engine situation: WebView2 (Chromium) on Windows, WebKitGTK on Linux, Android System WebView (Chromium) on Android.
WebKitGTK is worth a note, because it looks like the weak point and is not:
Electron + Capacitor was the runner-up and was rejected deliberately. It gives better engine parity (Chromium on all three) and needs no Rust at all, but it costs two shells, two dependency trees and three upgrade treadmills (Electron majors roughly every 8 weeks, plus Capacitor, plus AGP churn), and ~150 MB desktop artifacts. For a side project whose entire driver is less maintenance, that is the wrong trade. If the Rust work below turns out to be unacceptable, this is the fallback.
Also considered and rejected: keeping PHP and bundling it on Android. Boson has no Android target, and while PHP can be cross-compiled for Android and exec'd from
nativeLibraryDir, it would mean writing and maintaining the entire Android shell myself — Kotlin activity, WebView, process lifecycle under Doze, custom scheme handling — plus 15–30 MB of PHP binary per ABI. That is inventing what Boson currently provides for free, alone, on the platform I have least experience with.Open risks to scope before committing
src/Secret/currently spawnssecret-tooland FFIs intocrypt32.dll; neither works on Android. Either find plugin coverage for Keystore + DPAPI + libsecret behind one interface, or accepttauri-plugin-stronghold's portable encrypted vault — which is a different security model to an OS keychain and should be chosen deliberately, not by default.spellcheck="true"behaves less predictably there than on desktop. The current design leans on the per-element attribute as the authoritative gate (see Spell checking #24, Spellchecking Windows #26). Needs a spike.Licensing: an imposed constraint, not a preference
To be explicit, because this will otherwise be misread: I have no ideological objection to the GPL. Grafida is GPLv3 and I would be perfectly content to leave it there.
The constraint comes from distribution through the app stores — the Play Store in the Android case, and the App Store as already noted in #70. GPL-licensed dependencies are untenable under those distribution terms. That is what forces the relicensing, not taste.
Consequences:
joomla/*dependencies have to go. I auditedcomposer.lock: the GPL surface is exactly five packages, alljoomla/*—database,di,language, pluseventandstringwhich come in transitively through them. Replacing the three direct ones removes all five.saucer. Boson was never a licensing blocker.nette/schemaandnette/utils(transitive vialeague/commonmark) are tri-licensed BSD-3-Clause OR GPL-2.0-only OR GPL-3.0-only. I elect BSD-3. Listing them as "GPL" would misstate my own compliance position.On a TypeScript greenfield the dependency question largely evaporates — everything pulled in has to be permissive from the start. But it does not police itself:
license-checkerwith an allowlist in CI from day one.cargo-denywith an allowlist indeny.toml.One clarification on
\Dom\HTMLDocument: it is not a licensing problem. It is PHP core (ext/dom, PHP License 3.01) and the HTML5 tree builder behind it is Lexbor (Apache-2.0). It is also functionally the best option available on PHP. It only gets replaced because it is a PHP core class and cannot travel if the language does — parse5 (MIT) is the TypeScript equivalent with the same html5lib conformance. If the PHP codebase were staying,\Dom\HTMLDocumentwould stay with it.UI adaptation
Already worked out for the Apple build, and it reduces to a handful of primitives:
Scope reality
The headline numbers are ~19k lines of PHP plus ~17.6k lines of my own SPA JavaScript, but the real port is smaller:
src/Http/(~4k lines) largely evaporates. The 83-route internal API exists only because PHP and the webview are separate processes. A single-process TypeScript app turns most of it into direct calls.What makes two codebases manageable — and the proposed first milestone
This matters more than the framework choice.
Extract the HugeRTE integration layer as a standalone, permissively-licensed npm package consumed by BOTH codebases. It is JavaScript inside WKWebView in the Swift app and JavaScript inside the webview in the Tauri app. It is the fiddliest, most regression-prone code in the project: toolbar config, the 44px touch sizing, the clean-room slash tools,
csstheme,codeformats,localmedia, the CodeMirror source view, read-more marker handling. The slash tools have to be rewritten once anyway.Build it first and ship it into the Swift app, before the Tauri app exists. That way it delivers value to a codebase that already ships, proves the hardest component on a real platform, and — if the rewrite stalls or question 1 is answered "no" — nothing was wasted. A twelve-month rewrite that delivers nothing until month twelve is how side projects die.
Then promote the language-neutral assets to a shared data layer both codebases consume. All of it is mine to relicense:
tests/corpus/language/*/*.inidocs/+_manifest.jsonsrc/Ai/resources/*.jsonstorage/migrations/*.sqlAnd run
tests/corpus/as a conformance suite in both codebases' CI. That is the guarantee that Swift-Grafida and Tauri-Grafida publish byte-identical HTML — worth more than sharing implementation, because it makes divergence visible in CI instead of arriving as a support ticket six months later.One thing worth doing regardless
Removing the
joomla/*dependencies from the current PHP codebase is decoupled from all of the above. It is roughly 23 files, all behind interfaces I already own (src/Storage/wraps the database entirely;src/Application/Provider/wraps the container; the language catalogue can be forked from my own AWF code, which I can relicense freely).Doing it now:
For the database specifically, plain PDO behind the existing
src/Storage/interfaces is probably a better answer than Doctrine DBAL — it takes the dependency count to zero rather than swapping one for another, and it removes thebind()-by-reference,insert()->set()andloadResult()-returns-null traps, all of which arejoomla/databasequirks that do not exist in PDO.Status
Nothing here is decided. This issue establishes that if Android happens, it can happen without a third codebase — which is the precondition for taking question 1 seriously at all.