@sigx/lynx-dev-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/LICENSE +21 -0
  2. package/android/src/main/kotlin/com/sigx/devclient/DevGenericResourceFetcher.kt +73 -0
  3. package/android/src/main/kotlin/com/sigx/devclient/DevHomeScreen.kt +163 -0
  4. package/android/src/main/kotlin/com/sigx/devclient/DevLynxScreen.kt +208 -0
  5. package/android/src/main/kotlin/com/sigx/devclient/DevMenu.kt +278 -0
  6. package/android/src/main/kotlin/com/sigx/devclient/DevQRScanner.kt +174 -0
  7. package/android/src/main/kotlin/com/sigx/devclient/DevSettings.kt +61 -0
  8. package/android/src/main/kotlin/com/sigx/devclient/DevTemplateProvider.kt +76 -0
  9. package/android/src/main/kotlin/com/sigx/devclient/DevTemplateResourceFetcher.kt +113 -0
  10. package/android/src/main/kotlin/com/sigx/devclient/ErrorOverlay.kt +86 -0
  11. package/android/src/main/kotlin/com/sigx/devclient/PerfHud.kt +100 -0
  12. package/android/src/main/kotlin/com/sigx/devclient/ShakeDetector.kt +97 -0
  13. package/android/src/main/kotlin/com/sigx/devclient/SigxDevClient.kt +91 -0
  14. package/dist/index.d.ts +11 -0
  15. package/dist/index.js +11 -0
  16. package/ios/Sources/SigxDevClient/DevGenericResourceFetcher.swift +47 -0
  17. package/ios/Sources/SigxDevClient/DevHomeScreen.swift +128 -0
  18. package/ios/Sources/SigxDevClient/DevMenuView.swift +133 -0
  19. package/ios/Sources/SigxDevClient/DevQRScanner.swift +165 -0
  20. package/ios/Sources/SigxDevClient/DevTemplateProvider.swift +68 -0
  21. package/ios/Sources/SigxDevClient/DevTemplateResourceFetcher.swift +73 -0
  22. package/ios/Sources/SigxDevClient/ShakeDetector.swift +60 -0
  23. package/ios/Sources/SigxDevClient/SigxDevClient.swift +106 -0
  24. package/package.json +30 -0
@@ -0,0 +1,86 @@
1
+ package com.sigx.devclient
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.layout.*
5
+ import androidx.compose.foundation.rememberScrollState
6
+ import androidx.compose.foundation.verticalScroll
7
+ import androidx.compose.material3.Button
8
+ import androidx.compose.material3.ButtonDefaults
9
+ import androidx.compose.material3.Text
10
+ import androidx.compose.runtime.Composable
11
+ import androidx.compose.ui.Modifier
12
+ import androidx.compose.ui.graphics.Color
13
+ import androidx.compose.ui.text.font.FontFamily
14
+ import androidx.compose.ui.text.font.FontWeight
15
+ import androidx.compose.ui.unit.dp
16
+ import androidx.compose.ui.unit.sp
17
+
18
+ /**
19
+ * React Native-style red screen error overlay.
20
+ * Shows error message and stack trace with dismiss/reload actions.
21
+ */
22
+ @Composable
23
+ fun ErrorOverlay(
24
+ error: String?,
25
+ onDismiss: () -> Unit,
26
+ onReload: () -> Unit
27
+ ) {
28
+ if (error == null) return
29
+
30
+ Box(
31
+ modifier = Modifier
32
+ .fillMaxSize()
33
+ .background(Color(0xFFCC0000))
34
+ ) {
35
+ Column(
36
+ modifier = Modifier
37
+ .fillMaxSize()
38
+ .padding(24.dp)
39
+ .verticalScroll(rememberScrollState())
40
+ ) {
41
+ Text(
42
+ "Error",
43
+ color = Color.White,
44
+ fontSize = 24.sp,
45
+ fontWeight = FontWeight.Bold
46
+ )
47
+ Spacer(modifier = Modifier.height(16.dp))
48
+ Text(
49
+ error,
50
+ color = Color(0xFFFFCCCC),
51
+ fontSize = 14.sp,
52
+ fontFamily = FontFamily.Monospace,
53
+ lineHeight = 20.sp
54
+ )
55
+ Spacer(modifier = Modifier.height(24.dp))
56
+ Row(
57
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
58
+ ) {
59
+ Button(
60
+ onClick = onReload,
61
+ colors = ButtonDefaults.buttonColors(
62
+ containerColor = Color.White,
63
+ contentColor = Color(0xFFCC0000)
64
+ )
65
+ ) {
66
+ Text("Reload", fontWeight = FontWeight.Bold)
67
+ }
68
+ Button(
69
+ onClick = onDismiss,
70
+ colors = ButtonDefaults.buttonColors(
71
+ containerColor = Color(0x33FFFFFF),
72
+ contentColor = Color.White
73
+ )
74
+ ) {
75
+ Text("Dismiss")
76
+ }
77
+ }
78
+ Spacer(modifier = Modifier.height(24.dp))
79
+ Text(
80
+ "sigx dev client -- shake device or press Menu to open dev tools",
81
+ color = Color(0x99FFFFFF),
82
+ fontSize = 11.sp
83
+ )
84
+ }
85
+ }
86
+ }
@@ -0,0 +1,100 @@
1
+ package com.sigx.devclient
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.layout.*
5
+ import androidx.compose.foundation.shape.RoundedCornerShape
6
+ import androidx.compose.material3.Text
7
+ import androidx.compose.runtime.*
8
+ import androidx.compose.ui.Modifier
9
+ import androidx.compose.ui.graphics.Color
10
+ import androidx.compose.ui.text.font.FontFamily
11
+ import androidx.compose.ui.unit.dp
12
+ import androidx.compose.ui.unit.sp
13
+ import com.lynx.tasm.LynxPerfMetric
14
+ import com.lynx.tasm.LynxView
15
+ import kotlinx.coroutines.delay
16
+
17
+ /**
18
+ * Translucent performance overlay showing key Lynx rendering metrics.
19
+ * Uses LynxView.forceGetPerf() to read timing data.
20
+ */
21
+ @Composable
22
+ fun PerfHud(
23
+ visible: Boolean,
24
+ lynxView: LynxView?,
25
+ modifier: Modifier = Modifier
26
+ ) {
27
+ if (!visible || lynxView == null) return
28
+
29
+ var metrics by remember { mutableStateOf<LynxPerfMetric?>(null) }
30
+
31
+ LaunchedEffect(lynxView) {
32
+ while (true) {
33
+ try {
34
+ metrics = lynxView.forceGetPerf()
35
+ } catch (_: Exception) {}
36
+ delay(2000)
37
+ }
38
+ }
39
+
40
+ Box(
41
+ modifier = modifier
42
+ .padding(8.dp)
43
+ .background(Color(0xCC1E1E1E), RoundedCornerShape(8.dp))
44
+ .padding(10.dp)
45
+ ) {
46
+ Column {
47
+ Text(
48
+ "sigx perf",
49
+ color = Color(0xFF7C3AED),
50
+ fontSize = 11.sp,
51
+ fontFamily = FontFamily.Monospace
52
+ )
53
+ Spacer(modifier = Modifier.height(4.dp))
54
+
55
+ metrics?.let { m ->
56
+ PerfLine("TTI", m.tti)
57
+ PerfLine("Layout", m.layout)
58
+ PerfLine("JS Core", m.jsFinishLoadCore)
59
+ PerfLine("JS App", m.jsFinishLoadApp)
60
+ PerfLine("TASM Decode", m.tasmBinaryDecode)
61
+ PerfLine("Render Page", m.renderPage)
62
+ PerfLine("Diff Root", m.diffRootCreate)
63
+ if (m.isHasActualFMP) {
64
+ PerfLine("FMP", m.actualFMPDuration)
65
+ }
66
+ } ?: Text(
67
+ "Waiting for metrics...",
68
+ color = Color(0xFF94A3B8),
69
+ fontSize = 10.sp,
70
+ fontFamily = FontFamily.Monospace
71
+ )
72
+ }
73
+ }
74
+ }
75
+
76
+ @Composable
77
+ private fun PerfLine(label: String, value: Double) {
78
+ if (value <= 0) return
79
+ Row(
80
+ modifier = Modifier.fillMaxWidth(),
81
+ horizontalArrangement = Arrangement.SpaceBetween
82
+ ) {
83
+ Text(
84
+ label,
85
+ color = Color(0xFF94A3B8),
86
+ fontSize = 10.sp,
87
+ fontFamily = FontFamily.Monospace
88
+ )
89
+ Text(
90
+ "%.1fms".format(value),
91
+ color = when {
92
+ value < 100 -> Color(0xFF22C55E)
93
+ value < 300 -> Color(0xFFEAB308)
94
+ else -> Color(0xFFEF4444)
95
+ },
96
+ fontSize = 10.sp,
97
+ fontFamily = FontFamily.Monospace
98
+ )
99
+ }
100
+ }
@@ -0,0 +1,97 @@
1
+ package com.sigx.devclient
2
+
3
+ import android.content.Context
4
+ import android.hardware.Sensor
5
+ import android.hardware.SensorEvent
6
+ import android.hardware.SensorEventListener
7
+ import android.hardware.SensorManager
8
+ import kotlin.math.sqrt
9
+
10
+ /**
11
+ * Detects device shakes using the accelerometer.
12
+ * Ported from React Native's ShakeDetector
13
+ * (ReactAndroid/.../common/ShakeDetector.java) — requires sustained
14
+ * high-magnitude motion within a short window, not a single spike,
15
+ * so casual handling does not trigger it.
16
+ */
17
+ class ShakeDetector(
18
+ private val context: Context,
19
+ private val onShake: () -> Unit
20
+ ) : SensorEventListener {
21
+
22
+ private var sensorManager: SensorManager? = null
23
+
24
+ private val timestamps = LongArray(MAX_SAMPLES)
25
+ private val magnitudes = FloatArray(MAX_SAMPLES)
26
+ private var currentIndex = 0
27
+ private var numShakes = 0
28
+ private var lastTimestamp = 0L
29
+
30
+ companion object {
31
+ private const val MAX_SAMPLES = 25
32
+ private const val MIN_TIME_BETWEEN_SAMPLES_NS = 20_000_000L // 20 ms
33
+ private const val VISIBLE_TIME_RANGE_NS = 500_000_000L // 500 ms
34
+ private const val ACCELERATION_THRESHOLD = 15f // m/s² (~1.5× gravity)
35
+ private const val MIN_NUM_SHAKES = 1
36
+ }
37
+
38
+ fun start() {
39
+ sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager
40
+ sensorManager?.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)?.let { sensor ->
41
+ sensorManager?.registerListener(this, sensor, SensorManager.SENSOR_DELAY_GAME)
42
+ }
43
+ reset()
44
+ }
45
+
46
+ fun stop() {
47
+ sensorManager?.unregisterListener(this)
48
+ sensorManager = null
49
+ reset()
50
+ }
51
+
52
+ override fun onSensorChanged(event: SensorEvent?) {
53
+ val e = event ?: return
54
+ if (e.timestamp - lastTimestamp < MIN_TIME_BETWEEN_SAMPLES_NS) return
55
+ lastTimestamp = e.timestamp
56
+
57
+ val ax = e.values[0]
58
+ val ay = e.values[1]
59
+ val az = e.values[2]
60
+
61
+ timestamps[currentIndex] = e.timestamp
62
+ magnitudes[currentIndex] = sqrt(ax * ax + ay * ay + az * az)
63
+
64
+ maybeDispatchShake(e.timestamp)
65
+ currentIndex = (currentIndex + 1) % MAX_SAMPLES
66
+ }
67
+
68
+ private fun maybeDispatchShake(now: Long) {
69
+ if (numShakes >= 8 * MIN_NUM_SHAKES) {
70
+ reset()
71
+ onShake()
72
+ return
73
+ }
74
+
75
+ var count = 0
76
+ for (i in 0 until MAX_SAMPLES) {
77
+ val idx = (currentIndex - i + MAX_SAMPLES) % MAX_SAMPLES
78
+ if (now - timestamps[idx] > VISIBLE_TIME_RANGE_NS) break
79
+ if (magnitudes[idx] > ACCELERATION_THRESHOLD) {
80
+ count++
81
+ }
82
+ }
83
+ numShakes = count
84
+ }
85
+
86
+ private fun reset() {
87
+ numShakes = 0
88
+ lastTimestamp = 0L
89
+ currentIndex = 0
90
+ for (i in 0 until MAX_SAMPLES) {
91
+ timestamps[i] = 0L
92
+ magnitudes[i] = 0f
93
+ }
94
+ }
95
+
96
+ override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
97
+ }
@@ -0,0 +1,91 @@
1
+ package com.sigx.devclient
2
+
3
+ import android.app.Application
4
+ import android.content.Context
5
+ import android.util.Log
6
+ import com.lynx.tasm.LynxBooleanOption
7
+ import com.lynx.tasm.LynxEnv
8
+ import com.lynx.tasm.LynxViewBuilder
9
+ import com.lynx.tasm.service.LynxServiceCenter
10
+
11
+ /**
12
+ * Facade API for sigx-lynx dev client.
13
+ *
14
+ * Templates call these methods in order:
15
+ * 1. `SigxDevClient.registerServices()` -- before LynxEnv.init(), registers devtool service
16
+ * 2. `SigxDevClient.enableDevMode()` -- after LynxEnv.init(), enables debug/devtool/logbox
17
+ * 3. `SigxDevClient.configureForDev(builder, context)` -- in LynxView setup for dev mode
18
+ */
19
+ object SigxDevClient {
20
+
21
+ private const val TAG = "SigxDevClient"
22
+
23
+ /**
24
+ * Register LynxDevToolService with the service center.
25
+ * Call BEFORE LynxEnv.inst().init() inside a BuildConfig.DEBUG check.
26
+ */
27
+ fun registerServices() {
28
+ try {
29
+ val devToolServiceClass = Class.forName("com.lynx.service.devtool.LynxDevToolService")
30
+
31
+ // LynxDevToolService.INSTANCE is a Kotlin lazy companion property,
32
+ // accessed via the static getINSTANCE() method (not a field).
33
+ val getInstanceMethod = devToolServiceClass.getMethod("getINSTANCE")
34
+ val instance = getInstanceMethod.invoke(null)
35
+ ?: throw IllegalStateException("LynxDevToolService.getINSTANCE() returned null")
36
+
37
+ // Register with LynxServiceCenter
38
+ val center = LynxServiceCenter.inst()
39
+ val registerMethod = center.javaClass.methods.firstOrNull {
40
+ it.name == "registerService" &&
41
+ it.parameterTypes.size == 1 &&
42
+ it.parameterTypes[0].isAssignableFrom(instance.javaClass)
43
+ } ?: center.javaClass.methods.first { it.name == "registerService" }
44
+
45
+ registerMethod.invoke(center, instance)
46
+ Log.i(TAG, "Registered LynxDevToolService")
47
+
48
+ // Set preset values via the concrete class methods
49
+ devToolServiceClass.getMethod("setLynxDebugPresetValue", Boolean::class.java)
50
+ .invoke(instance, true)
51
+ devToolServiceClass.getMethod("setLogBoxPresetValue", Boolean::class.java)
52
+ .invoke(instance, true)
53
+ Log.i(TAG, "Set debug preset values")
54
+ } catch (e: Exception) {
55
+ Log.e(TAG, "Failed to register devtool service: ${e.message}", e)
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Enable debug features on LynxEnv.
61
+ * MUST be called AFTER LynxEnv.inst().init() -- calling before init() has
62
+ * no effect because init() resets these flags.
63
+ */
64
+ fun enableDevMode() {
65
+ LynxEnv.inst().enableLynxDebug(true)
66
+ LynxEnv.inst().enableDevtool(true)
67
+ LynxEnv.inst().enableLogBox(true)
68
+ Log.i(TAG, "Enabled devtool, debug, and logbox on LynxEnv (after init)")
69
+ }
70
+
71
+ /**
72
+ * Convenience method that calls registerServices() + enableDevMode().
73
+ * Only use this if you've already called LynxEnv.inst().init().
74
+ */
75
+ fun init(app: Application) {
76
+ registerServices()
77
+ enableDevMode()
78
+ }
79
+
80
+ /**
81
+ * Configure a LynxViewBuilder for dev mode (HTTP resource fetching + HMR).
82
+ * Call when launching with a dev server URL.
83
+ */
84
+ fun configureForDev(builder: LynxViewBuilder, context: Context) {
85
+ builder.setTemplateProvider(DevTemplateProvider(context))
86
+ builder.setTemplateResourceFetcher(DevTemplateResourceFetcher(context))
87
+ builder.setEnableGenericResourceFetcher(LynxBooleanOption.TRUE)
88
+ builder.setGenericResourceFetcher(DevGenericResourceFetcher())
89
+ Log.i(TAG, "Configured LynxViewBuilder for dev mode (template provider + resource fetchers)")
90
+ }
91
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @sigx/lynx-dev-client
3
+ *
4
+ * Dev client for sigx-lynx apps. Contains native resource fetchers,
5
+ * template provider, and devtool integration that enable HMR and
6
+ * dev tools in debug builds.
7
+ *
8
+ * The JS side is a no-op — all functionality is in the native code
9
+ * that gets auto-linked during prebuild.
10
+ */
11
+ export declare const DEV_CLIENT_VERSION = "0.1.0";
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @sigx/lynx-dev-client
3
+ *
4
+ * Dev client for sigx-lynx apps. Contains native resource fetchers,
5
+ * template provider, and devtool integration that enable HMR and
6
+ * dev tools in debug builds.
7
+ *
8
+ * The JS side is a no-op — all functionality is in the native code
9
+ * that gets auto-linked during prebuild.
10
+ */
11
+ export const DEV_CLIENT_VERSION = '0.1.0';
@@ -0,0 +1,47 @@
1
+ import Foundation
2
+ import Lynx
3
+
4
+ /// Handles generic resource fetching (JS chunks, CSS, JSON) for LynxView.
5
+ /// Required for HMR hot-update JSON/JS fetches during development.
6
+ class DevGenericResourceFetcher: NSObject, LynxGenericResourceFetcher {
7
+
8
+ private let session: URLSession = {
9
+ let config = URLSessionConfiguration.default
10
+ config.timeoutIntervalForRequest = 30
11
+ config.timeoutIntervalForResource = 60
12
+ return URLSession(configuration: config)
13
+ }()
14
+
15
+ func fetchResource(
16
+ _ request: LynxResourceRequest,
17
+ onComplete callback: @escaping LynxGenericResourceCompletionBlock
18
+ ) -> (() -> Void)! {
19
+ guard let url = URL(string: request.url ?? "") else {
20
+ callback(nil, NSError(domain: "com.sigx.devclient", code: 400,
21
+ userInfo: [NSLocalizedDescriptionKey: "Invalid URL: \(request.url ?? "")"]))
22
+ return {}
23
+ }
24
+
25
+ let task = session.dataTask(with: url) { data, response, error in
26
+ if let error = error {
27
+ callback(nil, error)
28
+ return
29
+ }
30
+ callback(data, nil)
31
+ }
32
+ task.resume()
33
+
34
+ // Return cancellation block
35
+ return { task.cancel() }
36
+ }
37
+
38
+ func fetchResourcePath(
39
+ _ request: LynxResourceRequest,
40
+ onComplete callback: @escaping LynxGenericResourcePathCompletionBlock
41
+ ) -> (() -> Void)! {
42
+ // Not used for dev server fetching — return error
43
+ callback(nil, NSError(domain: "com.sigx.devclient", code: 501,
44
+ userInfo: [NSLocalizedDescriptionKey: "fetchResourcePath not implemented"]))
45
+ return {}
46
+ }
47
+ }
@@ -0,0 +1,128 @@
1
+ import SwiftUI
2
+
3
+ /// Dev-mode landing screen for sigx-lynx apps that ship without a bundled
4
+ /// `main.lynx.bundle`. Lets the user enter a dev-server URL by hand, scan a
5
+ /// QR code, or pick from recent URLs. The app template renders this when it
6
+ /// has no `--sigx_dev_url` launch arg AND no bundle in `Bundle.main`.
7
+ ///
8
+ /// Wraps itself in a `NavigationStack` so it owns the title bar; consumer just
9
+ /// drops `DevHomeScreen { url in /* navigate to LynxView */ }` at the root.
10
+ public struct DevHomeScreen: View {
11
+ let onSelectUrl: (String) -> Void
12
+
13
+ @State private var urlText = ""
14
+ @State private var recentUrls: [String] = []
15
+ @State private var showQRScanner = false
16
+
17
+ public init(onSelectUrl: @escaping (String) -> Void) {
18
+ self.onSelectUrl = onSelectUrl
19
+ }
20
+
21
+ public var body: some View {
22
+ // NavigationView (iOS 13+) rather than NavigationStack (iOS 16+) so
23
+ // the dev client works on apps with deployment target 15.0.
24
+ NavigationView {
25
+ VStack(spacing: 0) {
26
+ VStack(spacing: 12) {
27
+ HStack {
28
+ TextField("Dev server URL", text: $urlText)
29
+ .textFieldStyle(.roundedBorder)
30
+ .keyboardType(.URL)
31
+ .autocapitalization(.none)
32
+ .disableAutocorrection(true)
33
+ .submitLabel(.go)
34
+ .onSubmit { connect() }
35
+
36
+ Button(action: { showQRScanner = true }) {
37
+ Image(systemName: "qrcode.viewfinder")
38
+ .font(.title2)
39
+ }
40
+ }
41
+
42
+ Button(action: connect) {
43
+ Text("Connect")
44
+ .frame(maxWidth: .infinity)
45
+ }
46
+ .buttonStyle(.borderedProminent)
47
+ .disabled(urlText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
48
+ }
49
+ .padding()
50
+
51
+ Divider()
52
+
53
+ if recentUrls.isEmpty {
54
+ Spacer()
55
+ VStack(spacing: 8) {
56
+ Image(systemName: "antenna.radiowaves.left.and.right")
57
+ .font(.largeTitle)
58
+ .foregroundColor(.secondary)
59
+ Text("Enter a dev server URL or scan a QR code")
60
+ .foregroundColor(.secondary)
61
+ .multilineTextAlignment(.center)
62
+ }
63
+ .padding()
64
+ Spacer()
65
+ } else {
66
+ HStack {
67
+ Text("Recent")
68
+ .font(.headline)
69
+ Spacer()
70
+ Button("Clear") {
71
+ SigxDevClient.clearRecentUrls()
72
+ recentUrls = []
73
+ }
74
+ .font(.subheadline)
75
+ }
76
+ .padding(.horizontal)
77
+ .padding(.top, 12)
78
+
79
+ List {
80
+ ForEach(recentUrls, id: \.self) { url in
81
+ Button(action: {
82
+ urlText = url
83
+ connectTo(url)
84
+ }) {
85
+ Text(url)
86
+ .lineLimit(1)
87
+ .foregroundColor(.primary)
88
+ }
89
+ }
90
+ .onDelete { indexSet in
91
+ for index in indexSet {
92
+ SigxDevClient.removeRecentUrl(recentUrls[index])
93
+ }
94
+ recentUrls = SigxDevClient.recentUrls
95
+ }
96
+ }
97
+ .listStyle(.plain)
98
+ }
99
+ }
100
+ .navigationTitle("sigx-lynx dev")
101
+ .navigationBarTitleDisplayMode(.inline)
102
+ .sheet(isPresented: $showQRScanner) {
103
+ NavigationView {
104
+ DevQRScanner { code in
105
+ showQRScanner = false
106
+ urlText = code
107
+ connectTo(code)
108
+ }
109
+ }
110
+ }
111
+ .onAppear {
112
+ recentUrls = SigxDevClient.recentUrls
113
+ }
114
+ }
115
+ }
116
+
117
+ private func connect() {
118
+ let trimmed = urlText.trimmingCharacters(in: .whitespacesAndNewlines)
119
+ guard !trimmed.isEmpty else { return }
120
+ connectTo(trimmed)
121
+ }
122
+
123
+ private func connectTo(_ url: String) {
124
+ SigxDevClient.addRecentUrl(url)
125
+ recentUrls = SigxDevClient.recentUrls
126
+ onSelectUrl(url)
127
+ }
128
+ }