Add runtime environment detection
This commit is contained in:
82
packages/ui/src/lib/runtime-env.ts
Normal file
82
packages/ui/src/lib/runtime-env.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
export type HostRuntime = "electron" | "tauri" | "web"
|
||||||
|
export type PlatformKind = "desktop" | "mobile"
|
||||||
|
|
||||||
|
export interface RuntimeEnvironment {
|
||||||
|
host: HostRuntime
|
||||||
|
platform: PlatformKind
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electronAPI?: unknown
|
||||||
|
__TAURI__?: {
|
||||||
|
invoke?: <T = unknown>(cmd: string, args?: Record<string, unknown>) => Promise<T>
|
||||||
|
event?: {
|
||||||
|
listen: (event: string, handler: (payload: { payload: unknown }) => void) => Promise<() => void>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectHost(): HostRuntime {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return "web"
|
||||||
|
}
|
||||||
|
|
||||||
|
const win = window as Window & { electronAPI?: unknown }
|
||||||
|
if (typeof win.electronAPI !== "undefined") {
|
||||||
|
return "electron"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof win.__TAURI__ !== "undefined") {
|
||||||
|
return "tauri"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof navigator !== "undefined" && /tauri/i.test(navigator.userAgent)) {
|
||||||
|
return "tauri"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "web"
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectPlatform(): PlatformKind {
|
||||||
|
if (typeof navigator === "undefined") {
|
||||||
|
return "desktop"
|
||||||
|
}
|
||||||
|
|
||||||
|
const uaData = (navigator as any).userAgentData
|
||||||
|
if (uaData?.mobile) {
|
||||||
|
return "mobile"
|
||||||
|
}
|
||||||
|
|
||||||
|
const ua = navigator.userAgent.toLowerCase()
|
||||||
|
if (/android|iphone|ipad|ipod|blackberry|mini|windows phone|mobile|silk/.test(ua)) {
|
||||||
|
return "mobile"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "desktop"
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedEnv: RuntimeEnvironment | null = null
|
||||||
|
|
||||||
|
export function detectRuntimeEnvironment(): RuntimeEnvironment {
|
||||||
|
if (cachedEnv) {
|
||||||
|
return cachedEnv
|
||||||
|
}
|
||||||
|
cachedEnv = {
|
||||||
|
host: detectHost(),
|
||||||
|
platform: detectPlatform(),
|
||||||
|
}
|
||||||
|
if (typeof console !== "undefined") {
|
||||||
|
const message = `[runtime] host=${cachedEnv.host} platform=${cachedEnv.platform}`
|
||||||
|
console.info(message)
|
||||||
|
}
|
||||||
|
return cachedEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
export const runtimeEnv = detectRuntimeEnvironment()
|
||||||
|
|
||||||
|
export const isElectronHost = () => runtimeEnv.host === "electron"
|
||||||
|
export const isTauriHost = () => runtimeEnv.host === "tauri"
|
||||||
|
export const isWebHost = () => runtimeEnv.host === "web"
|
||||||
|
export const isMobilePlatform = () => runtimeEnv.platform === "mobile"
|
||||||
@@ -3,6 +3,7 @@ import App from "./App"
|
|||||||
import { ThemeProvider } from "./lib/theme"
|
import { ThemeProvider } from "./lib/theme"
|
||||||
import { ConfigProvider } from "./stores/preferences"
|
import { ConfigProvider } from "./stores/preferences"
|
||||||
import { InstanceConfigProvider } from "./stores/instance-config"
|
import { InstanceConfigProvider } from "./stores/instance-config"
|
||||||
|
import { runtimeEnv } from "./lib/runtime-env"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
||||||
|
|
||||||
@@ -12,6 +13,11 @@ if (!root) {
|
|||||||
throw new Error("Root element not found")
|
throw new Error("Root element not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof document !== "undefined") {
|
||||||
|
document.documentElement.dataset.runtimeHost = runtimeEnv.host
|
||||||
|
document.documentElement.dataset.runtimePlatform = runtimeEnv.platform
|
||||||
|
}
|
||||||
|
|
||||||
render(
|
render(
|
||||||
() => (
|
() => (
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createSignal, onCleanup, onMount } from "solid-js"
|
import { createSignal, onCleanup, onMount } from "solid-js"
|
||||||
import { render } from "solid-js/web"
|
import { render } from "solid-js/web"
|
||||||
import iconUrl from "../../images/CodeNomad-Icon.png"
|
import iconUrl from "../../images/CodeNomad-Icon.png"
|
||||||
|
import { runtimeEnv, isTauriHost } from "../../lib/runtime-env"
|
||||||
import "../../index.css"
|
import "../../index.css"
|
||||||
import "./loading.css"
|
import "./loading.css"
|
||||||
|
|
||||||
@@ -17,6 +18,12 @@ const phrases = [
|
|||||||
"Persuading the AI to give you keyboard control…",
|
"Persuading the AI to give you keyboard control…",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const hostStatusMap: Record<typeof runtimeEnv.host, string> = {
|
||||||
|
electron: "Starting desktop shell…",
|
||||||
|
tauri: "Starting native shell…",
|
||||||
|
web: "Connecting to CodeNomad…",
|
||||||
|
}
|
||||||
|
|
||||||
interface CliStatus {
|
interface CliStatus {
|
||||||
state?: string
|
state?: string
|
||||||
url?: string | null
|
url?: string | null
|
||||||
@@ -30,12 +37,6 @@ interface TauriBridge {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
__TAURI__?: TauriBridge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickPhrase(previous?: string) {
|
function pickPhrase(previous?: string) {
|
||||||
const filtered = phrases.filter((phrase) => phrase !== previous)
|
const filtered = phrases.filter((phrase) => phrase !== previous)
|
||||||
const source = filtered.length > 0 ? filtered : phrases
|
const source = filtered.length > 0 ? filtered : phrases
|
||||||
@@ -52,26 +53,34 @@ function getTauriBridge(): TauriBridge | null {
|
|||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const bridge = (window as any).__TAURI__ as TauriBridge | undefined
|
const bridge = (window as { __TAURI__?: TauriBridge }).__TAURI__
|
||||||
if (!bridge || !bridge.event || !bridge.invoke) {
|
if (!bridge || !bridge.event || !bridge.invoke) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return bridge
|
return bridge
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function annotateDocument() {
|
||||||
|
if (typeof document === "undefined") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
document.documentElement.dataset.runtimeHost = runtimeEnv.host
|
||||||
|
document.documentElement.dataset.runtimePlatform = runtimeEnv.platform
|
||||||
|
}
|
||||||
|
|
||||||
function LoadingApp() {
|
function LoadingApp() {
|
||||||
const [phrase, setPhrase] = createSignal(pickPhrase())
|
const [phrase, setPhrase] = createSignal(pickPhrase())
|
||||||
const [error, setError] = createSignal<string | null>(null)
|
const [error, setError] = createSignal<string | null>(null)
|
||||||
const [status, setStatus] = createSignal<string>("Starting services…")
|
const [status, setStatus] = createSignal<string>(hostStatusMap[runtimeEnv.host] ?? "Starting services…")
|
||||||
|
|
||||||
const changePhrase = () => setPhrase(pickPhrase(phrase()))
|
const changePhrase = () => setPhrase(pickPhrase(phrase()))
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
annotateDocument()
|
||||||
setPhrase(pickPhrase())
|
setPhrase(pickPhrase())
|
||||||
const tauriBridge = getTauriBridge()
|
|
||||||
const unsubscribers: Array<() => void> = []
|
const unsubscribers: Array<() => void> = []
|
||||||
|
|
||||||
async function bootstrapTauri() {
|
async function bootstrapTauri(tauriBridge: TauriBridge | null) {
|
||||||
if (!tauriBridge || !tauriBridge.event || !tauriBridge.invoke) {
|
if (!tauriBridge || !tauriBridge.event || !tauriBridge.invoke) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -115,7 +124,9 @@ function LoadingApp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void bootstrapTauri()
|
if (isTauriHost()) {
|
||||||
|
void bootstrapTauri(getTauriBridge())
|
||||||
|
}
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
unsubscribers.forEach((unsubscribe) => {
|
unsubscribers.forEach((unsubscribe) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user