Vectrum
Sign In

Merge branch 'android-companion'

# Conflicts:
#	src/remote/mod.rs
#	src/ui/settings/connections.rs
#	src/ui/workspace/remote.rs
JjohannSocommitted 2 parents d6e0439 8465e48commit156ec72

Showing 286 changed files with 23856 additions and 13 deletions.

Merge commit — showing changes against the first parent.

+7−0Cargo.lockView File
@@ -5249,6 +5249,12 @@ dependencies = [
52495249 "bytemuck",
52505250 ]
52515251
5252+ [[package]]
5253+ name = "qrcode"
5254+ version = "0.14.1"
5255+ source = "registry+https://github.com/rust-lang/crates.io-index"
5256+ checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
5257+
52525258 [[package]]
52535259 name = "quick-error"
52545260 version = "2.0.1"
@@ -7482,6 +7488,7 @@ dependencies = [
74827488 "libc",
74837489 "mermaid-rs-renderer",
74847490 "openh264",
7491+ "qrcode",
74857492 "serde",
74867493 "serde_json",
74877494 "smol",
+1−0Cargo.tomlView File
@@ -19,6 +19,7 @@ image = { version = "0.25", default-features = false, features = ["jpeg"] }
1919 alacritty_terminal = "0.26"
2020 openh264 = "0.9"
2121 mermaid-rs-renderer = { version = "0.3.1", default-features = false }
22+ qrcode = { version = "0.14.1", default-features = false }
2223
2324 [profile.dev]
2425 debug = "line-tables-only"
+15−1README.mdView File
@@ -203,12 +203,17 @@ Gruvbox, Rosé Pine, Solarized and Dracula.
203203 side-by-side layout.
204204 - Remote connections, like T3 Code's environments: turn on **Allow remote connections** on
205205 one device, and paste its pairing code (address and key) under Settings → Connections on
206- another. The host's projects and threads appear in the sidebar tagged with its name; open,
206+ another, or scan the QR code shown next to it with a phone. The host's projects and threads appear in the sidebar tagged with its name; open,
207207 continue or start threads there and they run on the host. Their terminal drawer (Ctrl+J)
208208 runs its shells on the host too, in the thread's folder there. The first time a device connects
209209 with settings that differ from yours, a dialog asks whether to give it your settings or
210210 leave its own as they are. The connection is a WebSocket guarded by the key but not
211211 encrypted, so use it on trusted networks or over a VPN.
212+ - An Android companion app ([`android/`](android/README.md)), in Kotlin and Jetpack Compose:
213+ a remote client that looks like the desktop, in the same themes. Pair it by scanning the QR
214+ code under Settings → Connections; the sidebar, threads, composer and its pickers,
215+ approvals and questions, diffs, settings, terminals and the Device panel work from the
216+ phone, on the desktop's agents and files.
212217 - A settings view (Ctrl+, or **Settings** in the sidebar), laid out like T3 Code's: a
213218 search field over the pages (press `/` to search, ↑/↓ and Enter to jump to a setting,
214219 which scrolls into view and pulses), a "Settings / Page" breadcrumb, and every setting as
@@ -355,6 +360,15 @@ version not tagged yet, and a read & write API token in `VECTRUM_TOKEN` or
355360 `VECTRUM_URL` (`https://vectrum.cc`) and `VECTRUM_REPO` (from the origin remote) pick where
356361 it goes.
357362
363+ ### Android
364+
365+ `cd android && ./gradlew :app:assembleDebug` builds the companion app into
366+ `android/app/build/outputs/apk/debug/` (`assembleRelease` for a shrunk build, signed with the
367+ debug key until there's an upload key). It needs JDK 21 and the Android SDK (platform 37) in
368+ `ANDROID_HOME` or `android/local.properties`. The themes come from the desktop's palettes:
369+ after changing them, `cargo test export_android_themes -- --ignored` rewrites
370+ `android/app/src/main/assets/themes.json`.
371+
358372 ## Layout
359373
360374 - `src/claude.rs`: spawns and owns the agent process, `claude`, `pi` or `jcode api --stdio`
+6−0android/.gitignoreaddedView File
@@ -0,0 +1,6 @@
1+ .gradle/
2+ .kotlin/
3+ build/
4+ local.properties
5+ *.iml
6+ .idea/
+61−0android/README.mdaddedView File
@@ -0,0 +1,61 @@
1+ # Vectrum Code for Android
2+
3+ A companion app for [Vectrum Code](../README.md): the desktop's projects and threads on a phone,
4+ in the desktop's look and themes. It's a remote client like another desktop: threads run on
5+ the computer, with its agents, files, terminals and devices.
6+
7+ ## Pairing
8+
9+ On the computer, open Settings → Connections and turn on **Allow remote connections**. Then, on
10+ the phone, scan the QR code shown under **Pairing code**, or paste the code
11+ (`192.168.2.10:47823/<key>`). A `vectrum://…` link pairs too. Several computers can be paired;
12+ their threads share the sidebar, tagged with each one's name. The connection isn't encrypted,
13+ so pair on networks you trust or over a VPN such as Tailscale or WireGuard.
14+
15+ ## What it has
16+
17+ - The sidebar's inbox: pinned and active threads, the Working, Snoozed and Settled shelves,
18+ search, the project filter, and the thread menu on a long press (pin, settle, snooze, wake,
19+ rename, copy, delete) with the desktop's undo notices.
20+ - The transcript: Markdown, tool calls folded into **Worked for** rows, subagents, plans,
21+ questions, the changed-files card and the diff view.
22+ - The composer: model, reasoning and access pickers from the computer's agents, attachments,
23+ the `/` menu, steering and queueing, and the dock for approvals, questions, plans, queued
24+ messages, background tasks, usage limits and the task list.
25+ - New threads on any paired computer's projects, in the current checkout or a new worktree,
26+ and adding projects (a folder there, a new one, or a clone).
27+ - Settings: the computer's environment settings (with project overrides), Appearance with every
28+ theme family, Providers, Source Control, Connections, Archive and About.
29+ - The right panel: subagents, terminals (shells on the computer, in the thread's folder, with
30+ an extra-keys row) and the Device panel (the computer's emulators and simulators, streamed
31+ and controlled from the phone).
32+
33+ Left out because the remote connection doesn't carry them: the browser panel, usage reports,
34+ `@` file mentions, editing providers and starring models.
35+
36+ ## Build
37+
38+ ```sh
39+ ./gradlew :app:assembleDebug # app/build/outputs/apk/debug/app-debug.apk
40+ ./gradlew :app:testDebugUnitTest
41+ ```
42+
43+ JDK 21 and the Android SDK (platform 37) are needed; point `local.properties` at the SDK
44+ (`sdk.dir=…`) or set `ANDROID_HOME`.
45+
46+ ## Layout
47+
48+ - `net/`: the desktop's remote protocol (`Protocol.kt`, mirroring `src/remote/mod.rs`) and the
49+ WebSocket that keeps a host connected (`Connection.kt`).
50+ - `data/`: the app's state as Compose state: paired hosts, their projects, threads and open
51+ transcripts (`AppState.kt`), and this phone's own preferences (`DevicePrefs.kt`).
52+ - `theme/`: MetalUI. `Palette.kt` reads `assets/themes.json`, which the desktop exports
53+ (`cargo test export_android_themes -- --ignored`), so every theme matches it exactly;
54+ `Metal.kt` has the surfaces (raised, well, plate, with the desktop's layered shadows), caps,
55+ switches, LEDs, keycaps and engraved labels.
56+ - `ui/`: `sidebar/`, `chat/` (header, transcript, Markdown), `composer/` (composer, pickers,
57+ dock, start screen), `diff/`, `settings/`, `pair/` (pairing, adding projects), `panels/`
58+ (right panel, subagents, command palette), `terminal/` and `device/`.
59+ - `term/`: the terminal emulator the terminal panel draws.
60+ - `scripts/lucide.py`: turns the desktop's Lucide icons into vector drawables (`icons.txt`
61+ lists the ones in use).
+44−0android/app/build.gradle.ktsaddedView File
@@ -0,0 +1,44 @@
1+ plugins {
2+ alias(libs.plugins.android.application)
3+ alias(libs.plugins.kotlin.compose)
4+ alias(libs.plugins.kotlin.serialization)
5+ }
6+
7+ android {
8+ namespace = "cc.vectrum.code"
9+ compileSdk = 37
10+
11+ defaultConfig {
12+ applicationId = "cc.vectrum.code"
13+ minSdk = 29
14+ targetSdk = 37
15+ versionCode = 1
16+ versionName = "0.1.0"
17+ }
18+
19+ buildFeatures { compose = true }
20+
21+ testOptions { unitTests.isReturnDefaultValues = true }
22+
23+ buildTypes {
24+ release {
25+ isMinifyEnabled = true
26+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
27+ // Signed with the debug key until there's an upload key.
28+ signingConfig = signingConfigs.getByName("debug")
29+ }
30+ }
31+ }
32+
33+ dependencies {
34+ implementation(libs.androidx.activity.compose)
35+ implementation(libs.compose.runtime)
36+ implementation(libs.compose.foundation)
37+ implementation(libs.compose.ui)
38+ implementation(libs.compose.ui.text)
39+ implementation(libs.coroutines.android)
40+ implementation(libs.serialization.json)
41+ implementation(libs.okhttp)
42+ implementation(libs.code.scanner)
43+ testImplementation(libs.junit)
44+ }
+5−0android/app/proguard-rules.proaddedView File
@@ -0,0 +1,5 @@
1+ # kotlinx.serialization keeps its generated serializers through its own rules.
2+ -dontwarn okhttp3.internal.platform.**
3+ -dontwarn org.conscrypt.**
4+ -dontwarn org.bouncycastle.**
5+ -dontwarn org.openjsse.**
+39−0android/app/src/main/AndroidManifest.xmladdedView File
@@ -0,0 +1,39 @@
1+ <?xml version="1.0" encoding="utf-8"?>
2+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+ <uses-permission android:name="android.permission.INTERNET" />
5+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
6+
7+ <application
8+ android:name=".VectrumApp"
9+ android:icon="@mipmap/ic_launcher"
10+ android:roundIcon="@mipmap/ic_launcher"
11+ android:label="Vectrum Code"
12+ android:theme="@style/Theme.Vectrum"
13+ android:usesCleartextTraffic="true"
14+ android:supportsRtl="true">
15+
16+ <!-- Fetches Google's QR code scanner with the app, for pairing. -->
17+ <meta-data
18+ android:name="com.google.mlkit.vision.DEPENDENCIES"
19+ android:value="barcode_ui" />
20+
21+ <activity
22+ android:name=".MainActivity"
23+ android:exported="true"
24+ android:windowSoftInputMode="adjustResize"
25+ android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboard|keyboardHidden|navigation|density|uiMode">
26+ <intent-filter>
27+ <action android:name="android.intent.action.MAIN" />
28+ <category android:name="android.intent.category.LAUNCHER" />
29+ </intent-filter>
30+ <!-- A pairing code as a link: vectrum://192.168.2.10:47823/<key> -->
31+ <intent-filter>
32+ <action android:name="android.intent.action.VIEW" />
33+ <category android:name="android.intent.category.DEFAULT" />
34+ <category android:name="android.intent.category.BROWSABLE" />
35+ <data android:scheme="vectrum" />
36+ </intent-filter>
37+ </activity>
38+ </application>
39+ </manifest>
+1−0android/app/src/main/assets/themes.jsonaddedView File
@@ -0,0 +1 @@
1+ {"themes":[{"id":"graphite","name":"Graphite","palette":{"dark":true,"page":"#19191bff","s_hi":"#2c2c2fff","s":"#252527ff","s_lo":"#202022ff","well_top":"#141415ff","well_bot":"#18181aff","ink":"#f2f2f0ff","ink2":"#a6a6a9ff","ink3":"#939397ff","icon":"#d6d6d8ff","engrave":"#ffffff7d","tag_ring":"#ffffff29","rule":"#00000073","rule_lip":"#ffffff09","row_hover":"#ffffff0f","menu_bg":"#222225fa","menu_row_hover":"#ffffff0f","field_ink":"#8e8e93ff","primary_ink":"#1b1b1dff","green_deep":"#3fb97aff","green_ink":"#5fcb93ff","red":"#ff7a66ff","amber":"#f5bf55ff","amber_ink":"#e7c98aff","blue_ink":"#8fb0ffff","cap":["#313134ff","#28282bff"],"pressed":["#18181aff","#1c1c1eff"],"row_on":["#333337ff","#2a2a2dff"],"kbd":["#35353aff","#2a2a2dff"],"primary":["#f4f3f0ff","#e4e3dfff"],"primary_pressed":["#dad9d5ff","#e3e2deff"],"danger":["#e6594eff","#d0433aff"],"danger_pressed":["#be352cff","#cf4238ff"],"region":["#ffffff05","#ffffff03"],"led_off":["#8b8b8eff","#4a4a4dff"],"tints":["#fb794aff","#ed6ba2ff","#25c2c3ff","#ce76dcff","#3e9bfaff","#8986faff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffb3"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000047"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#78140a4d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#6e0a0024"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#aa1e143d"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#50000073"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#6e0a008c"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"bone","name":"Bone","palette":{"dark":false,"page":"#f3f3f1ff","s_hi":"#fcfbf9ff","s":"#f6f5f2ff","s_lo":"#f0efebff","well_top":"#e6e5e1ff","well_bot":"#edece8ff","ink":"#1b1b1dff","ink2":"#5c5c60ff","ink3":"#6c6c6fff","icon":"#3a3a3dff","engrave":"#282620a6","tag_ring":"#28262033","rule":"#00000013","rule_lip":"#ffffffcc","row_hover":"#ffffffb3","menu_bg":"#fbfaf8fa","menu_row_hover":"#0000000d","field_ink":"#8e8e93ff","primary_ink":"#ffffffff","green_deep":"#3fb97aff","green_ink":"#2b7a52ff","red":"#d8453bff","amber":"#f5bf55ff","amber_ink":"#9a6a12ff","blue_ink":"#3558c9ff","cap":["#ffffffff","#f4f3f0ff"],"pressed":["#e9e8e4ff","#efeeeaff"],"row_on":["#ffffffff","#f6f5f2ff"],"kbd":["#ffffffff","#f2f1eeff"],"primary":["#38383cff","#252528ff"],"primary_pressed":["#1c1c1eff","#242427ff"],"danger":["#e6594eff","#d0433aff"],"danger_pressed":["#be352cff","#cf4238ff"],"region":["#28262007","#28262003"],"led_off":["#d6d5d1ff","#c9c8c4ff"],"tints":["#d0560eff","#d54487ff","#108a8bff","#b653c5ff","#0f7ddcff","#736aecff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#6e645012"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#1816100b"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#1816100d"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#18161014"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#18161021"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#18161021"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#6e64500f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#1816100a"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#1816100a"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#18161014"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#1816101a"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffd9"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#1816100d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#18161012"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#18161017"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffe6"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000000f"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000000f"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#00000014"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#3c372824"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#3c37281c"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#1816101a"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffffe6"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#3c372812"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":5.0,"spread":1.0,"inset":true,"color":"#ffffff12"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff2e"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000080"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000026"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000038"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#000000b3"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#78140a4d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#6e0a0024"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#aa1e143d"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#50000073"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#6e0a008c"}],"led_off_sh":[{"x":0.0,"y":1.0,"blur":1.0,"spread":0.0,"inset":true,"color":"#00000033"}]}},{"id":"tokyo-night","name":"Tokyo Night","palette":{"dark":true,"page":"#16161eff","s_hi":"#20212fff","s":"#1a1b26ff","s_lo":"#171721ff","well_top":"#0e0f15ff","well_bot":"#111219ff","ink":"#c0caf5ff","ink2":"#9ba3cdff","ink3":"#767da4ff","icon":"#b1bbe5ff","engrave":"#c0caf580","tag_ring":"#c0caf529","rule":"#00000073","rule_lip":"#ffffff0b","row_hover":"#ffffff0f","menu_bg":"#181923fa","menu_row_hover":"#ffffff0f","field_ink":"#64698bff","primary_ink":"#16161eff","green_deep":"#9ece6aff","green_ink":"#9ece6aff","red":"#f7768eff","amber":"#e0af68ff","amber_ink":"#e0af68ff","blue_ink":"#7aa2f7ff","cap":["#242635ff","#1d1e2aff"],"pressed":["#111219ff","#14151dff"],"row_on":["#262838ff","#1e202cff"],"kbd":["#292a3bff","#1e202cff"],"primary":["#8daff8ff","#6795f6ff"],"primary_pressed":["#4a80f4ff","#5d8ef5ff"],"danger":["#f55976ff","#f33356ff"],"danger_pressed":["#f1163fff","#f33054ff"],"region":["#ffffff05","#ffffff03"],"led_off":["#757b98ff","#3b3e4fff"],"tints":["#ff9e64ff","#f7768eff","#73dacaff","#bb9af7ff","#7aa2f7ff","#9d7cd8ff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"tokyo-night-storm","name":"Tokyo Night Storm","palette":{"dark":true,"page":"#1f2335ff","s_hi":"#2a2e44ff","s":"#24283bff","s_lo":"#1f2333ff","well_top":"#131620ff","well_bot":"#181a27ff","ink":"#c0caf5ff","ink2":"#a1a9d1ff","ink3":"#8289acff","icon":"#b4bde6ff","engrave":"#c0caf580","tag_ring":"#c0caf529","rule":"#00000073","rule_lip":"#ffffff09","row_hover":"#ffffff0f","menu_bg":"#212536fa","menu_row_hover":"#ffffff0f","field_ink":"#6f7596ff","primary_ink":"#1f2335ff","green_deep":"#9ece6aff","green_ink":"#9ece6aff","red":"#f7768eff","amber":"#e0af68ff","amber_ink":"#e0af68ff","blue_ink":"#7aa2f7ff","cap":["#2d334bff","#272b3fff"],"pressed":["#181a27ff","#1c1f2dff"],"row_on":["#2f354eff","#282d42ff"],"kbd":["#323751ff","#282d42ff"],"primary":["#8daff8ff","#6795f6ff"],"primary_pressed":["#4a80f4ff","#5d8ef5ff"],"danger":["#f55976ff","#f33356ff"],"danger_pressed":["#f1163fff","#f33054ff"],"region":["#ffffff05","#ffffff03"],"led_off":["#7a81a1ff","#434860ff"],"tints":["#ff9e64ff","#f7768eff","#73dacaff","#bb9af7ff","#7aa2f7ff","#9d7cd8ff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"catppuccin-mocha","name":"Catppuccin Mocha","palette":{"dark":true,"page":"#181825ff","s_hi":"#242437ff","s":"#1e1e2eff","s_lo":"#1a1a28ff","well_top":"#101019ff","well_bot":"#14141eff","ink":"#cdd6f4ff","ink2":"#a6adc8ff","ink3":"#7f849cff","icon":"#bdc6e2ff","engrave":"#cdd6f480","tag_ring":"#cdd6f429","rule":"#00000073","rule_lip":"#ffffff09","row_hover":"#ffffff0f","menu_bg":"#1c1c2afa","menu_row_hover":"#ffffff0f","field_ink":"#6c7086ff","primary_ink":"#181825ff","green_deep":"#a6e3a1ff","green_ink":"#a6e3a1ff","red":"#f38ba8ff","amber":"#f9e2afff","amber_ink":"#f9e2afff","blue_ink":"#89b4faff","cap":["#28283dff","#212132ff"],"pressed":["#14141eff","#171723ff"],"row_on":["#2a2a40ff","#222234ff"],"kbd":["#2c2c44ff","#222234ff"],"primary":["#d6b9f9ff","#c093f5ff"],"primary_pressed":["#b077f3ff","#bb8af4ff"],"danger":["#ef6189ff","#eb3c6dff"],"danger_pressed":["#e82158ff","#eb3a6bff"],"region":["#ffffff05","#ffffff03"],"led_off":["#7e839bff","#414356ff"],"tints":["#fab387ff","#f5c2e7ff","#94e2d5ff","#cba6f7ff","#89b4faff","#b4befeff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"catppuccin-macchiato","name":"Catppuccin Macchiato","palette":{"dark":true,"page":"#1e2030ff","s_hi":"#2a2d43ff","s":"#24273aff","s_lo":"#1f2232ff","well_top":"#13151fff","well_bot":"#181a26ff","ink":"#cad3f5ff","ink2":"#a6aecdff","ink3":"#838aa4ff","icon":"#bcc4e5ff","engrave":"#cad3f580","tag_ring":"#cad3f529","rule":"#00000073","rule_lip":"#ffffff09","row_hover":"#ffffff0f","menu_bg":"#212435fa","menu_row_hover":"#ffffff0f","field_ink":"#70768fff","primary_ink":"#1e2030ff","green_deep":"#a6da95ff","green_ink":"#a6da95ff","red":"#ed8796ff","amber":"#eed49fff","amber_ink":"#eed49fff","blue_ink":"#8aadf4ff","cap":["#2e3149ff","#272a3eff"],"pressed":["#181a26ff","#1c1e2dff"],"row_on":["#30334dff","#282b41ff"],"kbd":["#323650ff","#282b41ff"],"primary":["#d1b3f8ff","#bb8df4ff"],"primary_pressed":["#aa71f2ff","#b584f3ff"],"danger":["#e8687bff","#e3445cff"],"danger_pressed":["#df2a45ff","#e3425aff"],"region":["#ffffff05","#ffffff03"],"led_off":["#7f86a1ff","#45495fff"],"tints":["#f5a97fff","#f5bde6ff","#8bd5caff","#c6a0f6ff","#8aadf4ff","#b7bdf8ff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"catppuccin-frappe","name":"Catppuccin Frappé","palette":{"dark":true,"page":"#292c3cff","s_hi":"#363b4fff","s":"#303446ff","s_lo":"#2a2d3dff","well_top":"#1a1c26ff","well_bot":"#20222eff","ink":"#c6d0f5ff","ink2":"#acb4d3ff","ink3":"#9299b2ff","icon":"#bcc5e8ff","engrave":"#c6d0f580","tag_ring":"#c6d0f529","rule":"#00000073","rule_lip":"#ffffff09","row_hover":"#ffffff0f","menu_bg":"#2c3041fa","menu_row_hover":"#ffffff0f","field_ink":"#7e859cff","primary_ink":"#292c3cff","green_deep":"#a6d189ff","green_ink":"#a6d189ff","red":"#e78284ff","amber":"#e5c890ff","amber_ink":"#e5c890ff","blue_ink":"#8caaeeff","cap":["#3a3f55ff","#33374aff"],"pressed":["#20222eff","#252836ff"],"row_on":["#3c4158ff","#34394cff"],"kbd":["#3f445bff","#34394cff"],"primary":["#d3aeeaff","#c18ee2ff"],"primary_pressed":["#b475dcff","#bd86e0ff"],"danger":["#e36d6fff","#dc4a4dff"],"danger_pressed":["#d73134ff","#dc484bff"],"region":["#ffffff05","#ffffff03"],"led_off":["#828aa6ff","#4e5369ff"],"tints":["#ef9f76ff","#f4b8e4ff","#81c8beff","#ca9ee6ff","#8caaeeff","#babbf1ff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"github-dark","name":"GitHub Dark","palette":{"dark":true,"page":"#010409ff","s_hi":"#121820ff","s":"#0d1117ff","s_lo":"#0b0f14ff","well_top":"#07090cff","well_bot":"#090b0fff","ink":"#e6edf3ff","ink2":"#b2b9c2ff","ink3":"#7d8590ff","icon":"#d1d8dfff","engrave":"#e6edf380","tag_ring":"#e6edf329","rule":"#00000073","rule_lip":"#ffffff12","row_hover":"#ffffff0f","menu_bg":"#0c1015fa","menu_row_hover":"#ffffff0f","field_ink":"#676e78ff","primary_ink":"#ffffffff","green_deep":"#3fb950ff","green_ink":"#3fb950ff","red":"#f85149ff","amber":"#d29922ff","amber_ink":"#d29922ff","blue_ink":"#4493f8ff","cap":["#161d27ff","#10141cff"],"pressed":["#090b0fff","#0a0d12ff"],"row_on":["#181f2aff","#11161eff"],"kbd":["#1a222eff","#11161eff"],"primary":["#27963dff","#1f762fff"],"primary_pressed":["#185e26ff","#1d6e2cff"],"danger":["#f85a53ff","#f7352cff"],"danger_pressed":["#f6190eff","#f73229ff"],"region":["#ffffff05","#ffffff03"],"led_off":["#848a90ff","#383d43ff"],"tints":["#ffa657ff","#db61a2ff","#a5d6ffff","#d2a8ffff","#79c0ffff","#a371f7ff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"github-dark-dimmed","name":"GitHub Dark Dimmed","palette":{"dark":true,"page":"#1c2128ff","s_hi":"#282e37ff","s":"#22272eff","s_lo":"#1e2228ff","well_top":"#121519ff","well_bot":"#161a1eff","ink":"#adbac7ff","ink2":"#96a2afff","ink3":"#7e8b97ff","icon":"#a4b1bdff","engrave":"#adbac780","tag_ring":"#adbac729","rule":"#00000073","rule_lip":"#ffffff09","row_hover":"#ffffff0f","menu_bg":"#1f242afa","menu_row_hover":"#ffffff0f","field_ink":"#6c7782ff","primary_ink":"#ffffffff","green_deep":"#57ab5aff","green_ink":"#57ab5aff","red":"#e76058ff","amber":"#c69026ff","amber_ink":"#c69026ff","blue_ink":"#539bf5ff","cap":["#2d333cff","#252a32ff"],"pressed":["#161a1eff","#1a1e23ff"],"row_on":["#2f363fff","#272c34ff"],"kbd":["#313843ff","#272c34ff"],"primary":["#3a8b40ff","#2e6f32ff"],"primary_pressed":["#255929ff","#2b672fff"],"danger":["#e8645dff","#e24239ff"],"danger_pressed":["#dd2a20ff","#e24037ff"],"region":["#ffffff05","#ffffff03"],"led_off":["#6e7882ff","#3e444dff"],"tints":["#f69d50ff","#c96198ff","#96d0ffff","#dcbdfbff","#6cb6ffff","#986ee2ff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"one-dark","name":"One Dark","palette":{"dark":true,"page":"#21252bff","s_hi":"#2e333cff","s":"#282c34ff","s_lo":"#23262dff","well_top":"#16181cff","well_bot":"#1a1d22ff","ink":"#abb2bfff","ink2":"#9fa4afff","ink3":"#8d919aff","icon":"#a6acb9ff","engrave":"#abb2bf80","tag_ring":"#abb2bf29","rule":"#00000073","rule_lip":"#ffffff09","row_hover":"#ffffff0f","menu_bg":"#252930fa","menu_row_hover":"#ffffff0f","field_ink":"#787d86ff","primary_ink":"#21252bff","green_deep":"#98c379ff","green_ink":"#98c379ff","red":"#e17079ff","amber":"#e5c07bff","amber_ink":"#e5c07bff","blue_ink":"#61afefff","cap":["#333842ff","#2b2f38ff"],"pressed":["#1a1d22ff","#1f2228ff"],"row_on":["#353a45ff","#2d313aff"],"kbd":["#383d48ff","#2d313aff"],"primary":["#74b8f1ff","#4ea6edff"],"primary_pressed":["#3398eaff","#45a1ecff"],"danger":["#e17079ff","#da4f59ff"],"danger_pressed":["#d43542ff","#d94c57ff"],"region":["#ffffff05","#ffffff03"],"led_off":["#707680ff","#424750ff"],"tints":["#d19a66ff","#e06c75ff","#56b6c2ff","#c678ddff","#61afefff","#e5c07bff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"nord","name":"Nord","palette":{"dark":true,"page":"#2e3440ff","s_hi":"#41495bff","s":"#3b4252ff","s_lo":"#333947ff","well_top":"#20242cff","well_bot":"#272c36ff","ink":"#eceff4ff","ink2":"#c8ced9ff","ink3":"#a5adbfff","icon":"#dee2e9ff","engrave":"#eceff480","tag_ring":"#eceff429","rule":"#00000073","rule_lip":"#ffffff09","row_hover":"#ffffff0f","menu_bg":"#363d4cfa","menu_row_hover":"#ffffff0f","field_ink":"#9098a9ff","primary_ink":"#2e3440ff","green_deep":"#a3be8cff","green_ink":"#a6c08fff","red":"#d89fa4ff","amber":"#ebcb8bff","amber_ink":"#ebcb8bff","blue_ink":"#a3bad2ff","cap":["#454e61ff","#3e4556ff"],"pressed":["#272c36ff","#2d333fff"],"row_on":["#485063ff","#3f4758ff"],"kbd":["#4a5367ff","#3f4758ff"],"primary":["#97c8d6ff","#79b8caff"],"primary_pressed":["#63adc2ff","#72b4c7ff"],"danger":["#c57078ff","#b9525cff"],"danger_pressed":["#a9444eff","#b8515bff"],"region":["#ffffff05","#ffffff03"],"led_off":["#9ca1abff","#5e6572ff"],"tints":["#d08770ff","#cc8289ff","#8fbcbbff","#b48eadff","#81a1c1ff","#7c99bcff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"gruvbox-dark","name":"Gruvbox Dark","palette":{"dark":true,"page":"#1d2021ff","s_hi":"#2f2f2fff","s":"#282828ff","s_lo":"#232323ff","well_top":"#161616ff","well_bot":"#1a1a1aff","ink":"#ebdbb2ff","ink2":"#caba9bff","ink3":"#a89984ff","icon":"#decea9ff","engrave":"#ebdbb280","tag_ring":"#ebdbb229","rule":"#00000073","rule_lip":"#ffffff09","row_hover":"#ffffff0f","menu_bg":"#252525fa","menu_row_hover":"#ffffff0f","field_ink":"#8e8272ff","primary_ink":"#1d2021ff","green_deep":"#b8bb26ff","green_ink":"#b8bb26ff","red":"#fb523eff","amber":"#fabd2fff","amber_ink":"#fabd2fff","blue_ink":"#83a598ff","cap":["#343434ff","#2c2c2cff"],"pressed":["#1a1a1aff","#1f1f1fff"],"row_on":["#373737ff","#2d2d2dff"],"kbd":["#3a3a3aff","#2d2d2dff"],"primary":["#fe8b2dff","#fe7505ff"],"primary_pressed":["#e36701ff","#f77001ff"],"danger":["#fb5643ff","#fb331bff"],"danger_pressed":["#f21e05ff","#fa3018ff"],"region":["#ffffff05","#ffffff03"],"led_off":["#938a74ff","#4f4c44ff"],"tints":["#fe8019ff","#fb4934ff","#8ec07cff","#d3869bff","#83a598ff","#fabd2fff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"rose-pine","name":"Rosé Pine","palette":{"dark":true,"page":"#191724ff","s_hi":"#252337ff","s":"#1f1d2eff","s_lo":"#1b1928ff","well_top":"#111019ff","well_bot":"#14131eff","ink":"#e0def4ff","ink2":"#b8b5cfff","ink3":"#908caaff","icon":"#d0cee5ff","engrave":"#e0def480","tag_ring":"#e0def429","rule":"#00000073","rule_lip":"#ffffff09","row_hover":"#ffffff0f","menu_bg":"#1d1b2afa","menu_row_hover":"#ffffff0f","field_ink":"#797691ff","primary_ink":"#191724ff","green_deep":"#9ccfd8ff","green_ink":"#9ccfd8ff","red":"#eb6f92ff","amber":"#f6c177ff","amber_ink":"#f6c177ff","blue_ink":"#c4a7e7ff","cap":["#29273dff","#222032ff"],"pressed":["#14131eff","#181623ff"],"row_on":["#2b2940ff","#232135ff"],"kbd":["#2e2b44ff","#232135ff"],"primary":["#f0cbcaff","#e6adaaff"],"primary_pressed":["#e09692ff","#e4a5a2ff"],"danger":["#ea668bff","#e54270ff"],"danger_pressed":["#e1275cff","#e4406eff"],"region":["#ffffff05","#ffffff03"],"led_off":["#89879bff","#464456ff"],"tints":["#ebbcbaff","#eb6f92ff","#9ccfd8ff","#c4a7e7ff","#327793ff","#f6c177ff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"rose-pine-moon","name":"Rosé Pine Moon","palette":{"dark":true,"page":"#232136ff","s_hi":"#302d48ff","s":"#2a273fff","s_lo":"#252237ff","well_top":"#171522ff","well_bot":"#1c1a2aff","ink":"#e0def4ff","ink2":"#b8b5cfff","ink3":"#908caaff","icon":"#d0cee5ff","engrave":"#e0def480","tag_ring":"#e0def429","rule":"#00000073","rule_lip":"#ffffff09","row_hover":"#ffffff0f","menu_bg":"#27243afa","menu_row_hover":"#ffffff0f","field_ink":"#7c7895ff","primary_ink":"#232136ff","green_deep":"#9ccfd8ff","green_ink":"#9ccfd8ff","red":"#eb6f92ff","amber":"#f6c177ff","amber_ink":"#f6c177ff","blue_ink":"#c4a7e7ff","cap":["#34314eff","#2d2a43ff"],"pressed":["#1c1a2aff","#201e31ff"],"row_on":["#363352ff","#2e2b46ff"],"kbd":["#393555ff","#2e2b46ff"],"primary":["#edaaa8ff","#e78a86ff"],"primary_pressed":["#e1716dff","#e5817eff"],"danger":["#ea668bff","#e54270ff"],"danger_pressed":["#e1275cff","#e4406eff"],"region":["#ffffff05","#ffffff03"],"led_off":["#8e8ca3ff","#4e4c63ff"],"tints":["#ea9a97ff","#eb6f92ff","#9ccfd8ff","#c4a7e7ff","#3e8fb0ff","#f6c177ff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"solarized-dark","name":"Solarized Dark","palette":{"dark":true,"page":"#00212bff","s_hi":"#003745ff","s":"#002b36ff","s_lo":"#00252fff","well_top":"#00171dff","well_bot":"#001c24ff","ink":"#9eaaaaff","ink2":"#9aaaaeff","ink3":"#7f979fff","icon":"#9caaacff","engrave":"#9eaaaa80","tag_ring":"#9eaaaa29","rule":"#00000073","rule_lip":"#ffffff0d","row_hover":"#ffffff0f","menu_bg":"#002832fa","menu_row_hover":"#ffffff0f","field_ink":"#66828aff","primary_ink":"#00212bff","green_deep":"#859900ff","green_ink":"#8ea300ff","red":"#e66a68ff","amber":"#b58900ff","amber_ink":"#c49500ff","blue_ink":"#48a0deff","cap":["#003f4fff","#00313dff"],"pressed":["#001c24ff","#00212aff"],"row_on":["#004354ff","#003441ff"],"kbd":["#00475aff","#003441ff"],"primary":["#3295daff","#2380c1ff"],"primary_pressed":["#1e6ea7ff","#217ab8ff"],"danger":["#df4340ff","#d32724ff"],"danger_pressed":["#b9221fff","#d12623ff"],"region":["#ffffff05","#ffffff03"],"led_off":["#577176ff","#20444dff"],"tints":["#d44e17ff","#d6438aff","#2aa198ff","#7075c5ff","#268bd2ff","#b58900ff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"dracula","name":"Dracula","palette":{"dark":true,"page":"#21222cff","s_hi":"#2e313eff","s":"#282a36ff","s_lo":"#23252fff","well_top":"#16171dff","well_bot":"#1a1c24ff","ink":"#f8f8f2ff","ink2":"#bcc2d3ff","ink3":"#7f8cb5ff","icon":"#e0e2e6ff","engrave":"#f8f8f280","tag_ring":"#f8f8f229","rule":"#00000073","rule_lip":"#ffffff09","row_hover":"#ffffff0f","menu_bg":"#252732fa","menu_row_hover":"#ffffff0f","field_ink":"#6e789bff","primary_ink":"#21222cff","green_deep":"#50fa7bff","green_ink":"#50fa7bff","red":"#ff5555ff","amber":"#f1fa8cff","amber_ink":"#f1fa8cff","blue_ink":"#8be9fdff","cap":["#333544ff","#2b2d3aff"],"pressed":["#1a1c24ff","#1f202aff"],"row_on":["#353747ff","#2d2f3cff"],"kbd":["#373a4bff","#2d2f3cff"],"primary":["#c9a6faff","#b180f8ff"],"primary_pressed":["#9f63f6ff","#ab76f7ff"],"danger":["#ff5050ff","#ff2727ff"],"danger_pressed":["#ff0909ff","#ff2525ff"],"region":["#ffffff05","#ffffff03"],"led_off":["#9a9b9dff","#52535cff"],"tints":["#ffb86cff","#ff79c6ff","#8be9fdff","#bd93f9ff","#50fa7bff","#f1fa8cff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0e"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000073"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000038"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#00000057"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0000004d"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffff0d"},{"x":1.5,"y":2.5,"blur":3.0,"spread":-1.0,"inset":true,"color":"#ffffff17"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#0000004d"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000066"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#00000033"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#00000047"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0f"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000008c"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0000004d"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffff0d"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff1a"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000004d"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#0000004d"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#000000a6"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000080"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#00000099"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0000008c"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffff12"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#00000080"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000059"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffff0d"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#00000059"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#00000073"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#00000059"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000099"}],"led_off_sh":[{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#00000040"}]}},{"id":"tokyo-night-day","name":"Tokyo Night Day","palette":{"dark":false,"page":"#d0d5e3ff","s_hi":"#e8e8ecff","s":"#e1e2e7ff","s_lo":"#dadbe1ff","well_top":"#ced0d8ff","well_bot":"#d6d7deff","ink":"#3054a7ff","ink2":"#3c5599ff","ink3":"#5466a8ff","icon":"#3554a2ff","engrave":"#3054a7a6","tag_ring":"#3054a733","rule":"#00000013","rule_lip":"#ffffffcc","row_hover":"#ffffffb3","menu_bg":"#e5e6eafa","menu_row_hover":"#0000000d","field_ink":"#707fb5ff","primary_ink":"#ffffffff","green_deep":"#587539ff","green_ink":"#506b34ff","red":"#d80a46ff","amber":"#8c6c3eff","amber_ink":"#7a5e36ff","blue_ink":"#155fc5ff","cap":["#f2f2f5ff","#dfe0e5ff"],"pressed":["#d1d2daff","#d9dae0ff"],"row_on":["#f2f2f5ff","#e1e2e7ff"],"kbd":["#f2f2f5ff","#dddee4ff"],"primary":["#4088ebff","#1c72e7ff"],"primary_pressed":["#1664ceff","#186de1ff"],"danger":["#f63d73ff","#f41757ff"],"danger_pressed":["#e10b49ff","#f41455ff"],"region":["#20212807","#20212803"],"led_off":["#bcbecaff","#aeb1beff"],"tints":["#b15c00ff","#f52a65ff","#118c74ff","#9854f1ff","#2e7de9ff","#7847bdff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#54586a12"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#1213160b"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#1213160d"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#12131614"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#12131621"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#12131621"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#54586a0f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#1213160a"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#1213160a"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#12131614"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#1213161a"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffd9"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#1213160d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#12131612"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#12131617"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffe6"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000000f"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000000f"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#00000014"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#2c2e3824"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#2c2e381c"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#1213161a"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffffe6"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#2c2e3812"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0c38734d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0b316624"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#1253ab3d"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#08244a73"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0b31668c"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#7a06274d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#6b052324"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#b4083a3d"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#4e041973"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#6b05238c"}],"led_off_sh":[{"x":0.0,"y":1.0,"blur":1.0,"spread":0.0,"inset":true,"color":"#00000033"}]}},{"id":"catppuccin-latte","name":"Catppuccin Latte","palette":{"dark":false,"page":"#e6e9efff","s_hi":"#f6f7faff","s":"#eff1f5ff","s_lo":"#e7eaf0ff","well_top":"#dadfe8ff","well_bot":"#e3e6edff","ink":"#4c4f69ff","ink2":"#5d6078ff","ink3":"#6e7287ff","icon":"#53566fff","engrave":"#4c4f69a6","tag_ring":"#4c4f6933","rule":"#00000013","rule_lip":"#ffffffcc","row_hover":"#ffffffb3","menu_bg":"#f4f5f8fa","menu_row_hover":"#0000000d","field_ink":"#888b9dff","primary_ink":"#ffffffff","green_deep":"#40a02bff","green_ink":"#327c21ff","red":"#d20f39ff","amber":"#df8e1dff","amber_ink":"#976014ff","blue_ink":"#145ff5ff","cap":["#ffffffff","#eceff3ff"],"pressed":["#dde1eaff","#e6e9efff"],"row_on":["#ffffffff","#eff1f5ff"],"kbd":["#ffffffff","#eaedf2ff"],"primary":["#934cf1ff","#7d26edff"],"primary_pressed":["#6d12e3ff","#771dedff"],"danger":["#e5103eff","#bf0e34ff"],"danger_pressed":["#a20c2cff","#bd0d33ff"],"region":["#1b212c07","#1b212c03"],"led_off":["#c6cddcff","#b7c0d2ff"],"tints":["#ef5801ff","#e44ebcff","#179299ff","#8839efff","#1e66f5ff","#687efdff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#49587512"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0f12190b"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0f12190d"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#0f121914"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#0f121921"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#0f121921"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#4958750f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0f12190a"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0f12190a"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#0f121914"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#0f12191a"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffd9"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0f12190d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0f121912"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0f121917"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffe6"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000000f"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000000f"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#00000014"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#262e3e24"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#262e3e1c"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#0f12191a"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffffe6"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#262e3e12"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#390a764d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#32086824"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#540eaf3d"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#24064b73"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#3208688c"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#7708204d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#69071c24"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#b00d303d"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#4c051573"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#69071c8c"}],"led_off_sh":[{"x":0.0,"y":1.0,"blur":1.0,"spread":0.0,"inset":true,"color":"#00000033"}]}},{"id":"github-light","name":"GitHub Light","palette":{"dark":false,"page":"#f6f8faff","s_hi":"#ffffffff","s":"#ffffffff","s_lo":"#f8f8f8ff","well_top":"#eeeeeeff","well_bot":"#f5f5f5ff","ink":"#1f2328ff","ink2":"#42484fff","ink3":"#656d76ff","icon":"#2d3238ff","engrave":"#1f2328a6","tag_ring":"#1f232833","rule":"#00000013","rule_lip":"#ffffffcc","row_hover":"#ffffffb3","menu_bg":"#fffffffa","menu_row_hover":"#0000000d","field_ink":"#848a91ff","primary_ink":"#ffffffff","green_deep":"#1a7f37ff","green_ink":"#1a7f37ff","red":"#d1242fff","amber":"#9a6700ff","amber_ink":"#9a6700ff","blue_ink":"#0969daff","cap":["#ffffffff","#fdfdfdff"],"pressed":["#f0f0f0ff","#f8f8f8ff"],"row_on":["#ffffffff","#ffffffff"],"kbd":["#ffffffff","#fbfbfbff"],"primary":["#239944ff","#1b7736ff"],"primary_pressed":["#165e2aff","#196f32ff"],"danger":["#db2e39ff","#c0212bff"],"danger_pressed":["#a51d25ff","#bd212bff"],"region":["#24242407","#24242403"],"led_off":["#dededeff","#d1d1d1ff"],"tints":["#bc4c00ff","#bf3989ff","#1b7c83ff","#8250dfff","#0550aeff","#6639baff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#5f5f5f12"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#1414140b"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#1414140d"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#14141414"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#14141421"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#14141421"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#5f5f5f0f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#1414140a"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#1414140a"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#14141414"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#1414141a"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffd9"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#1414140d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#14141412"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#14141417"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffe6"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000000f"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000000f"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#00000014"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#32323224"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#3232321c"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#1414141a"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffffe6"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#32323212"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#18682f4d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#155b2924"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#239a453d"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#0f421e73"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#155b298c"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#6d13184d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#60101624"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#a11c243d"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#460c1073"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#6010168c"}],"led_off_sh":[{"x":0.0,"y":1.0,"blur":1.0,"spread":0.0,"inset":true,"color":"#00000033"}]}},{"id":"one-light","name":"One Light","palette":{"dark":false,"page":"#eaeaebff","s_hi":"#ffffffff","s":"#fafafaff","s_lo":"#f3f3f3ff","well_top":"#e9e9e9ff","well_bot":"#f0f0f0ff","ink":"#383a42ff","ink2":"#585a62ff","ink3":"#787a82ff","icon":"#45474fff","engrave":"#383a42a6","tag_ring":"#383a4233","rule":"#00000013","rule_lip":"#ffffffcc","row_hover":"#ffffffb3","menu_bg":"#fefefefa","menu_row_hover":"#0000000d","field_ink":"#92939aff","primary_ink":"#ffffffff","green_deep":"#50a14fff","green_ink":"#3f7f3eff","red":"#e14133ff","amber":"#c18401ff","amber_ink":"#986801ff","blue_ink":"#2d6bf1ff","cap":["#ffffffff","#f8f8f8ff"],"pressed":["#ebebebff","#f3f3f3ff"],"row_on":["#ffffffff","#fafafaff"],"kbd":["#ffffffff","#f6f6f6ff"],"primary":["#5386f3ff","#2d6af1ff"],"primary_pressed":["#1056efff","#2364f0ff"],"danger":["#e7665bff","#e14637ff"],"danger_pressed":["#da3020ff","#e14335ff"],"region":["#24242407","#24242403"],"led_off":["#d9d9d9ff","#ccccccff"],"tints":["#986801ff","#e45649ff","#0184bcff","#a626a4ff","#4078f2ff","#c18401ff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#5f5f5f12"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#1414140b"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#1414140d"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#14141414"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#14141421"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#14141421"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#5f5f5f0f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#1414140a"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#1414140a"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#14141414"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#1414141a"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffd9"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#1414140d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#14141412"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#14141417"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffe6"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000000f"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000000f"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#00000014"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#32323224"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#3232321c"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#1414141a"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffffe6"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#32323212"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#082b774d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#07266924"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#0c40b13d"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#051c4c73"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0726698c"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#6f18104d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#62150e24"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#a424183d"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#47100b73"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#62150e8c"}],"led_off_sh":[{"x":0.0,"y":1.0,"blur":1.0,"spread":0.0,"inset":true,"color":"#00000033"}]}},{"id":"gruvbox-light","name":"Gruvbox Light","palette":{"dark":false,"page":"#f2e5bcff","s_hi":"#fcf4d2ff","s":"#fbf1c7ff","s_lo":"#faeebbff","well_top":"#f9e9a7ff","well_bot":"#faecb4ff","ink":"#3c3836ff","ink2":"#5c544dff","ink3":"#7c6f64ff","icon":"#49433fff","engrave":"#3c3836a6","tag_ring":"#3c383633","rule":"#00000013","rule_lip":"#ffffffcc","row_hover":"#ffffffb3","menu_bg":"#fcf3cefa","menu_row_hover":"#0000000d","field_ink":"#958978ff","primary_ink":"#ffffffff","green_deep":"#79740eff","green_ink":"#74700dff","red":"#9d0006ff","amber":"#b57614ff","amber_ink":"#956110ff","blue_ink":"#076678ff","cap":["#fdf8e4ff","#fbf0c3ff"],"pressed":["#f9eaacff","#faeeb9ff"],"row_on":["#fdf8e4ff","#fbf1c7ff"],"kbd":["#fdf8e4ff","#faefc0ff"],"primary":["#c34103ff","#9b3303ff"],"primary_pressed":["#7d2902ff","#913002ff"],"danger":["#b10007ff","#890005ff"],"danger_pressed":["#6a0004ff","#860005ff"],"region":["#2d291b07","#2d291b03"],"led_off":["#f7e289ff","#f5dc71ff"],"tints":["#af3a03ff","#9d0006ff","#427b58ff","#8f3f71ff","#076678ff","#b57614ff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#776e4712"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#19170f0b"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#19170f0d"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#19170f14"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#19170f21"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#19170f21"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#776e470f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#19170f0a"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#19170f0a"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#19170f14"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#19170f1a"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffd9"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#19170f0d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#19170f12"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#19170f17"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffe6"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000000f"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000000f"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#00000014"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#3f3a2624"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#3f3a261c"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#19170f1a"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffffe6"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#3f3a2612"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#7d2a024d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#6e250224"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#ba3d033d"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#501b0173"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#6e25028c"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#8000054d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#70000424"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#bd00073d"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#52000373"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#7000048c"}],"led_off_sh":[{"x":0.0,"y":1.0,"blur":1.0,"spread":0.0,"inset":true,"color":"#00000033"}]}},{"id":"rose-pine-dawn","name":"Rosé Pine Dawn","palette":{"dark":false,"page":"#faf4edff","s_hi":"#ffffffff","s":"#fffaf3ff","s_lo":"#fff4e6ff","well_top":"#ffecd1ff","well_bot":"#fff2dfff","ink":"#575279ff","ink2":"#686486ff","ink3":"#797593ff","icon":"#5e597eff","engrave":"#575279a6","tag_ring":"#57527933","rule":"#00000013","rule_lip":"#ffffffcc","row_hover":"#ffffffb3","menu_bg":"#fffdfbfa","menu_row_hover":"#0000000d","field_ink":"#9490a6ff","primary_ink":"#ffffffff","green_deep":"#56949fff","green_ink":"#487b85ff","red":"#b4637aff","amber":"#ea9d34ff","amber_ink":"#a26511ff","blue_ink":"#80679dff","cap":["#ffffffff","#fff8efff"],"pressed":["#ffeed6ff","#fff4e4ff"],"row_on":["#ffffffff","#fffaf3ff"],"kbd":["#ffffffff","#fff7ebff"],"primary":["#dc918eff","#d2736eff"],"primary_pressed":["#cb5c57ff","#d06b67ff"],"danger":["#bb7186ff","#ad556eff"],"danger_pressed":["#9a4a61ff","#ad546dff"],"region":["#2d251b07","#2d251b03"],"led_off":["#ffdeb1ff","#ffd497ff"],"tints":["#d37772ff","#b4637aff","#56949fff","#907aa9ff","#286983ff","#cc7e15ff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#77634712"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#19150f0b"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#19150f0d"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#19150f14"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#19150f21"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#19150f21"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#7763470f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#19150f0a"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#19150f0a"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#19150f14"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#19150f1a"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffd9"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#19150f0d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#19150f12"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#19150f17"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffe6"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000000f"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000000f"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#00000014"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#3f342624"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#3f34261c"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#19150f1a"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffffe6"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#3f342612"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#61211e4d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#561d1b24"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#90312d3d"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#3e151373"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#561d1b8c"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#5629364d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#4c243024"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#7f3d503d"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#371a2373"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#4c24308c"}],"led_off_sh":[{"x":0.0,"y":1.0,"blur":1.0,"spread":0.0,"inset":true,"color":"#00000033"}]}},{"id":"solarized-light","name":"Solarized Light","palette":{"dark":false,"page":"#eee8d5ff","s_hi":"#fefaeeff","s":"#fdf6e3ff","s_lo":"#fcf2d7ff","well_top":"#fbecc3ff","well_bot":"#fcf0d0ff","ink":"#566b72ff","ink2":"#5a6b6eff","ink3":"#6c7c7cff","icon":"#576b71ff","engrave":"#566b72a6","tag_ring":"#566b7233","rule":"#00000013","rule_lip":"#ffffffcc","row_hover":"#ffffffb3","menu_bg":"#fef8eafa","menu_row_hover":"#0000000d","field_ink":"#899490ff","primary_ink":"#ffffffff","green_deep":"#859900ff","green_ink":"#667500ff","red":"#dc322fff","amber":"#b58900ff","amber_ink":"#8c6a00ff","blue_ink":"#2074afff","cap":["#ffffffff","#fdf5dfff"],"pressed":["#fbedc8ff","#fcf2d5ff"],"row_on":["#ffffffff","#fdf6e3ff"],"kbd":["#ffffffff","#fcf4dcff"],"primary":["#3295daff","#2380c1ff"],"primary_pressed":["#1e6ea7ff","#217ab8ff"],"danger":["#df4340ff","#d32724ff"],"danger_pressed":["#b9221fff","#d12623ff"],"region":["#2d281b07","#2d281b03"],"led_off":["#f9e2a5ff","#f7da8dff"],"tints":["#cb4b16ff","#d33682ff","#299d94ff","#6c71c4ff","#268bd2ff","#b08500ff"],"raise":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#776a4712"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#19160f0b"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#19160f0d"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-6.0,"inset":false,"color":"#19160f14"},{"x":0.0,"y":26.0,"blur":50.0,"spread":-16.0,"inset":false,"color":"#19160f21"},{"x":0.0,"y":52.0,"blur":96.0,"spread":-36.0,"inset":false,"color":"#19160f21"}],"raise_lite":[{"x":0.0,"y":0.0,"blur":6.0,"spread":2.0,"inset":true,"color":"#ffffffbf"},{"x":2.0,"y":3.0,"blur":3.0,"spread":-1.0,"inset":true,"color":"#fffffff2"},{"x":-1.0,"y":-3.0,"blur":5.0,"spread":-2.0,"inset":true,"color":"#776a470f"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#19160f0a"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#19160f0a"},{"x":0.0,"y":8.0,"blur":18.0,"spread":-8.0,"inset":false,"color":"#19160f14"},{"x":0.0,"y":22.0,"blur":40.0,"spread":-18.0,"inset":false,"color":"#19160f1a"}],"raise_sm":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffd9"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#19160f0d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#19160f12"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#19160f17"}],"btn":[{"x":0.0,"y":0.0,"blur":4.0,"spread":1.0,"inset":true,"color":"#ffffffe6"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffffff"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#0000000f"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#0000000f"},{"x":0.0,"y":4.0,"blur":9.0,"spread":-3.0,"inset":false,"color":"#00000014"}],"pressed_sh":[{"x":0.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#3f382624"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":3.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"well":[{"x":1.0,"y":2.0,"blur":5.0,"spread":-1.0,"inset":true,"color":"#3f38261c"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#19160f1a"},{"x":0.0,"y":0.5,"blur":0.0,"spread":0.0,"inset":false,"color":"#ffffffe6"}],"region_sh":[{"x":0.0,"y":2.0,"blur":6.0,"spread":-1.0,"inset":true,"color":"#3f382612"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":true,"color":"#00000009"},{"x":0.0,"y":-2.0,"blur":4.0,"spread":-2.0,"inset":true,"color":"#ffffffe6"}],"primary_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#14476c4d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#113f5f24"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#1d6aa03d"}],"primary_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#0d2e4573"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#113f5f8c"}],"danger_sh":[{"x":0.0,"y":0.0,"blur":6.0,"spread":1.0,"inset":true,"color":"#ffffff24"},{"x":1.0,"y":2.0,"blur":2.0,"spread":-1.0,"inset":true,"color":"#ffffff52"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#6d14124d"},{"x":0.0,"y":1.0,"blur":2.0,"spread":0.0,"inset":false,"color":"#60121024"},{"x":0.0,"y":5.0,"blur":12.0,"spread":-4.0,"inset":false,"color":"#a21e1b3d"}],"danger_pressed_sh":[{"x":0.0,"y":1.0,"blur":3.0,"spread":0.0,"inset":true,"color":"#460d0c73"},{"x":0.0,"y":0.0,"blur":0.0,"spread":0.5,"inset":false,"color":"#6012108c"}],"led_off_sh":[{"x":0.0,"y":1.0,"blur":1.0,"spread":0.0,"inset":true,"color":"#00000033"}]}}],"families":[{"name":"Vectrum","themes":["bone","graphite"]},{"name":"Tokyo Night","themes":["tokyo-night-day","tokyo-night","tokyo-night-storm"]},{"name":"Catppuccin","themes":["catppuccin-latte","catppuccin-mocha","catppuccin-macchiato","catppuccin-frappe"]},{"name":"GitHub","themes":["github-light","github-dark","github-dark-dimmed"]},{"name":"One","themes":["one-light","one-dark"]},{"name":"Nord","themes":["nord"]},{"name":"Gruvbox","themes":["gruvbox-light","gruvbox-dark"]},{"name":"Rosé Pine","themes":["rose-pine-dawn","rose-pine","rose-pine-moon"]},{"name":"Solarized","themes":["solarized-light","solarized-dark"]},{"name":"Dracula","themes":["dracula"]}]}
+68−0android/app/src/main/java/cc/vectrum/code/MainActivity.ktaddedView File
@@ -0,0 +1,68 @@
1+ package cc.vectrum.code
2+
3+ import android.content.Intent
4+ import android.graphics.Color
5+ import android.os.Bundle
6+ import androidx.activity.ComponentActivity
7+ import androidx.activity.SystemBarStyle
8+ import androidx.activity.compose.setContent
9+ import androidx.activity.enableEdgeToEdge
10+ import androidx.compose.foundation.isSystemInDarkTheme
11+ import androidx.compose.runtime.DisposableEffect
12+ import androidx.compose.runtime.LaunchedEffect
13+ import androidx.compose.runtime.remember
14+ import cc.vectrum.code.net.parsePairing
15+ import cc.vectrum.code.net.withPort
16+ import cc.vectrum.code.theme.ProvidePalette
17+ import cc.vectrum.code.theme.Themes
18+ import cc.vectrum.code.ui.Root
19+ import cc.vectrum.code.ui.Nav
20+
21+ class MainActivity : ComponentActivity() {
22+ private val app get() = (application as VectrumApp).state
23+ private val nav = Nav()
24+
25+ override fun onCreate(savedInstanceState: Bundle?) {
26+ super.onCreate(savedInstanceState)
27+ handle(intent)
28+ setContent {
29+ val systemDark = isSystemInDarkTheme()
30+ val prefs = app.prefs
31+ val palette = when (prefs.themeMode) {
32+ "light" -> Themes.find(prefs.lightTheme, false)
33+ "dark" -> Themes.find(prefs.darkTheme, true)
34+ else -> if (systemDark) Themes.find(prefs.darkTheme, true) else Themes.find(prefs.lightTheme, false)
35+ }
36+ val dark = palette.dark
37+ DisposableEffect(dark) {
38+ val style = if (dark) SystemBarStyle.dark(Color.TRANSPARENT)
39+ else SystemBarStyle.light(Color.TRANSPARENT, Color.TRANSPARENT)
40+ enableEdgeToEdge(statusBarStyle = style, navigationBarStyle = style)
41+ onDispose {}
42+ }
43+ ProvidePalette(palette) {
44+ Root(app, nav)
45+ }
46+ }
47+ }
48+
49+ override fun onNewIntent(intent: Intent) {
50+ super.onNewIntent(intent)
51+ handle(intent)
52+ }
53+
54+ override fun onResume() {
55+ super.onResume()
56+ app.resume()
57+ }
58+
59+ /** A `vectrum://address/key` link pairs with that host. */
60+ private fun handle(intent: Intent?) {
61+ val data = intent?.data ?: return
62+ if (data.scheme != "vectrum") return
63+ val (address, key) = parsePairing(data.toString())
64+ if (address.isBlank() || key == null) return
65+ app.addHost(withPort(address), key)
66+ nav.home()
67+ }
68+ }
+17−0android/app/src/main/java/cc/vectrum/code/VectrumApp.ktaddedView File
@@ -0,0 +1,17 @@
1+ package cc.vectrum.code
2+
3+ import android.app.Application
4+ import cc.vectrum.code.data.AppState
5+ import cc.vectrum.code.theme.Themes
6+
7+ class VectrumApp : Application() {
8+ lateinit var state: AppState
9+ private set
10+
11+ override fun onCreate() {
12+ super.onCreate()
13+ Themes.load(this)
14+ state = AppState(this)
15+ state.start()
16+ }
17+ }
+411−0android/app/src/main/java/cc/vectrum/code/data/AppState.ktaddedView File
@@ -0,0 +1,411 @@
1+ package cc.vectrum.code.data
2+
3+ import android.content.Context
4+ import android.content.SharedPreferences
5+ import android.os.Build
6+ import androidx.compose.runtime.getValue
7+ import androidx.compose.runtime.mutableStateListOf
8+ import androidx.compose.runtime.mutableStateMapOf
9+ import androidx.compose.runtime.mutableStateOf
10+ import androidx.compose.runtime.setValue
11+ import cc.vectrum.code.net.ClientMessage
12+ import cc.vectrum.code.net.Connection
13+ import cc.vectrum.code.net.ConnectionStatus
14+ import cc.vectrum.code.net.Folder
15+ import cc.vectrum.code.net.ProjectInfo
16+ import cc.vectrum.code.net.RemoteFileDiff
17+ import cc.vectrum.code.net.RemoteInstance
18+ import cc.vectrum.code.net.RemoteModel
19+ import cc.vectrum.code.net.SavedItem
20+ import cc.vectrum.code.net.ServerMessage
21+ import cc.vectrum.code.net.SettingsPatch
22+ import cc.vectrum.code.net.ThreadAction
23+ import cc.vectrum.code.net.ThreadDetail
24+ import cc.vectrum.code.net.ThreadSummary
25+ import cc.vectrum.code.net.WireJson
26+ import kotlinx.coroutines.CompletableDeferred
27+ import kotlinx.serialization.Serializable
28+ import kotlinx.serialization.builtins.ListSerializer
29+ import kotlinx.serialization.json.JsonElement
30+ import kotlinx.serialization.json.JsonObject
31+ import kotlinx.serialization.json.JsonPrimitive
32+ import java.util.UUID
33+
34+ /** A desktop this phone pairs with, as saved: its address and key. */
35+ @Serializable
36+ data class SavedHost(
37+ val id: String = UUID.randomUUID().toString(),
38+ /** The host's name, as it last said; shown until it says again. */
39+ val name: String = "",
40+ val address: String = "",
41+ val key: String = "",
42+ val enabled: Boolean = true,
43+ )
44+
45+ /** A thread anywhere: the host it lives on and its id there. */
46+ data class ThreadRef(val host: String, val thread: String)
47+
48+ /**
49+ * Everything the app knows, as Compose state: the paired hosts, each one's
50+ * projects and threads, and the threads that are open. Messages from hosts
51+ * are applied on the main thread, so the UI reads it all directly.
52+ */
53+ class AppState(context: Context) {
54+ private val store: SharedPreferences = context.getSharedPreferences("vectrum", Context.MODE_PRIVATE)
55+ val prefs = DevicePrefs(store)
56+
57+ val hosts = mutableStateListOf<HostState>()
58+
59+ /** A short message at the bottom of the screen, like the desktop's notices. */
60+ var notice by mutableStateOf<Notice?>(null)
61+ private set
62+
63+ private val deviceName = listOfNotNull(Build.MANUFACTURER?.replaceFirstChar { it.uppercase() }, Build.MODEL)
64+ .distinct()
65+ .joinToString(" ")
66+ .ifBlank { "Android" }
67+
68+ init {
69+ val saved = store.getString("hosts", null)?.let {
70+ runCatching { WireJson.decodeFromString(ListSerializer(SavedHost.serializer()), it) }.getOrNull()
71+ } ?: emptyList()
72+ saved.forEach { hosts.add(HostState(it, this)) }
73+ }
74+
75+ fun start() = hosts.filter { it.saved.enabled }.forEach { it.connect(deviceName) }
76+
77+ fun stop() = hosts.forEach { it.disconnect() }
78+
79+ /** Reconnects whatever dropped, as when the app comes back to the foreground. */
80+ fun resume() = hosts.filter { it.saved.enabled && it.status !is ConnectionStatus.Connected }.forEach { it.retry() }
81+
82+ fun host(id: String): HostState? = hosts.find { it.saved.id == id }
83+
84+ fun addHost(address: String, key: String, name: String = ""): HostState {
85+ val existing = hosts.find { it.saved.address == address }
86+ if (existing != null) {
87+ existing.update(existing.saved.copy(key = key, enabled = true))
88+ existing.retry()
89+ return existing
90+ }
91+ val host = HostState(SavedHost(name = name, address = address, key = key), this)
92+ hosts.add(host)
93+ persistHosts()
94+ host.connect(deviceName)
95+ return host
96+ }
97+
98+ fun removeHost(host: HostState) {
99+ host.disconnect()
100+ hosts.remove(host)
101+ persistHosts()
102+ }
103+
104+ fun setHostEnabled(host: HostState, enabled: Boolean) {
105+ host.update(host.saved.copy(enabled = enabled))
106+ if (enabled) host.connect(deviceName) else host.disconnect()
107+ }
108+
109+ internal fun persistHosts() {
110+ val list = hosts.map { it.saved }
111+ store.edit().putString("hosts", WireJson.encodeToString(ListSerializer(SavedHost.serializer()), list)).apply()
112+ }
113+
114+ fun thread(ref: ThreadRef): ThreadSummary? = host(ref.host)?.threads?.get(ref.thread)
115+
116+ fun showNotice(text: String, error: Boolean = false, undo: (() -> Unit)? = null) {
117+ notice = Notice(text, error, undo, System.currentTimeMillis())
118+ }
119+
120+ fun clearNotice(shown: Notice) {
121+ if (notice == shown) notice = null
122+ }
123+ }
124+
125+ data class Notice(val text: String, val error: Boolean, val undo: (() -> Unit)?, val at: Long)
126+
127+ /** One paired host: its connection and what it shares. */
128+ class HostState(saved: SavedHost, private val app: AppState) {
129+ var saved by mutableStateOf(saved)
130+ private set
131+ var status by mutableStateOf<ConnectionStatus>(ConnectionStatus.Offline("Not connected"))
132+ private set
133+
134+ /** The name the host gives itself, or the address until it does. */
135+ val name: String get() = saved.name.ifBlank { saved.address }
136+ var version by mutableStateOf("")
137+ private set
138+
139+ val projects = mutableStateListOf<ProjectInfo>()
140+ val threads = mutableStateMapOf<String, ThreadSummary>()
141+
142+ /** The host's environment settings and `project_overrides`, raw. */
143+ var settings by mutableStateOf(JsonObject(emptyMap()))
144+ private set
145+ var instances by mutableStateOf<List<RemoteInstance>>(emptyList())
146+ private set
147+ var favorites by mutableStateOf<List<String>>(emptyList())
148+ private set
149+
150+ /** Threads open in the app, with their transcripts. */
151+ val open = mutableStateMapOf<String, OpenThread>()
152+
153+ /** The host's devices, as `DeviceState` last said, for the Device panel. */
154+ var deviceState by mutableStateOf<JsonObject?>(null)
155+ private set
156+
157+ private var connection: Connection? = null
158+ private var nextRequest = 1L
159+ private val subscribed = mutableSetOf<String>()
160+ private val createdCallbacks = mutableMapOf<Long, (String) -> Unit>()
161+ private val folderRequests = mutableMapOf<String, CompletableDeferred<ServerMessage.Folders>>()
162+ private val diffRequests = mutableMapOf<Long, CompletableDeferred<ServerMessage.DiffReply>>()
163+ private val deviceRequests = mutableMapOf<Long, CompletableDeferred<ServerMessage.DeviceReply>>()
164+ private val terminals = mutableMapOf<Long, TerminalListener>()
165+
166+ /** Listeners for device events agents cause: (thread, device or `null` for all, opened). */
167+ val deviceEvents = mutableListOf<(String, JsonObject?, String?) -> Unit>()
168+
169+ val connected: Boolean get() = status is ConnectionStatus.Connected
170+
171+ internal fun update(saved: SavedHost) {
172+ this.saved = saved
173+ app.persistHosts()
174+ }
175+
176+ fun connect(deviceName: String) {
177+ connection?.stop()
178+ val connection = Connection(saved.address, saved.key, deviceName, ::onStatus, ::onMessage)
179+ this.connection = connection
180+ connection.start()
181+ }
182+
183+ fun disconnect() {
184+ connection?.stop()
185+ connection = null
186+ status = ConnectionStatus.Offline("Turned off")
187+ }
188+
189+ fun retry() {
190+ val connection = connection
191+ if (connection == null) {
192+ if (saved.enabled) connect(Build.MODEL ?: "Android")
193+ } else {
194+ connection.reconnect()
195+ }
196+ }
197+
198+ fun send(message: ClientMessage): Boolean = connection?.send(message) ?: false
199+
200+ private fun onStatus(status: ConnectionStatus) {
201+ this.status = status
202+ if (status is ConnectionStatus.Connected) {
203+ version = status.version
204+ if (saved.name != status.host) update(saved.copy(name = status.host))
205+ send(ClientMessage.Catalog)
206+ subscribed.forEach { send(ClientMessage.Subscribe(it)) }
207+ }
208+ if (status !is ConnectionStatus.Connected && status !is ConnectionStatus.Connecting) {
209+ // Whatever waits on a reply won't get one from this connection.
210+ folderRequests.values.forEach { it.complete(ServerMessage.Folders(error = "The host is offline.")) }
211+ folderRequests.clear()
212+ diffRequests.values.forEach { it.complete(ServerMessage.DiffReply(error = "The host is offline.")) }
213+ diffRequests.clear()
214+ deviceRequests.values.forEach { it.complete(ServerMessage.DeviceReply(error = "The host is offline.")) }
215+ deviceRequests.clear()
216+ terminals.values.forEach { it.onExit("The connection dropped.") }
217+ terminals.clear()
218+ }
219+ }
220+
221+ private fun onMessage(message: ServerMessage) {
222+ when (message) {
223+ is ServerMessage.State -> {
224+ projects.clear()
225+ projects.addAll(message.projects)
226+ val ids = message.threads.map { it.id }.toSet()
227+ threads.keys.retainAll(ids)
228+ message.threads.forEach { threads[it.id] = it }
229+ open.values.forEach { thread -> message.threads.find { it.id == thread.id }?.let { thread.summary = it } }
230+ }
231+ is ServerMessage.Thread -> {
232+ threads[message.summary.id] = message.summary
233+ open.getOrPut(message.summary.id) { OpenThread(this, message.summary.id) }.apply(message)
234+ }
235+ is ServerMessage.Created -> createdCallbacks.remove(message.reference)?.invoke(message.thread)
236+ is ServerMessage.Error -> app.showNotice(message.message, error = true)
237+ is ServerMessage.Settings -> settings = message.values
238+ is ServerMessage.Folders -> folderRequests.remove(message.path)?.complete(message)
239+ is ServerMessage.ProjectAdded -> app.showNotice("Added ${message.path.substringAfterLast('/')}")
240+ is ServerMessage.TerminalOutput -> terminals[message.terminal]?.onOutput(
241+ android.util.Base64.decode(message.data, android.util.Base64.DEFAULT),
242+ )
243+ is ServerMessage.TerminalExited -> terminals.remove(message.terminal)?.onExit(message.error)
244+ is ServerMessage.DeviceReply -> deviceRequests.remove(message.request)?.complete(message)
245+ is ServerMessage.DeviceState -> deviceState = message.state
246+ is ServerMessage.DeviceOpened -> deviceEvents.toList().forEach { it(message.thread, message.device, null) }
247+ is ServerMessage.DeviceClosed -> deviceEvents.toList().forEach { it(message.thread, null, message.device ?: "") }
248+ is ServerMessage.Catalog -> {
249+ instances = message.instances
250+ favorites = message.favorites
251+ }
252+ is ServerMessage.DiffReply -> diffRequests.remove(message.request)?.complete(message)
253+ is ServerMessage.Welcome, is ServerMessage.Denied -> {}
254+ }
255+ }
256+
257+ // ---- threads ---------------------------------------------------------------
258+
259+ /** Opens a thread: its transcript comes and stays current until [close]. */
260+ fun open(id: String): OpenThread {
261+ val thread = open.getOrPut(id) { OpenThread(this, id) }
262+ threads[id]?.let { thread.summary = it }
263+ if (subscribed.add(id)) send(ClientMessage.Subscribe(id))
264+ return thread
265+ }
266+
267+ fun close(id: String) {
268+ if (subscribed.remove(id)) send(ClientMessage.Unsubscribe(id))
269+ }
270+
271+ fun act(thread: String, action: ThreadAction) {
272+ if (!send(ClientMessage.Action(thread, action))) app.showNotice("$name is offline.", error = true)
273+ }
274+
275+ /** Starts a thread with its first message; [onCreated] gets its id once the host made it. */
276+ fun create(message: ClientMessage.Create, onCreated: (String) -> Unit) {
277+ val reference = nextRequest++
278+ createdCallbacks[reference] = onCreated
279+ if (!send(message.copy(reference = reference))) {
280+ createdCallbacks.remove(reference)
281+ app.showNotice("$name is offline.", error = true)
282+ }
283+ }
284+
285+ fun updateSettings(patch: SettingsPatch) {
286+ // Shown at once; the host sends the settings back as they ended up.
287+ settings = JsonObject(settings + patch.values)
288+ if (!send(ClientMessage.UpdateSettings(patch))) app.showNotice("$name is offline.", error = true)
289+ }
290+
291+ fun setting(key: String): JsonElement? = settings[key]
292+
293+ fun refreshCatalog() = send(ClientMessage.Catalog)
294+
295+ fun model(instance: String, id: String?): RemoteModel? {
296+ // Threads saved before instances existed name none; any instance's model will do then.
297+ val models = instances.find { it.id == instance }?.models ?: instances.flatMap { it.models }
298+ return models.find { it.id == id } ?: models.find { it.resolved == id && !it.legacy } ?: models.find { it.resolved == id }
299+ }
300+
301+ // ---- requests --------------------------------------------------------------
302+
303+ suspend fun listFolders(path: String): ServerMessage.Folders {
304+ val reply = CompletableDeferred<ServerMessage.Folders>()
305+ folderRequests[path]?.complete(ServerMessage.Folders(path = path, error = "Asked again."))
306+ folderRequests[path] = reply
307+ if (!send(ClientMessage.ListFolders(path))) {
308+ folderRequests.remove(path)
309+ return ServerMessage.Folders(path = path, error = "$name is offline.")
310+ }
311+ return reply.await()
312+ }
313+
314+ suspend fun diff(thread: String, base: String, head: String, path: String?, ignoreWhitespace: Boolean): ServerMessage.DiffReply {
315+ val request = nextRequest++
316+ val reply = CompletableDeferred<ServerMessage.DiffReply>()
317+ diffRequests[request] = reply
318+ if (!send(ClientMessage.Diff(request, thread, base, head, path, ignoreWhitespace))) {
319+ diffRequests.remove(request)
320+ return ServerMessage.DiffReply(request, error = "$name is offline.")
321+ }
322+ return reply.await()
323+ }
324+
325+ /** Sends a `DeviceCall` (an object tagged by `call`) and waits for its reply. */
326+ suspend fun device(call: JsonObject): ServerMessage.DeviceReply {
327+ val request = nextRequest++
328+ val reply = CompletableDeferred<ServerMessage.DeviceReply>()
329+ deviceRequests[request] = reply
330+ if (!send(ClientMessage.Device(request, call))) {
331+ deviceRequests.remove(request)
332+ return ServerMessage.DeviceReply(request, error = "$name is offline.")
333+ }
334+ return reply.await()
335+ }
336+
337+ fun deviceCall(name: String, vararg fields: Pair<String, JsonElement>): JsonObject =
338+ JsonObject(mapOf("call" to JsonPrimitive(name)) + fields)
339+
340+ // ---- terminals -------------------------------------------------------------
341+
342+ /** Starts a shell on the host in a thread's folder; returns its id, or `null` while offline. */
343+ fun openTerminal(thread: String?, project: String, columns: Int, lines: Int, listener: TerminalListener): Long? {
344+ val terminal = nextRequest++
345+ terminals[terminal] = listener
346+ if (!send(ClientMessage.TerminalOpen(terminal, thread, project, columns, lines))) {
347+ terminals.remove(terminal)
348+ return null
349+ }
350+ return terminal
351+ }
352+
353+ fun terminalInput(terminal: Long, bytes: ByteArray) {
354+ send(ClientMessage.TerminalInput(terminal, android.util.Base64.encodeToString(bytes, android.util.Base64.NO_WRAP)))
355+ }
356+
357+ fun resizeTerminal(terminal: Long, columns: Int, lines: Int) = send(ClientMessage.TerminalResize(terminal, columns, lines))
358+
359+ fun closeTerminal(terminal: Long) {
360+ terminals.remove(terminal)
361+ send(ClientMessage.TerminalClose(terminal))
362+ }
363+ }
364+
365+ interface TerminalListener {
366+ fun onOutput(bytes: ByteArray)
367+
368+ fun onExit(error: String?)
369+ }
370+
371+ /** A thread that's open, with its transcript kept current by the host. */
372+ class OpenThread(val host: HostState, val id: String) {
373+ var summary by mutableStateOf(host.threads[id] ?: ThreadSummary(id = id))
374+ internal set
375+ var detail by mutableStateOf(ThreadDetail())
376+ private set
377+ val items = mutableStateListOf<SavedItem>()
378+
379+ /** The host sent the transcript at least once. */
380+ var loaded by mutableStateOf(false)
381+ private set
382+
383+ val ref get() = ThreadRef(host.saved.id, id)
384+
385+ internal fun apply(message: ServerMessage.Thread) {
386+ summary = message.summary
387+ detail = message.detail
388+ val from = message.from.coerceAtMost(items.size)
389+ // Items before `from` are as last sent; the rest are replaced.
390+ if (from < items.size) items.removeRange(from, items.size)
391+ items.addAll(message.items)
392+ if (items.size > message.len) items.removeRange(message.len, items.size)
393+ loaded = true
394+ }
395+
396+ fun act(action: ThreadAction) = host.act(id, action)
397+
398+ val working: Boolean get() = summary.status == "working" || detail.thinking
399+
400+ /** The project this thread runs in, if the host lists it. */
401+ val project: ProjectInfo? get() = host.projects.find { it.path == summary.project }
402+ }
403+
404+ /** The Folders reply's subfolders, for pickers. */
405+ typealias Folders = List<Folder>
406+
407+ /** Patches with one value. */
408+ fun patchOf(key: String, value: JsonElement) = SettingsPatch(values = JsonObject(mapOf(key to value)))
409+
410+ /** One diff, as fetched. */
411+ typealias FileDiffs = List<RemoteFileDiff>
+88−0android/app/src/main/java/cc/vectrum/code/data/DevicePrefs.ktaddedView File
@@ -0,0 +1,88 @@
1+ package cc.vectrum.code.data
2+
3+ import android.content.SharedPreferences
4+ import androidx.compose.runtime.getValue
5+ import androidx.compose.runtime.mutableStateOf
6+ import androidx.compose.runtime.setValue
7+ import cc.vectrum.code.net.WireJson
8+ import kotlinx.serialization.json.JsonElement
9+ import kotlinx.serialization.json.JsonObject
10+ import kotlinx.serialization.json.JsonPrimitive
11+ import kotlinx.serialization.json.booleanOrNull
12+ import kotlinx.serialization.json.jsonObject
13+ import kotlinx.serialization.json.jsonPrimitive
14+
15+ /**
16+ * This phone's own preferences: what the desktop calls device preferences
17+ * (appearance, conversation, confirmations), under the desktop's setting
18+ * keys and with its defaults. Environment settings belong to the host and
19+ * come over the connection instead.
20+ */
21+ class DevicePrefs(private val store: SharedPreferences) {
22+ private var values by mutableStateOf(load())
23+
24+ private fun load(): JsonObject = store.getString("prefs", null)
25+ ?.let { runCatching { WireJson.parseToJsonElement(it).jsonObject }.getOrNull() }
26+ ?: JsonObject(emptyMap())
27+
28+ operator fun get(key: String): JsonElement? = values[key]
29+
30+ fun string(key: String): String = values[key]?.jsonPrimitive?.content ?: DEFAULTS.getValue(key).toString()
31+
32+ fun bool(key: String): Boolean = values[key]?.jsonPrimitive?.booleanOrNull ?: (DEFAULTS[key] as? Boolean ?: false)
33+
34+ fun set(key: String, value: String) = put(key, JsonPrimitive(value))
35+
36+ fun set(key: String, value: Boolean) = put(key, JsonPrimitive(value))
37+
38+ /** Whether a preference differs from its default, for the settings' reset buttons. */
39+ fun changed(key: String): Boolean = key in values && values[key]?.jsonPrimitive?.content != DEFAULTS[key].toString()
40+
41+ fun reset(key: String) {
42+ values = JsonObject(values - key)
43+ save()
44+ }
45+
46+ private fun put(key: String, value: JsonElement) {
47+ values = JsonObject(values + (key to value))
48+ save()
49+ }
50+
51+ private fun save() = store.edit().putString("prefs", values.toString()).apply()
52+
53+ // The ones read everywhere, by name.
54+ /** `system`, `light` or `dark`. */
55+ val themeMode get() = string("theme_mode")
56+ val lightTheme get() = string("light_theme")
57+ val darkTheme get() = string("dark_theme")
58+
59+ companion object {
60+ /** The desktop's defaults (src/settings.rs), as JSON scalars. */
61+ val DEFAULTS: Map<String, Any> = mapOf(
62+ "theme_mode" to "system",
63+ "light_theme" to "bone",
64+ "dark_theme" to "graphite",
65+ "diff_colors" to "red_green",
66+ "time_format" to "system",
67+ "fold_turns" to true,
68+ "show_costs" to true,
69+ "follow_up" to "queue",
70+ "streaming" to "paragraphs",
71+ "slash_skills" to true,
72+ "inline_ui" to true,
73+ "hide_whitespace" to true,
74+ "diff_collapsed" to true,
75+ "diff_side_by_side" to false,
76+ "composer_context" to false,
77+ "plan_mode" to false,
78+ "context_meter" to false,
79+ "confirm_unpin" to false,
80+ "confirm_archive" to false,
81+ "confirm_settle" to false,
82+ "confirm_delete" to true,
83+ "thread_notifications" to "notifications",
84+ "working_section" to false,
85+ "show_settled" to true,
86+ )
87+ }
88+ }
+138−0android/app/src/main/java/cc/vectrum/code/net/Connection.ktaddedView File
@@ -0,0 +1,138 @@
1+ package cc.vectrum.code.net
2+
3+ import android.os.Handler
4+ import android.os.Looper
5+ import okhttp3.OkHttpClient
6+ import okhttp3.Request
7+ import okhttp3.Response
8+ import okhttp3.WebSocket
9+ import okhttp3.WebSocketListener
10+ import java.util.concurrent.TimeUnit
11+
12+ /** Where a connection to a host stands. */
13+ sealed interface ConnectionStatus {
14+ data object Connecting : ConnectionStatus
15+
16+ data class Connected(val host: String, val version: String) : ConnectionStatus
17+
18+ /** The host turned us away (a wrong key, another protocol); no retrying. */
19+ data class Denied(val reason: String) : ConnectionStatus
20+
21+ /** Lost or never made; retried after a while. */
22+ data class Offline(val reason: String) : ConnectionStatus
23+ }
24+
25+ /**
26+ * One WebSocket to a host, kept up: it says hello with the key, hands every
27+ * message to [onMessage] on the main thread, and reconnects with a backoff
28+ * when the connection drops.
29+ */
30+ class Connection(
31+ private val address: String,
32+ private val key: String,
33+ private val deviceName: String,
34+ private val onStatus: (ConnectionStatus) -> Unit,
35+ private val onMessage: (ServerMessage) -> Unit,
36+ ) {
37+ private val main = Handler(Looper.getMainLooper())
38+ private var socket: WebSocket? = null
39+ private var welcomed = false
40+ private var stopped = false
41+ private var attempt = 0
42+ private var generation = 0
43+
44+ fun start() {
45+ stopped = false
46+ open()
47+ }
48+
49+ fun stop() {
50+ stopped = true
51+ generation++
52+ main.removeCallbacksAndMessages(null)
53+ socket?.close(1000, "bye")
54+ socket = null
55+ }
56+
57+ /** Drops the connection and dials again at once, as after the network comes back. */
58+ fun reconnect() {
59+ if (stopped) return
60+ generation++
61+ main.removeCallbacksAndMessages(null)
62+ socket?.cancel()
63+ socket = null
64+ attempt = 0
65+ open()
66+ }
67+
68+ /** Sends once welcomed; messages before that are dropped like the desktop drops them. */
69+ fun send(message: ClientMessage): Boolean {
70+ val socket = socket ?: return false
71+ if (!welcomed) return false
72+ return socket.send(encode(message))
73+ }
74+
75+ val connected: Boolean get() = welcomed && socket != null
76+
77+ private fun open() {
78+ val gen = ++generation
79+ welcomed = false
80+ onStatus(ConnectionStatus.Connecting)
81+ val request = Request.Builder().url("ws://${withPort(address)}/").build()
82+ socket = client.newWebSocket(
83+ request,
84+ object : WebSocketListener() {
85+ override fun onOpen(webSocket: WebSocket, response: Response) {
86+ webSocket.send(encode(ClientMessage.Hello(key = key, name = deviceName)))
87+ }
88+
89+ override fun onMessage(webSocket: WebSocket, text: String) {
90+ val message = parseServerMessage(text) ?: return
91+ main.post {
92+ if (gen != generation) return@post
93+ when (message) {
94+ is ServerMessage.Welcome -> {
95+ welcomed = true
96+ attempt = 0
97+ onStatus(ConnectionStatus.Connected(message.host, message.version))
98+ }
99+ is ServerMessage.Denied -> {
100+ stopped = true
101+ onStatus(ConnectionStatus.Denied(message.reason))
102+ }
103+ else -> onMessage(message)
104+ }
105+ }
106+ }
107+
108+ override fun onClosed(webSocket: WebSocket, code: Int, reason: String) =
109+ dropped(gen, reason.ifBlank { "The host closed the connection." })
110+
111+ override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) =
112+ dropped(gen, t.message ?: t.javaClass.simpleName)
113+ },
114+ )
115+ }
116+
117+ private fun dropped(gen: Int, reason: String) {
118+ main.post {
119+ if (gen != generation || stopped) return@post
120+ welcomed = false
121+ socket = null
122+ onStatus(ConnectionStatus.Offline(reason))
123+ val delay = RETRY_DELAYS[attempt.coerceAtMost(RETRY_DELAYS.lastIndex)]
124+ attempt++
125+ main.postDelayed({ if (gen == generation && !stopped) open() }, delay)
126+ }
127+ }
128+
129+ companion object {
130+ private val RETRY_DELAYS = longArrayOf(1_000, 2_000, 4_000, 8_000, 15_000)
131+
132+ private val client = OkHttpClient.Builder()
133+ .connectTimeout(8, TimeUnit.SECONDS)
134+ .readTimeout(0, TimeUnit.SECONDS)
135+ .pingInterval(0, TimeUnit.SECONDS)
136+ .build()
137+ }
138+ }
+542−0android/app/src/main/java/cc/vectrum/code/net/Protocol.ktaddedView File
@@ -0,0 +1,542 @@
1+ @file:OptIn(ExperimentalSerializationApi::class)
2+
3+ package cc.vectrum.code.net
4+
5+ import kotlinx.serialization.ExperimentalSerializationApi
6+ import kotlinx.serialization.SerialName
7+ import kotlinx.serialization.Serializable
8+ import kotlinx.serialization.json.Json
9+ import kotlinx.serialization.json.JsonArray
10+ import kotlinx.serialization.json.JsonClassDiscriminator
11+ import kotlinx.serialization.json.JsonElement
12+ import kotlinx.serialization.json.JsonNamingStrategy
13+ import kotlinx.serialization.json.JsonNull
14+ import kotlinx.serialization.json.JsonObject
15+ import kotlinx.serialization.json.decodeFromJsonElement
16+ import kotlinx.serialization.json.jsonObject
17+ import kotlinx.serialization.json.jsonPrimitive
18+ import kotlinx.serialization.json.longOrNull
19+
20+ // The desktop's remote protocol (src/remote/mod.rs): JSON over WebSocket text
21+ // messages, enums tagged by `type`, fields in snake_case. A phone is a client
22+ // like another desktop: it says hello with the host's key, gets the projects
23+ // and threads, subscribes to the threads it opens and acts on them.
24+
25+ /** The port a host listens on unless told otherwise. */
26+ const val DEFAULT_PORT = 47823
27+
28+ /** Bumped by the desktop when the messages change incompatibly. */
29+ const val PROTOCOL = 1
30+
31+ val WireJson = Json {
32+ ignoreUnknownKeys = true
33+ explicitNulls = false
34+ encodeDefaults = true
35+ coerceInputValues = true
36+ namingStrategy = JsonNamingStrategy.SnakeCase
37+ classDiscriminator = "type"
38+ }
39+
40+ /** A thread as the sidebar needs it. Times are milliseconds since the epoch. */
41+ @Serializable
42+ data class ThreadSummary(
43+ val id: String = "",
44+ val project: String = "",
45+ val title: String = "",
46+ val provider: String = "claude",
47+ val instance: String = "",
48+ val model: String? = null,
49+ val permissionMode: String = "default",
50+ val reasoning: String = "default",
51+ val fastMode: Boolean = false,
52+ /** `starting`, `idle`, `working` or `exited`. */
53+ val status: String = "idle",
54+ /** A tool call or question waits for the user. */
55+ val attention: Boolean = false,
56+ val settled: Boolean = false,
57+ val pinned: Boolean = false,
58+ val snoozedUntil: Long? = null,
59+ val created: Long = 0,
60+ val updated: Long = 0,
61+ val lastMessage: Long = 0,
62+ val branch: String? = null,
63+ val totalCost: Double = 0.0,
64+ val hasItems: Boolean = false,
65+ /** For a subagent's thread, the thread that delegated its task. */
66+ val parent: String? = null,
67+ )
68+
69+ @Serializable
70+ data class QueuedMessage(val text: String = "", val attachments: List<String> = emptyList())
71+
72+ @Serializable
73+ data class PullRequest(val number: Long = 0, val url: String = "", val state: String = "OPEN")
74+
75+ @Serializable
76+ data class FileChange(val path: String = "", val added: Long? = null, val removed: Long? = null)
77+
78+ @Serializable
79+ data class TurnChanges(val base: String = "", val head: String = "", val files: List<FileChange> = emptyList()) {
80+ val added get() = files.sumOf { it.added ?: 0 }
81+ val removed get() = files.sumOf { it.removed ?: 0 }
82+ }
83+
84+ /** A command, monitor or subagent the host's agent left running. */
85+ @Serializable
86+ data class RemoteTask(val taskId: String = "", val description: String = "", val elapsedMs: Long = 0)
87+
88+ /** The rest of an open thread's state, sent with its transcript. */
89+ @Serializable
90+ data class ThreadDetail(
91+ val cwd: String = "",
92+ val activeModel: String? = null,
93+ val thinking: Boolean = false,
94+ val preparing: Boolean = false,
95+ val turnElapsedMs: Long? = null,
96+ val contextTokens: Long? = null,
97+ val contextWindow: Long? = null,
98+ val queue: List<QueuedMessage> = emptyList(),
99+ val limitResets: Long? = null,
100+ val resumeAt: Long? = null,
101+ val pendingWorktree: Boolean = false,
102+ val pullRequest: PullRequest? = null,
103+ /** `[turn end index, changes]` pairs. */
104+ val turnChanges: List<JsonArray> = emptyList(),
105+ val backgroundTasks: List<RemoteTask> = emptyList(),
106+ ) {
107+ /** What each finished turn changed, by its `TurnEnd` item's index. */
108+ fun changesByTurn(): Map<Int, TurnChanges> = turnChanges.mapNotNull { pair ->
109+ val ix = pair.getOrNull(0)?.jsonPrimitive?.longOrNull?.toInt() ?: return@mapNotNull null
110+ val changes = pair.getOrNull(1)?.let { WireJson.decodeFromJsonElement<TurnChanges>(it) } ?: return@mapNotNull null
111+ ix to changes
112+ }.toMap()
113+ }
114+
115+ @Serializable
116+ data class ProjectInfo(
117+ val path: String = "",
118+ val name: String = "",
119+ /** `null` outside a git repository. */
120+ val branch: String? = null,
121+ val branches: List<String> = emptyList(),
122+ val git: Boolean = false,
123+ val added: Long = 0,
124+ val removed: Long = 0,
125+ )
126+
127+ /** One agent among several a call started. */
128+ @Serializable
129+ data class SubagentMember(
130+ val id: String = "",
131+ val label: String = "",
132+ val kind: String? = null,
133+ /** `running`, `completed`, `failed` or `stopped`. */
134+ val status: String = "running",
135+ val started: Long = 0,
136+ val finished: Long? = null,
137+ val tokens: Long? = null,
138+ val toolUses: Int = 0,
139+ /** `[tool name, input]`. */
140+ val tool: JsonArray? = null,
141+ val summary: String? = null,
142+ )
143+
144+ @Serializable
145+ data class SubagentProgress(
146+ val started: Long = 0,
147+ val finished: Long? = null,
148+ val tokens: Long? = null,
149+ /** Its latest tool call: `[tool name, input]`. */
150+ val tool: JsonArray? = null,
151+ val summary: String? = null,
152+ val members: List<SubagentMember> = emptyList(),
153+ )
154+
155+ /** One entry of a transcript, as the host saves and sends it. */
156+ @Serializable
157+ sealed class SavedItem {
158+ @Serializable
159+ @SerialName("user")
160+ data class User(val text: String = "", val attachments: List<String> = emptyList(), val at: Long = 0) : SavedItem()
161+
162+ @Serializable
163+ @SerialName("assistant")
164+ data class Assistant(val text: String = "") : SavedItem()
165+
166+ @Serializable
167+ @SerialName("tool")
168+ data class Tool(
169+ val id: String = "",
170+ val name: String = "",
171+ val input: JsonElement = JsonNull,
172+ val result: String? = null,
173+ /** `done`, `running` or `error`. */
174+ val state: String = "done",
175+ /** `pending`, `allowed`, `denied` or `cancelled`, if it asked for permission. */
176+ val permission: String? = null,
177+ val suggestions: List<JsonElement> = emptyList(),
178+ val subagentToolCount: Int = 0,
179+ /** An AskUserQuestion form's answers so far, one per question. */
180+ val answers: List<String?>? = null,
181+ /** `running`, `completed`, `failed` or `stopped`, if it ran in the background. */
182+ val background: String? = null,
183+ val subagent: SubagentProgress? = null,
184+ ) : SavedItem()
185+
186+ @Serializable
187+ @SerialName("notice")
188+ data class Notice(val error: Boolean = false, val text: String = "") : SavedItem()
189+
190+ @Serializable
191+ @SerialName("task_event")
192+ data class TaskEvent(val summary: String = "", val status: String = "") : SavedItem()
193+
194+ @Serializable
195+ @SerialName("question")
196+ data class Question(
197+ val title: String = "",
198+ val message: String? = null,
199+ val options: List<String> = emptyList(),
200+ val confirm: Boolean = false,
201+ val answer: String? = null,
202+ ) : SavedItem()
203+
204+ @Serializable
205+ @SerialName("turn_end")
206+ data class TurnEnd(val durationMs: Long = 0, val cost: Double? = null, val at: Long = 0) : SavedItem()
207+ }
208+
209+ /** A file sent along with a message, base64. */
210+ @Serializable
211+ data class Upload(val name: String, val data: String)
212+
213+ /** What a client asks of a thread on the host. */
214+ @Serializable
215+ @JsonClassDiscriminator("action")
216+ sealed class ThreadAction {
217+ @Serializable @SerialName("send")
218+ data class Send(val text: String, val attachments: List<Upload> = emptyList()) : ThreadAction()
219+
220+ @Serializable @SerialName("queue")
221+ data class Queue(val text: String, val attachments: List<Upload> = emptyList()) : ThreadAction()
222+
223+ @Serializable @SerialName("steer_queued")
224+ data class SteerQueued(val ix: Int) : ThreadAction()
225+
226+ @Serializable @SerialName("remove_queued")
227+ data class RemoveQueued(val ix: Int) : ThreadAction()
228+
229+ @Serializable @SerialName("interrupt")
230+ data object Interrupt : ThreadAction()
231+
232+ /** `decision`: `allow`, `always` or `deny`. */
233+ @Serializable @SerialName("permission")
234+ data class Permission(val ix: Int, val decision: String) : ThreadAction()
235+
236+ @Serializable @SerialName("answer")
237+ data class Answer(val ix: Int, val option: Int) : ThreadAction()
238+
239+ /** Answers an AskUserQuestion form, one answer per question. */
240+ @Serializable @SerialName("ask_submit")
241+ data class AskSubmit(val ix: Int, val answers: List<String>) : ThreadAction()
242+
243+ @Serializable @SerialName("ask_skip")
244+ data class AskSkip(val ix: Int) : ThreadAction()
245+
246+ /** Sends a running command or subagent to the background. */
247+ @Serializable @SerialName("background")
248+ data class Background(val ix: Int) : ThreadAction()
249+
250+ @Serializable @SerialName("stop_task")
251+ data class StopTask(val taskId: String) : ThreadAction()
252+
253+ @Serializable @SerialName("set_model")
254+ data class SetModel(val model: String?) : ThreadAction()
255+
256+ @Serializable @SerialName("set_permission_mode")
257+ data class SetPermissionMode(val mode: String) : ThreadAction()
258+
259+ @Serializable @SerialName("set_reasoning")
260+ data class SetReasoning(val reasoning: String) : ThreadAction()
261+
262+ @Serializable @SerialName("set_fast_mode")
263+ data class SetFastMode(val on: Boolean) : ThreadAction()
264+
265+ @Serializable @SerialName("settle")
266+ data object Settle : ThreadAction()
267+
268+ @Serializable @SerialName("unsettle")
269+ data object Unsettle : ThreadAction()
270+
271+ @Serializable @SerialName("pin")
272+ data class Pin(val pinned: Boolean) : ThreadAction()
273+
274+ @Serializable @SerialName("snooze")
275+ data class Snooze(val until: Long?) : ThreadAction()
276+
277+ @Serializable @SerialName("rename")
278+ data class Rename(val title: String) : ThreadAction()
279+
280+ @Serializable @SerialName("cancel_resume")
281+ data object CancelResume : ThreadAction()
282+
283+ @Serializable @SerialName("schedule_resume")
284+ data class ScheduleResume(val at: Long) : ThreadAction()
285+
286+ @Serializable @SerialName("delete")
287+ data object Delete : ThreadAction()
288+ }
289+
290+ /** What the Add project palette picked. */
291+ @Serializable
292+ @JsonClassDiscriminator("kind")
293+ sealed class ProjectSource {
294+ @Serializable @SerialName("folder")
295+ data class Folder(val path: String) : ProjectSource()
296+
297+ @Serializable @SerialName("new")
298+ data class New(val name: String) : ProjectSource()
299+
300+ @Serializable @SerialName("url")
301+ data class Url(val url: String) : ProjectSource()
302+
303+ /** `forge`: `git_hub`, `git_lab`, `azure_dev_ops`, `bitbucket` or `forgejo`. */
304+ @Serializable @SerialName("forge")
305+ data class Forge(val forge: String, val repo: String) : ProjectSource()
306+ }
307+
308+ /** Changes to the host's environment settings or its projects' overrides. */
309+ @Serializable
310+ data class SettingsPatch(
311+ val values: JsonObject = JsonObject(emptyMap()),
312+ val projects: Map<String, JsonObject> = emptyMap(),
313+ )
314+
315+ @Serializable
316+ sealed class ClientMessage {
317+ @Serializable @SerialName("hello")
318+ data class Hello(val key: String, val name: String, val protocol: Int = PROTOCOL) : ClientMessage()
319+
320+ @Serializable @SerialName("subscribe")
321+ data class Subscribe(val thread: String) : ClientMessage()
322+
323+ @Serializable @SerialName("unsubscribe")
324+ data class Unsubscribe(val thread: String) : ClientMessage()
325+
326+ @Serializable @SerialName("action")
327+ data class Action(val thread: String, val action: ThreadAction) : ClientMessage()
328+
329+ /** Starts a thread in one of the host's projects with its first message. */
330+ @Serializable @SerialName("create")
331+ data class Create(
332+ val reference: Long,
333+ val project: String,
334+ val instance: String,
335+ val provider: String,
336+ val model: String?,
337+ val permissionMode: String,
338+ val reasoning: String,
339+ val fastMode: Boolean,
340+ val worktree: Boolean,
341+ val base: String?,
342+ val text: String,
343+ val attachments: List<Upload> = emptyList(),
344+ ) : ClientMessage()
345+
346+ @Serializable @SerialName("update_settings")
347+ data class UpdateSettings(val patch: SettingsPatch) : ClientMessage()
348+
349+ /** Lists a folder on the host, as typed: `~/Work/`. */
350+ @Serializable @SerialName("list_folders")
351+ data class ListFolders(val path: String) : ClientMessage()
352+
353+ @Serializable @SerialName("add_project")
354+ data class AddProject(val source: ProjectSource) : ClientMessage()
355+
356+ @Serializable @SerialName("terminal_open")
357+ data class TerminalOpen(val terminal: Long, val thread: String?, val project: String, val columns: Int, val lines: Int) : ClientMessage()
358+
359+ /** Typed bytes, base64. */
360+ @Serializable @SerialName("terminal_input")
361+ data class TerminalInput(val terminal: Long, val data: String) : ClientMessage()
362+
363+ @Serializable @SerialName("terminal_resize")
364+ data class TerminalResize(val terminal: Long, val columns: Int, val lines: Int) : ClientMessage()
365+
366+ @Serializable @SerialName("terminal_close")
367+ data class TerminalClose(val terminal: Long) : ClientMessage()
368+
369+ /** Something for the host's devices; `call` is a `DeviceCall` (tagged by `call`). */
370+ @Serializable @SerialName("device")
371+ data class Device(val request: Long, val call: JsonObject) : ClientMessage()
372+
373+ /** The host's agents and their models. Older hosts don't answer. */
374+ @Serializable @SerialName("catalog")
375+ data object Catalog : ClientMessage()
376+
377+ /** The diff between two of a thread's snapshots. Older hosts don't answer. */
378+ @Serializable @SerialName("diff")
379+ data class Diff(
380+ val request: Long,
381+ val thread: String,
382+ val base: String,
383+ val head: String,
384+ val path: String?,
385+ val ignoreWhitespace: Boolean,
386+ ) : ClientMessage()
387+ }
388+
389+ @Serializable
390+ data class Folder(val name: String = "", val git: Boolean = false)
391+
392+ @Serializable
393+ data class RemoteModel(
394+ val id: String = "",
395+ val resolved: String = "",
396+ val name: String = "",
397+ val detail: String = "",
398+ val legacy: Boolean = false,
399+ val efforts: List<String> = emptyList(),
400+ val defaultEffort: String? = null,
401+ val ultracode: Boolean = false,
402+ val fastMode: Boolean = false,
403+ val autoMode: Boolean = false,
404+ val contextWindow: Long? = null,
405+ val hidden: Boolean = false,
406+ )
407+
408+ @Serializable
409+ data class RemoteCommand(
410+ val name: String = "",
411+ val description: String = "",
412+ val argumentHint: String = "",
413+ val skill: Boolean = false,
414+ )
415+
416+ /** One agent instance on the host, say a second Claude account. */
417+ @Serializable
418+ data class RemoteInstance(
419+ val id: String = "",
420+ val provider: String = "claude",
421+ val name: String = "",
422+ val accent: String? = null,
423+ /** `ready`, `checking`, `missing`, `disabled` or `failed`. */
424+ val status: String = "ready",
425+ val account: String? = null,
426+ val plan: String? = null,
427+ val defaultModel: String? = null,
428+ val models: List<RemoteModel> = emptyList(),
429+ val commands: List<RemoteCommand> = emptyList(),
430+ )
431+
432+ @Serializable
433+ data class RemoteDiffLine(
434+ /** `hunk`, `context`, `added` or `removed`. */
435+ val kind: String = "context",
436+ val text: String = "",
437+ val oldLine: Int? = null,
438+ val newLine: Int? = null,
439+ )
440+
441+ @Serializable
442+ data class RemoteFileDiff(val path: String = "", val binary: Boolean = false, val lines: List<RemoteDiffLine> = emptyList())
443+
444+ @Serializable
445+ sealed class ServerMessage {
446+ @Serializable @SerialName("welcome")
447+ data class Welcome(val host: String = "", val version: String = "", val protocol: Int = 0) : ServerMessage()
448+
449+ @Serializable @SerialName("denied")
450+ data class Denied(val reason: String = "") : ServerMessage()
451+
452+ @Serializable @SerialName("state")
453+ data class State(val projects: List<ProjectInfo> = emptyList(), val threads: List<ThreadSummary> = emptyList()) : ServerMessage()
454+
455+ /** A subscribed thread changed: its transcript from item `from` on; `len` items in all. */
456+ @Serializable @SerialName("thread")
457+ data class Thread(
458+ val summary: ThreadSummary = ThreadSummary(),
459+ val detail: ThreadDetail = ThreadDetail(),
460+ val from: Int = 0,
461+ val len: Int = 0,
462+ val items: List<SavedItem> = emptyList(),
463+ ) : ServerMessage()
464+
465+ @Serializable @SerialName("created")
466+ data class Created(val reference: Long = 0, val thread: String = "") : ServerMessage()
467+
468+ @Serializable @SerialName("error")
469+ data class Error(val message: String = "") : ServerMessage()
470+
471+ /** The host's environment settings and project overrides, raw. */
472+ @Serializable @SerialName("settings")
473+ data class Settings(val values: JsonObject = JsonObject(emptyMap())) : ServerMessage()
474+
475+ @Serializable @SerialName("folders")
476+ data class Folders(
477+ val path: String = "",
478+ val folders: List<Folder> = emptyList(),
479+ val truncated: Boolean = false,
480+ val error: String? = null,
481+ ) : ServerMessage()
482+
483+ @Serializable @SerialName("project_added")
484+ data class ProjectAdded(val path: String = "") : ServerMessage()
485+
486+ @Serializable @SerialName("terminal_output")
487+ data class TerminalOutput(val terminal: Long = 0, val data: String = "") : ServerMessage()
488+
489+ @Serializable @SerialName("terminal_exited")
490+ data class TerminalExited(val terminal: Long = 0, val error: String? = null) : ServerMessage()
491+
492+ @Serializable @SerialName("device_reply")
493+ data class DeviceReply(val request: Long = 0, val ok: JsonElement? = null, val error: String? = null) : ServerMessage()
494+
495+ @Serializable @SerialName("device_state")
496+ data class DeviceState(val state: JsonObject = JsonObject(emptyMap())) : ServerMessage()
497+
498+ @Serializable @SerialName("device_opened")
499+ data class DeviceOpened(val thread: String = "", val device: JsonObject = JsonObject(emptyMap())) : ServerMessage()
500+
501+ @Serializable @SerialName("device_closed")
502+ data class DeviceClosed(val thread: String = "", val device: String? = null) : ServerMessage()
503+
504+ @Serializable @SerialName("catalog")
505+ data class Catalog(val instances: List<RemoteInstance> = emptyList(), val favorites: List<String> = emptyList()) : ServerMessage()
506+
507+ @Serializable @SerialName("diff_reply")
508+ data class DiffReply(val request: Long = 0, val files: List<RemoteFileDiff> = emptyList(), val error: String? = null) : ServerMessage()
509+ }
510+
511+ /** A message from the host, or `null` for one this app doesn't know. */
512+ fun parseServerMessage(text: String): ServerMessage? = try {
513+ WireJson.decodeFromString(ServerMessage.serializer(), text)
514+ } catch (_: Exception) {
515+ null
516+ }
517+
518+ fun encode(message: ClientMessage): String = WireJson.encodeToString(ClientMessage.serializer(), message)
519+
520+ /** A pairing code bundles a host's address and key: `192.168.2.10:47823/<key>`. */
521+ fun parsePairing(code: String): Pair<String, String?> {
522+ val trimmed = code.trim().removePrefix("ws://").removePrefix("vectrum://")
523+ val slash = trimmed.indexOf('/')
524+ if (slash < 0) return trimmed to null
525+ val key = trimmed.substring(slash + 1).trim()
526+ return trimmed.substring(0, slash) to key.ifEmpty { null }
527+ }
528+
529+ /** `host` or `host:port`, with the default port filled in. */
530+ fun withPort(address: String): String {
531+ val trimmed = address.trim()
532+ val colon = trimmed.lastIndexOf(':')
533+ val hasPort = colon > 0 && trimmed.substring(colon + 1).toIntOrNull() != null &&
534+ (!trimmed.substring(0, colon).contains(':') || trimmed.substring(0, colon).endsWith("]"))
535+ return if (hasPort) trimmed else "$trimmed:$DEFAULT_PORT"
536+ }
537+
538+ /** A tool input's string field, if it has one. */
539+ fun JsonElement.str(key: String): String? =
540+ (this as? JsonObject)?.get(key)?.let { runCatching { it.jsonPrimitive.content }.getOrNull() }
541+
542+ fun JsonElement.obj(): JsonObject? = runCatching { jsonObject }.getOrNull()
+94−0android/app/src/main/java/cc/vectrum/code/term/Keys.ktaddedView File
@@ -0,0 +1,94 @@
1+ package cc.vectrum.code.term
2+
3+ /** Keys that send escape sequences rather than text. */
4+ enum class TermKey {
5+ Enter, Tab, Escape, Backspace, Up, Down, Left, Right, Home, End, Insert, Delete, PageUp, PageDown,
6+ F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12,
7+ }
8+
9+ /**
10+ * What a key sends to the shell, as xterm encodes it: the desktop's
11+ * `key_bytes` (src/terminal.rs). Cursor keys follow DECCKM ([appCursor]).
12+ */
13+ fun keyBytes(key: TermKey, appCursor: Boolean, ctrl: Boolean = false, alt: Boolean = false, shift: Boolean = false): ByteArray {
14+ // xterm's modifier parameter: 1 + Shift + 2·Alt + 4·Ctrl.
15+ val param = 1 + (if (shift) 1 else 0) + (if (alt) 2 else 0) + (if (ctrl) 4 else 0)
16+ fun escaped(text: String) = ((if (alt) "\u001b" else "") + text).toByteArray()
17+ fun cursor(letter: Char) = when {
18+ param == 1 && appCursor -> "\u001bO$letter"
19+ param == 1 -> "\u001b[$letter"
20+ else -> "\u001b[1;$param$letter"
21+ }.toByteArray()
22+ fun tilde(code: Int) = (if (param == 1) "\u001b[$code~" else "\u001b[$code;$param~").toByteArray()
23+ return when (key) {
24+ TermKey.Enter -> escaped("\r")
25+ TermKey.Tab -> if (shift) "\u001b[Z".toByteArray() else escaped("\t")
26+ TermKey.Escape -> escaped("\u001b")
27+ TermKey.Backspace -> if (ctrl) escaped("\u0008") else escaped("\u007f")
28+ TermKey.Up -> cursor('A')
29+ TermKey.Down -> cursor('B')
30+ TermKey.Right -> cursor('C')
31+ TermKey.Left -> cursor('D')
32+ TermKey.Home -> cursor('H')
33+ TermKey.End -> cursor('F')
34+ TermKey.Insert -> tilde(2)
35+ TermKey.Delete -> tilde(3)
36+ TermKey.PageUp -> tilde(5)
37+ TermKey.PageDown -> tilde(6)
38+ TermKey.F1, TermKey.F2, TermKey.F3, TermKey.F4 -> {
39+ val letter = 'P' + (key.ordinal - TermKey.F1.ordinal)
40+ (if (param == 1) "\u001bO$letter" else "\u001b[1;$param$letter").toByteArray()
41+ }
42+ TermKey.F5 -> tilde(15)
43+ TermKey.F6 -> tilde(17)
44+ TermKey.F7 -> tilde(18)
45+ TermKey.F8 -> tilde(19)
46+ TermKey.F9 -> tilde(20)
47+ TermKey.F10 -> tilde(21)
48+ TermKey.F11 -> tilde(23)
49+ TermKey.F12 -> tilde(24)
50+ }
51+ }
52+
53+ /**
54+ * Typed text, with Ctrl and Alt applied to it as the desktop applies them
55+ * to a key: Ctrl turns a letter into its control code, Alt puts ESC first.
56+ */
57+ fun textBytes(text: String, ctrl: Boolean = false, alt: Boolean = false): ByteArray {
58+ if (text.isEmpty()) return ByteArray(0)
59+ var body = text.replace("\r\n", "\r").replace('\n', '\r')
60+ if (ctrl) {
61+ val first = body.codePointAt(0)
62+ val control = controlCode(first)
63+ if (control != null) body = String(Character.toChars(control)) + body.substring(Character.charCount(first))
64+ }
65+ return ((if (alt) "\u001b" else "") + body).toByteArray()
66+ }
67+
68+ /** The control code Ctrl and a character make, as the desktop's table has them. */
69+ fun controlCode(c: Int): Int? {
70+ val lower = if (c in 'A'.code..'Z'.code) c + 32 else c
71+ return when (lower) {
72+ in 'a'.code..'z'.code -> lower - 'a'.code + 1
73+ ' '.code, '@'.code, '2'.code -> 0
74+ '['.code, '3'.code -> 0x1b
75+ '\\'.code, '4'.code -> 0x1c
76+ ']'.code, '5'.code -> 0x1d
77+ '^'.code, '6'.code -> 0x1e
78+ '_'.code, '-'.code, '/'.code, '7'.code -> 0x1f
79+ '?'.code, '8'.code -> 0x7f
80+ else -> null
81+ }
82+ }
83+
84+ /**
85+ * What a paste sends: line breaks as Enter, and inside bracketed paste
86+ * markers when the program asked for them (with any markers in the text
87+ * taken out, so a paste can't end itself early).
88+ */
89+ fun pasteBytes(text: String, bracketed: Boolean): ByteArray =
90+ if (bracketed) {
91+ ("\u001b[200~" + text.replace("\u001b", "") + "\u001b[201~").toByteArray()
92+ } else {
93+ text.replace("\r\n", "\r").replace('\n', '\r').toByteArray()
94+ }
+105−0android/app/src/main/java/cc/vectrum/code/term/Line.ktaddedView File
@@ -0,0 +1,105 @@
1+ package cc.vectrum.code.term
2+
3+ /**
4+ * A cell's color: [DEFAULT], one of the 256 indexed colors, or 24-bit RGB,
5+ * packed in an `Int` so a line's colors are plain arrays.
6+ */
7+ object TermColor {
8+ const val DEFAULT = 0
9+ private const val INDEXED = 1 shl 24
10+ private const val RGB = 2 shl 24
11+
12+ fun indexed(index: Int) = INDEXED or (index and 0xff)
13+
14+ fun rgb(r: Int, g: Int, b: Int) = RGB or ((r and 0xff) shl 16) or ((g and 0xff) shl 8) or (b and 0xff)
15+
16+ fun isIndexed(color: Int) = color ushr 24 == 1
17+
18+ fun isRgb(color: Int) = color ushr 24 == 2
19+
20+ /** The index of an indexed color, or the `0xrrggbb` of an RGB one. */
21+ fun value(color: Int) = color and 0xffffff
22+ }
23+
24+ /** A cell's attributes, as bits. */
25+ object Attr {
26+ const val BOLD = 1
27+ const val DIM = 2
28+ const val ITALIC = 4
29+ const val UNDERLINE = 8
30+ const val INVERSE = 16
31+ const val HIDDEN = 32
32+ const val STRIKE = 64
33+
34+ /** The first half of a double-width character. */
35+ const val WIDE = 128
36+
37+ /** The second half of a double-width character: nothing to draw. */
38+ const val SPACER = 256
39+ }
40+
41+ /** One row of cells: code points (0 for blank), colors and attributes. */
42+ class Line(columns: Int) {
43+ var chars = IntArray(columns)
44+ private set
45+ var fg = IntArray(columns)
46+ private set
47+ var bg = IntArray(columns)
48+ private set
49+ var attrs = IntArray(columns)
50+ private set
51+
52+ /** The text goes on in the next line: it wrapped here rather than broke. */
53+ var wrapped = false
54+
55+ val columns: Int get() = chars.size
56+
57+ fun resize(columns: Int) {
58+ val old = chars.size
59+ if (columns == old) return
60+ chars = chars.copyOf(columns)
61+ fg = fg.copyOf(columns)
62+ bg = bg.copyOf(columns)
63+ attrs = attrs.copyOf(columns)
64+ // A wide character cut in half by the new edge goes.
65+ if (columns > 0 && attrs[columns - 1] and Attr.WIDE != 0) {
66+ chars[columns - 1] = 0
67+ attrs[columns - 1] = 0
68+ }
69+ if (columns < old) wrapped = false
70+ }
71+
72+ /** Blanks `from until to` in background [background]. */
73+ fun clear(from: Int, to: Int, background: Int) {
74+ val end = to.coerceAtMost(chars.size)
75+ for (i in from.coerceAtLeast(0) until end) {
76+ chars[i] = 0
77+ fg[i] = TermColor.DEFAULT
78+ bg[i] = background
79+ attrs[i] = 0
80+ }
81+ }
82+
83+ fun copyCell(from: Int, to: Int) {
84+ chars[to] = chars[from]
85+ fg[to] = fg[from]
86+ bg[to] = bg[from]
87+ attrs[to] = attrs[from]
88+ }
89+
90+ /** The row's text, blanks as spaces; trailing blanks trimmed when asked. */
91+ fun text(trim: Boolean = true): String {
92+ val builder = StringBuilder(chars.size)
93+ for (i in chars.indices) {
94+ if (attrs[i] and Attr.SPACER != 0) continue
95+ val c = chars[i]
96+ if (c == 0) builder.append(' ') else builder.appendCodePoint(c)
97+ }
98+ return if (trim) builder.trimEnd().toString() else builder.toString()
99+ }
100+
101+ fun isBlank(): Boolean {
102+ for (i in chars.indices) if (chars[i] != 0 || bg[i] != TermColor.DEFAULT) return false
103+ return true
104+ }
105+ }
+975−0android/app/src/main/java/cc/vectrum/code/term/Terminal.ktaddedView File
@@ -0,0 +1,975 @@
1+ package cc.vectrum.code.term
2+
3+ /** How the cursor is drawn, as `DECSCUSR` sets it. */
4+ enum class CursorShape { Block, Underline, Beam }
5+
6+ /**
7+ * A compact VT/xterm emulator: the screen a shell draws on. It parses what
8+ * the shell prints (UTF-8, C0 controls, ESC, CSI, OSC and DCS sequences)
9+ * into a grid of cells with an alternate screen and a scrollback, and
10+ * answers the queries a shell asks (cursor position, device attributes,
11+ * colors) through [respond]. The desktop does this with
12+ * `alacritty_terminal`; this keeps to what shells and full-screen programs
13+ * use.
14+ *
15+ * Not thread-safe: feed, resize and read it on one thread.
16+ */
17+ class Terminal(
18+ columns: Int,
19+ rows: Int,
20+ private val scrollbackLimit: Int = SCROLLBACK,
21+ /** Bytes the terminal sends back to the shell, as answers to queries. */
22+ private val respond: (ByteArray) -> Unit = {},
23+ /** The `0xrrggbb` of an indexed color (256 is the foreground, 257 the background, 258 the cursor), for color queries. */
24+ private val colorOf: (Int) -> Int? = { null },
25+ ) {
26+ var columns = columns.coerceAtLeast(1)
27+ private set
28+ var rows = rows.coerceAtLeast(1)
29+ private set
30+
31+ private var main = ArrayList<Line>()
32+ private var alternate = ArrayList<Line>()
33+
34+ /** The rows on screen, top to bottom. */
35+ private var screen = main
36+
37+ /** Lines scrolled off the top of the main screen, oldest first. */
38+ private val history = ArrayDeque<Line>()
39+
40+ /** The alternate screen shows: full-screen programs draw there. */
41+ var altScreen = false
42+ private set
43+
44+ // ---- cursor and pen ----
45+
46+ var cursorRow = 0
47+ private set
48+ var cursorColumn = 0
49+ private set
50+
51+ /** A character went into the last column; the next one wraps first. */
52+ private var pendingWrap = false
53+ private var fg = TermColor.DEFAULT
54+ private var bg = TermColor.DEFAULT
55+ private var attrs = 0
56+ private var lastPrinted = ' '.code
57+
58+ private class Saved(
59+ val row: Int,
60+ val column: Int,
61+ val fg: Int,
62+ val bg: Int,
63+ val attrs: Int,
64+ val origin: Boolean,
65+ val g0: Boolean,
66+ val g1: Boolean,
67+ val shifted: Boolean,
68+ )
69+
70+ private var savedMain: Saved? = null
71+ private var savedAlternate: Saved? = null
72+
73+ // ---- modes ----
74+
75+ /** DECCKM: cursor keys send `ESC O A` rather than `ESC [ A`. */
76+ var applicationCursor = false
77+ private set
78+
79+ /** The program wants pastes between `ESC [200~` and `ESC [201~`. */
80+ var bracketedPaste = false
81+ private set
82+ var cursorVisible = true
83+ private set
84+ var cursorShape = CursorShape.Block
85+ private set
86+
87+ /** Mouse reporting the program asked for: 0 none, else 1000, 1002 or 1003. */
88+ var mouseTracking = 0
89+ private set
90+
91+ /** Mouse reports in SGR form (`?1006`). */
92+ var sgrMouse = false
93+ private set
94+
95+ /** The wheel sends arrow keys on the alternate screen (`?1007`, on by default). */
96+ var alternateScroll = true
97+ private set
98+ private var autoWrap = true
99+ private var originMode = false
100+ private var insertMode = false
101+ private var newLineMode = false
102+
103+ private var scrollTop = 0
104+ private var scrollBottom = this.rows - 1
105+ private var tabStops = BooleanArray(this.columns)
106+
107+ /** G0 and G1 are DEC line drawing rather than ASCII, and which one is shifted in. */
108+ private var g0Graphics = false
109+ private var g1Graphics = false
110+ private var shiftedOut = false
111+
112+ /** How many lines up the view is scrolled into the scrollback; 0 follows the shell. */
113+ var displayOffset = 0
114+ private set
115+
116+ // ---- parser ----
117+
118+ private enum class State { Ground, Escape, EscapeIntermediate, Csi, Osc, Ignore, IgnoreEscape, Charset }
119+
120+ private val decoder = Utf8Decoder()
121+ private var state = State.Ground
122+ private val params = IntArray(MAX_PARAMS)
123+
124+ /** Whether a parameter is a colon subparameter of the one before it. */
125+ private val colon = BooleanArray(MAX_PARAMS)
126+ private var paramCount = 0
127+ private var paramStarted = false
128+ private var privateMarker = 0
129+ private var intermediate = 0
130+ private var charsetTarget = 0
131+ private val osc = StringBuilder()
132+ private var oscEscape = false
133+
134+ init {
135+ repeat(this.rows) {
136+ main.add(Line(this.columns))
137+ alternate.add(Line(this.columns))
138+ }
139+ resetTabs()
140+ }
141+
142+ val historySize: Int get() = history.size
143+
144+ // ---- input ----
145+
146+ fun feed(bytes: ByteArray, offset: Int = 0, length: Int = bytes.size) {
147+ decoder.decode(bytes, offset, length) { process(it) }
148+ }
149+
150+ fun feed(text: String) = feed(text.toByteArray())
151+
152+ private fun process(c: Int) {
153+ // C0 controls act in the middle of most sequences, as in a VT.
154+ if (c < 0x20 && state != State.Osc && state != State.Ignore && state != State.IgnoreEscape) {
155+ when (c) {
156+ 0x1b -> {
157+ state = State.Escape
158+ intermediate = 0
159+ }
160+ 0x18, 0x1a -> state = State.Ground
161+ else -> control(c)
162+ }
163+ return
164+ }
165+ when (state) {
166+ State.Ground -> print(c)
167+ State.Escape -> escape(c)
168+ State.EscapeIntermediate -> escapeIntermediate(c)
169+ State.Charset -> {
170+ val graphics = c == '0'.code
171+ if (charsetTarget == 0) g0Graphics = graphics else if (charsetTarget == 1) g1Graphics = graphics
172+ state = State.Ground
173+ }
174+ State.Csi -> csiChar(c)
175+ State.Osc -> oscChar(c)
176+ State.Ignore -> if (c == 0x1b) state = State.IgnoreEscape else if (c == 0x07) state = State.Ground
177+ State.IgnoreEscape -> state = if (c == '\\'.code) State.Ground else State.Ignore
178+ }
179+ }
180+
181+ private fun control(c: Int) {
182+ when (c) {
183+ 0x07 -> {}
184+ 0x08 -> {
185+ pendingWrap = false
186+ if (cursorColumn > 0) cursorColumn--
187+ }
188+ 0x09 -> tab(1)
189+ 0x0a, 0x0b, 0x0c -> {
190+ lineFeed()
191+ if (newLineMode) carriageReturn()
192+ }
193+ 0x0d -> carriageReturn()
194+ 0x0e -> shiftedOut = true
195+ 0x0f -> shiftedOut = false
196+ }
197+ }
198+
199+ private fun escape(c: Int) {
200+ state = State.Ground
201+ when (c) {
202+ '['.code -> startCsi()
203+ ']'.code -> {
204+ osc.setLength(0)
205+ oscEscape = false
206+ state = State.Osc
207+ }
208+ 'P'.code, 'X'.code, '^'.code, '_'.code -> state = State.Ignore
209+ '('.code, ')'.code, '*'.code, '+'.code -> {
210+ charsetTarget = c - '('.code
211+ state = State.Charset
212+ }
213+ '7'.code -> saveCursor()
214+ '8'.code -> restoreCursor()
215+ 'D'.code -> lineFeed()
216+ 'E'.code -> {
217+ lineFeed()
218+ carriageReturn()
219+ }
220+ 'M'.code -> reverseIndex()
221+ 'H'.code -> if (cursorColumn < columns) tabStops[cursorColumn] = true
222+ 'c'.code -> reset()
223+ in 0x20..0x2f -> {
224+ intermediate = c
225+ state = State.EscapeIntermediate
226+ }
227+ // `=`, `>` (keypad modes), `\` (a lone string terminator) and the rest: nothing to do.
228+ }
229+ }
230+
231+ private fun escapeIntermediate(c: Int) {
232+ if (c in 0x20..0x2f) return
233+ state = State.Ground
234+ if (intermediate == '#'.code && c == '8'.code) {
235+ // DECALN: the screen full of Es.
236+ for (line in screen) {
237+ line.clear(0, columns, TermColor.DEFAULT)
238+ for (i in 0 until columns) line.chars[i] = 'E'.code
239+ }
240+ }
241+ }
242+
243+ // ---- printing ----
244+
245+ private fun print(c0: Int) {
246+ var c = c0
247+ val graphics = if (shiftedOut) g1Graphics else g0Graphics
248+ if (graphics && c in 0x5f..0x7e) c = DEC_GRAPHICS[c - 0x5f].code
249+ val width = charWidth(c)
250+ if (width == 0) return
251+ if (pendingWrap) {
252+ if (autoWrap) {
253+ screen[cursorRow].wrapped = true
254+ lineFeed()
255+ cursorColumn = 0
256+ }
257+ pendingWrap = false
258+ }
259+ if (width == 2 && cursorColumn == columns - 1) {
260+ if (columns < 2) return
261+ if (!autoWrap) return
262+ // It doesn't fit at the end of the line; it goes on the next.
263+ val line = screen[cursorRow]
264+ clearWide(line, cursorColumn)
265+ line.clear(cursorColumn, columns, bg)
266+ line.wrapped = true
267+ lineFeed()
268+ cursorColumn = 0
269+ }
270+ val line = screen[cursorRow]
271+ if (insertMode) insertBlanks(line, cursorColumn, width)
272+ clearWide(line, cursorColumn)
273+ if (width == 2) clearWide(line, cursorColumn + 1)
274+ line.chars[cursorColumn] = c
275+ line.fg[cursorColumn] = fg
276+ line.bg[cursorColumn] = bg
277+ line.attrs[cursorColumn] = attrs or if (width == 2) Attr.WIDE else 0
278+ if (width == 2) {
279+ line.chars[cursorColumn + 1] = 0
280+ line.fg[cursorColumn + 1] = fg
281+ line.bg[cursorColumn + 1] = bg
282+ line.attrs[cursorColumn + 1] = attrs or Attr.SPACER
283+ }
284+ lastPrinted = c
285+ if (cursorColumn + width >= columns) {
286+ cursorColumn = columns - 1
287+ pendingWrap = true
288+ } else {
289+ cursorColumn += width
290+ }
291+ }
292+
293+ /** Overwriting either half of a wide character blanks the other half. */
294+ private fun clearWide(line: Line, column: Int) {
295+ if (column !in 0 until line.columns) return
296+ val a = line.attrs[column]
297+ if (a and Attr.SPACER != 0 && column > 0) {
298+ line.chars[column - 1] = 0
299+ line.attrs[column - 1] = line.attrs[column - 1] and Attr.WIDE.inv()
300+ }
301+ if (a and Attr.WIDE != 0 && column + 1 < line.columns) {
302+ line.chars[column + 1] = 0
303+ line.attrs[column + 1] = line.attrs[column + 1] and Attr.SPACER.inv()
304+ }
305+ line.attrs[column] = a and (Attr.WIDE or Attr.SPACER).inv()
306+ }
307+
308+ private fun carriageReturn() {
309+ cursorColumn = 0
310+ pendingWrap = false
311+ }
312+
313+ private fun lineFeed() {
314+ pendingWrap = false
315+ if (cursorRow == scrollBottom) {
316+ scrollUp(1)
317+ } else if (cursorRow < rows - 1) {
318+ cursorRow++
319+ }
320+ }
321+
322+ private fun reverseIndex() {
323+ pendingWrap = false
324+ if (cursorRow == scrollTop) scrollDown(1) else if (cursorRow > 0) cursorRow--
325+ }
326+
327+ private fun tab(count: Int) {
328+ pendingWrap = false
329+ repeat(count) {
330+ var column = cursorColumn + 1
331+ while (column < columns - 1 && !tabStops[column]) column++
332+ cursorColumn = column.coerceAtMost(columns - 1)
333+ }
334+ }
335+
336+ private fun backTab(count: Int) {
337+ pendingWrap = false
338+ repeat(count) {
339+ var column = cursorColumn - 1
340+ while (column > 0 && !tabStops[column]) column--
341+ cursorColumn = column.coerceAtLeast(0)
342+ }
343+ }
344+
345+ private fun blankLine() = Line(columns).also { if (bg != TermColor.DEFAULT) it.clear(0, columns, bg) }
346+
347+ /** Scrolls the region up: its top lines go, into the scrollback when it's the main screen's top. */
348+ private fun scrollUp(count: Int, top: Int = scrollTop, bottom: Int = scrollBottom) {
349+ val n = count.coerceAtMost(bottom - top + 1)
350+ repeat(n) {
351+ val gone = screen.removeAt(top)
352+ if (top == 0 && !altScreen) {
353+ history.addLast(gone)
354+ if (history.size > scrollbackLimit) history.removeFirst() else if (displayOffset > 0) displayOffset++
355+ displayOffset = displayOffset.coerceAtMost(history.size)
356+ }
357+ screen.add(bottom, blankLine())
358+ }
359+ }
360+
361+ private fun scrollDown(count: Int, top: Int = scrollTop, bottom: Int = scrollBottom) {
362+ val n = count.coerceAtMost(bottom - top + 1)
363+ repeat(n) {
364+ screen.removeAt(bottom)
365+ screen.add(top, blankLine())
366+ }
367+ }
368+
369+ private fun insertBlanks(line: Line, at: Int, count: Int) {
370+ val n = count.coerceAtMost(columns - at)
371+ clearWide(line, at)
372+ for (i in columns - 1 downTo at + n) line.copyCell(i - n, i)
373+ line.clear(at, at + n, bg)
374+ if (line.attrs[columns - 1] and Attr.WIDE != 0) line.clear(columns - 1, columns, bg)
375+ }
376+
377+ // ---- CSI ----
378+
379+ private fun startCsi() {
380+ state = State.Csi
381+ paramCount = 0
382+ paramStarted = false
383+ privateMarker = 0
384+ intermediate = 0
385+ params.fill(-1)
386+ colon.fill(false)
387+ }
388+
389+ private fun csiChar(c: Int) {
390+ when (c) {
391+ in '0'.code..'9'.code -> {
392+ if (paramCount == 0) paramCount = 1
393+ val ix = paramCount - 1
394+ if (ix < MAX_PARAMS) {
395+ val current = params[ix].coerceAtLeast(0)
396+ params[ix] = (current * 10 + (c - '0'.code)).coerceAtMost(65535)
397+ }
398+ paramStarted = true
399+ }
400+ ';'.code, ':'.code -> {
401+ if (paramCount == 0) paramCount = 1
402+ if (paramCount < MAX_PARAMS) {
403+ colon[paramCount] = c == ':'.code
404+ paramCount++
405+ }
406+ }
407+ '?'.code, '>'.code, '='.code, '<'.code -> if (paramCount == 0) privateMarker = c
408+ in 0x20..0x2f -> intermediate = c
409+ in 0x40..0x7e -> {
410+ state = State.Ground
411+ csi(c)
412+ }
413+ else -> state = State.Ground
414+ }
415+ }
416+
417+ private fun param(ix: Int, default: Int): Int {
418+ if (ix >= paramCount) return default
419+ val value = params[ix]
420+ return if (value <= 0) default else value
421+ }
422+
423+ /** A parameter where 0 is meaningful. */
424+ private fun raw(ix: Int, default: Int): Int = if (ix >= paramCount || params[ix] < 0) default else params[ix]
425+
426+ private fun csi(final: Int) {
427+ if (privateMarker == '?'.code) {
428+ when (final) {
429+ 'h'.code -> for (i in 0 until paramCount.coerceAtLeast(1)) privateMode(raw(i, 0), true)
430+ 'l'.code -> for (i in 0 until paramCount.coerceAtLeast(1)) privateMode(raw(i, 0), false)
431+ 'n'.code -> if (raw(0, 0) == 6) reportCursor(true)
432+ }
433+ return
434+ }
435+ if (privateMarker == '>'.code) {
436+ if (final == 'c'.code) reply("\u001b[>0;100;0c")
437+ return
438+ }
439+ if (privateMarker != 0) return
440+ if (intermediate == ' '.code) {
441+ if (final == 'q'.code) {
442+ cursorShape = when (raw(0, 0)) {
443+ 3, 4 -> CursorShape.Underline
444+ 5, 6 -> CursorShape.Beam
445+ else -> CursorShape.Block
446+ }
447+ }
448+ return
449+ }
450+ if (intermediate == '!'.code) {
451+ if (final == 'p'.code) softReset()
452+ return
453+ }
454+ if (intermediate != 0) return
455+ when (final) {
456+ '@'.code -> {
457+ pendingWrap = false
458+ insertBlanks(screen[cursorRow], cursorColumn, param(0, 1))
459+ }
460+ 'A'.code -> moveTo(cursorRow - param(0, 1), cursorColumn, clampToRegion = true)
461+ 'B'.code, 'e'.code -> moveTo(cursorRow + param(0, 1), cursorColumn, clampToRegion = true)
462+ 'C'.code, 'a'.code -> moveTo(cursorRow, cursorColumn + param(0, 1))
463+ 'D'.code -> moveTo(cursorRow, cursorColumn - param(0, 1))
464+ 'E'.code -> moveTo(cursorRow + param(0, 1), 0, clampToRegion = true)
465+ 'F'.code -> moveTo(cursorRow - param(0, 1), 0, clampToRegion = true)
466+ 'G'.code, '`'.code -> moveTo(cursorRow, param(0, 1) - 1)
467+ 'H'.code, 'f'.code -> {
468+ val row = param(0, 1) - 1
469+ val column = param(1, 1) - 1
470+ if (originMode) moveTo(row + scrollTop, column, clampToRegion = true) else moveTo(row, column)
471+ }
472+ 'd'.code -> {
473+ val row = param(0, 1) - 1
474+ if (originMode) moveTo(row + scrollTop, cursorColumn, clampToRegion = true) else moveTo(row, cursorColumn)
475+ }
476+ 'I'.code -> tab(param(0, 1))
477+ 'Z'.code -> backTab(param(0, 1))
478+ 'J'.code -> eraseDisplay(raw(0, 0))
479+ 'K'.code -> eraseLine(raw(0, 0))
480+ 'L'.code -> if (cursorRow in scrollTop..scrollBottom) {
481+ scrollDown(param(0, 1), cursorRow, scrollBottom)
482+ carriageReturn()
483+ }
484+ 'M'.code -> if (cursorRow in scrollTop..scrollBottom) {
485+ scrollUpInPlace(param(0, 1), cursorRow, scrollBottom)
486+ carriageReturn()
487+ }
488+ 'P'.code -> deleteChars(param(0, 1))
489+ 'X'.code -> {
490+ pendingWrap = false
491+ val line = screen[cursorRow]
492+ val end = (cursorColumn + param(0, 1)).coerceAtMost(columns)
493+ clearWide(line, cursorColumn)
494+ if (end < columns) clearWide(line, end - 1)
495+ line.clear(cursorColumn, end, bg)
496+ }
497+ 'S'.code -> scrollUp(param(0, 1))
498+ 'T'.code -> scrollDown(param(0, 1))
499+ 'b'.code -> repeat(param(0, 1).coerceAtMost(65535)) { print(lastPrinted) }
500+ 'c'.code -> if (raw(0, 0) == 0) reply("\u001b[?62;22c")
501+ 'g'.code -> when (raw(0, 0)) {
502+ 0 -> if (cursorColumn < columns) tabStops[cursorColumn] = false
503+ 3 -> tabStops.fill(false)
504+ }
505+ 'h'.code -> for (i in 0 until paramCount.coerceAtLeast(1)) ansiMode(raw(i, 0), true)
506+ 'l'.code -> for (i in 0 until paramCount.coerceAtLeast(1)) ansiMode(raw(i, 0), false)
507+ 'm'.code -> sgr()
508+ 'n'.code -> when (raw(0, 0)) {
509+ 5 -> reply("\u001b[0n")
510+ 6 -> reportCursor(false)
511+ }
512+ 'r'.code -> {
513+ val top = param(0, 1) - 1
514+ val bottom = param(1, rows) - 1
515+ if (top < bottom && bottom < rows) {
516+ scrollTop = top
517+ scrollBottom = bottom
518+ } else if (paramCount == 0) {
519+ scrollTop = 0
520+ scrollBottom = rows - 1
521+ }
522+ moveTo(if (originMode) scrollTop else 0, 0)
523+ }
524+ 's'.code -> saveCursor()
525+ 'u'.code -> restoreCursor()
526+ 't'.code -> if (raw(0, 0) == 18) reply("\u001b[8;$rows;${columns}t")
527+ }
528+ }
529+
530+ /** DL: lines at the cursor go; blanks come in at the region's bottom. Never into the scrollback. */
531+ private fun scrollUpInPlace(count: Int, top: Int, bottom: Int) {
532+ val n = count.coerceAtMost(bottom - top + 1)
533+ repeat(n) {
534+ screen.removeAt(top)
535+ screen.add(bottom, blankLine())
536+ }
537+ }
538+
539+ private fun deleteChars(count: Int) {
540+ pendingWrap = false
541+ val line = screen[cursorRow]
542+ val n = count.coerceAtMost(columns - cursorColumn)
543+ clearWide(line, cursorColumn)
544+ if (cursorColumn + n < columns) clearWide(line, cursorColumn + n)
545+ for (i in cursorColumn until columns - n) line.copyCell(i + n, i)
546+ line.clear(columns - n, columns, bg)
547+ }
548+
549+ private fun moveTo(row: Int, column: Int, clampToRegion: Boolean = false) {
550+ pendingWrap = false
551+ val (top, bottom) = if (clampToRegion && cursorRow in scrollTop..scrollBottom) scrollTop to scrollBottom else 0 to rows - 1
552+ cursorRow = row.coerceIn(top, bottom)
553+ cursorColumn = column.coerceIn(0, columns - 1)
554+ }
555+
556+ private fun eraseDisplay(mode: Int) {
557+ pendingWrap = false
558+ when (mode) {
559+ 0 -> {
560+ val line = screen[cursorRow]
561+ clearWide(line, cursorColumn)
562+ line.clear(cursorColumn, columns, bg)
563+ line.wrapped = false
564+ for (row in cursorRow + 1 until rows) blank(row)
565+ }
566+ 1 -> {
567+ for (row in 0 until cursorRow) blank(row)
568+ val line = screen[cursorRow]
569+ if (cursorColumn + 1 < columns) clearWide(line, cursorColumn + 1)
570+ line.clear(0, cursorColumn + 1, bg)
571+ }
572+ 2 -> for (row in 0 until rows) blank(row)
573+ 3 -> {
574+ history.clear()
575+ displayOffset = 0
576+ }
577+ }
578+ }
579+
580+ private fun blank(row: Int) {
581+ screen[row].clear(0, columns, bg)
582+ screen[row].wrapped = false
583+ }
584+
585+ private fun eraseLine(mode: Int) {
586+ pendingWrap = false
587+ val line = screen[cursorRow]
588+ when (mode) {
589+ 0 -> {
590+ clearWide(line, cursorColumn)
591+ line.clear(cursorColumn, columns, bg)
592+ line.wrapped = false
593+ }
594+ 1 -> {
595+ if (cursorColumn + 1 < columns) clearWide(line, cursorColumn + 1)
596+ line.clear(0, cursorColumn + 1, bg)
597+ }
598+ 2 -> {
599+ line.clear(0, columns, bg)
600+ line.wrapped = false
601+ }
602+ }
603+ }
604+
605+ private fun sgr() {
606+ if (paramCount == 0) {
607+ fg = TermColor.DEFAULT
608+ bg = TermColor.DEFAULT
609+ attrs = 0
610+ return
611+ }
612+ var i = 0
613+ while (i < paramCount) {
614+ val p = raw(i, 0)
615+ when (p) {
616+ 0 -> {
617+ fg = TermColor.DEFAULT
618+ bg = TermColor.DEFAULT
619+ attrs = 0
620+ }
621+ 1 -> attrs = attrs or Attr.BOLD
622+ 2 -> attrs = attrs or Attr.DIM
623+ 3 -> attrs = attrs or Attr.ITALIC
624+ 4 -> attrs = if (i + 1 < paramCount && colon[i + 1]) {
625+ // `4:0` turns underline off; `4:3` and the like are styles of it.
626+ i++
627+ if (raw(i, 1) == 0) attrs and Attr.UNDERLINE.inv() else attrs or Attr.UNDERLINE
628+ } else {
629+ attrs or Attr.UNDERLINE
630+ }
631+ 7 -> attrs = attrs or Attr.INVERSE
632+ 8 -> attrs = attrs or Attr.HIDDEN
633+ 9 -> attrs = attrs or Attr.STRIKE
634+ 21 -> attrs = attrs or Attr.UNDERLINE
635+ 22 -> attrs = attrs and (Attr.BOLD or Attr.DIM).inv()
636+ 23 -> attrs = attrs and Attr.ITALIC.inv()
637+ 24 -> attrs = attrs and Attr.UNDERLINE.inv()
638+ 27 -> attrs = attrs and Attr.INVERSE.inv()
639+ 28 -> attrs = attrs and Attr.HIDDEN.inv()
640+ 29 -> attrs = attrs and Attr.STRIKE.inv()
641+ in 30..37 -> fg = TermColor.indexed(p - 30)
642+ 39 -> fg = TermColor.DEFAULT
643+ in 40..47 -> bg = TermColor.indexed(p - 40)
644+ 49 -> bg = TermColor.DEFAULT
645+ in 90..97 -> fg = TermColor.indexed(p - 90 + 8)
646+ in 100..107 -> bg = TermColor.indexed(p - 100 + 8)
647+ 38, 48, 58 -> {
648+ val (color, used) = extendedColor(i)
649+ if (color != null) {
650+ if (p == 38) fg = color else if (p == 48) bg = color
651+ }
652+ i += used
653+ }
654+ }
655+ i++
656+ }
657+ }
658+
659+ /** `38;5;n`, `38;2;r;g;b` and their colon forms; returns the color and how many parameters it took. */
660+ private fun extendedColor(at: Int): Pair<Int?, Int> {
661+ val colonForm = at + 1 < paramCount && colon[at + 1]
662+ if (colonForm) {
663+ var end = at + 1
664+ while (end + 1 < paramCount && colon[end + 1]) end++
665+ val count = end - at
666+ val kind = raw(at + 1, -1)
667+ return when {
668+ kind == 5 && count >= 2 -> TermColor.indexed(raw(at + 2, 0)) to count
669+ kind == 2 && count >= 5 -> TermColor.rgb(raw(end - 2, 0), raw(end - 1, 0), raw(end, 0)) to count
670+ kind == 2 && count == 4 -> TermColor.rgb(raw(at + 2, 0), raw(at + 3, 0), raw(at + 4, 0)) to count
671+ else -> null to count
672+ }
673+ }
674+ return when (raw(at + 1, -1)) {
675+ 5 -> if (at + 2 < paramCount) TermColor.indexed(raw(at + 2, 0)) to 2 else null to 1
676+ 2 -> if (at + 4 < paramCount) TermColor.rgb(raw(at + 2, 0), raw(at + 3, 0), raw(at + 4, 0)) to 4 else null to (paramCount - at - 1)
677+ else -> null to 0
678+ }
679+ }
680+
681+ private fun privateMode(mode: Int, on: Boolean) {
682+ when (mode) {
683+ 1 -> applicationCursor = on
684+ 6 -> {
685+ originMode = on
686+ moveTo(if (on) scrollTop else 0, 0)
687+ }
688+ 7 -> autoWrap = on
689+ 25 -> cursorVisible = on
690+ 47, 1047 -> switchScreen(on, clear = mode == 1047 && !on)
691+ 1048 -> if (on) saveCursor() else restoreCursor()
692+ 1049 -> if (on) {
693+ saveCursor()
694+ switchScreen(true, clear = false)
695+ for (row in 0 until rows) blank(row)
696+ } else {
697+ switchScreen(false, clear = false)
698+ restoreCursor()
699+ }
700+ 9, 1000, 1002, 1003 -> mouseTracking = if (on) mode else 0
701+ 1006 -> sgrMouse = on
702+ 1007 -> alternateScroll = on
703+ 2004 -> bracketedPaste = on
704+ }
705+ }
706+
707+ private fun ansiMode(mode: Int, on: Boolean) {
708+ when (mode) {
709+ 4 -> insertMode = on
710+ 20 -> newLineMode = on
711+ }
712+ }
713+
714+ private fun switchScreen(alt: Boolean, clear: Boolean) {
715+ if (alt == altScreen) return
716+ if (!alt && clear) for (row in 0 until rows) alternate[row].clear(0, columns, TermColor.DEFAULT)
717+ altScreen = alt
718+ screen = if (alt) alternate else main
719+ if (alt) displayOffset = 0
720+ pendingWrap = false
721+ }
722+
723+ private fun saveCursor() {
724+ val saved = Saved(cursorRow, cursorColumn, fg, bg, attrs, originMode, g0Graphics, g1Graphics, shiftedOut)
725+ if (altScreen) savedAlternate = saved else savedMain = saved
726+ }
727+
728+ private fun restoreCursor() {
729+ val saved = (if (altScreen) savedAlternate else savedMain)
730+ if (saved == null) {
731+ moveTo(0, 0)
732+ return
733+ }
734+ cursorRow = saved.row.coerceIn(0, rows - 1)
735+ cursorColumn = saved.column.coerceIn(0, columns - 1)
736+ fg = saved.fg
737+ bg = saved.bg
738+ attrs = saved.attrs
739+ originMode = saved.origin
740+ g0Graphics = saved.g0
741+ g1Graphics = saved.g1
742+ shiftedOut = saved.shifted
743+ pendingWrap = false
744+ }
745+
746+ private fun reportCursor(private: Boolean) {
747+ val row = if (originMode) cursorRow - scrollTop else cursorRow
748+ reply("\u001b[${if (private) "?" else ""}${row + 1};${cursorColumn + 1}R")
749+ }
750+
751+ private fun softReset() {
752+ cursorVisible = true
753+ originMode = false
754+ autoWrap = true
755+ insertMode = false
756+ applicationCursor = false
757+ scrollTop = 0
758+ scrollBottom = rows - 1
759+ fg = TermColor.DEFAULT
760+ bg = TermColor.DEFAULT
761+ attrs = 0
762+ g0Graphics = false
763+ g1Graphics = false
764+ shiftedOut = false
765+ savedMain = null
766+ savedAlternate = null
767+ }
768+
769+ /** RIS: as the terminal started, scrollback included. */
770+ fun reset() {
771+ softReset()
772+ switchScreen(false, clear = true)
773+ for (row in 0 until rows) blank(row)
774+ history.clear()
775+ displayOffset = 0
776+ cursorRow = 0
777+ cursorColumn = 0
778+ pendingWrap = false
779+ bracketedPaste = false
780+ mouseTracking = 0
781+ sgrMouse = false
782+ alternateScroll = true
783+ newLineMode = false
784+ cursorShape = CursorShape.Block
785+ resetTabs()
786+ }
787+
788+ private fun resetTabs() {
789+ tabStops = BooleanArray(columns) { it % 8 == 0 && it > 0 }
790+ }
791+
792+ private fun reply(text: String) = respond(text.toByteArray())
793+
794+ // ---- OSC ----
795+
796+ private fun oscChar(c: Int) {
797+ when {
798+ c == 0x07 -> endOsc("\u0007")
799+ oscEscape -> {
800+ oscEscape = false
801+ if (c == '\\'.code) endOsc("\u001b\\") else {
802+ // ESC starts something new: the OSC is over.
803+ state = State.Escape
804+ escape(c)
805+ }
806+ }
807+ c == 0x1b -> oscEscape = true
808+ osc.length < MAX_OSC -> osc.appendCodePoint(c)
809+ }
810+ }
811+
812+ private fun endOsc(terminator: String) {
813+ state = State.Ground
814+ val text = osc.toString()
815+ osc.setLength(0)
816+ val parts = text.split(';')
817+ // Titles (0, 1, 2) and the rest are ignored; color queries are answered.
818+ when (parts.getOrNull(0)) {
819+ "4" -> {
820+ var i = 1
821+ while (i + 1 < parts.size) {
822+ val index = parts[i].toIntOrNull()
823+ if (index != null && parts[i + 1] == "?") answerColor("4;$index", index, terminator)
824+ i += 2
825+ }
826+ }
827+ "10", "11", "12" -> if (parts.getOrNull(1) == "?") {
828+ val index = when (parts[0]) {
829+ "10" -> 256
830+ "11" -> 257
831+ else -> 258
832+ }
833+ answerColor(parts[0], index, terminator)
834+ }
835+ }
836+ }
837+
838+ private fun answerColor(prefix: String, index: Int, terminator: String) {
839+ val rgb = colorOf(index) ?: return
840+ fun channel(shift: Int): String {
841+ val value = (rgb shr shift) and 0xff
842+ return "%02x%02x".format(value, value)
843+ }
844+ reply("\u001b]$prefix;rgb:${channel(16)}/${channel(8)}/${channel(0)}$terminator")
845+ }
846+
847+ // ---- size ----
848+
849+ /**
850+ * Fits the screen to a new size. Lines that no longer fit above the
851+ * cursor go into the scrollback, and come back when the screen grows.
852+ */
853+ fun resize(newColumns: Int, newRows: Int) {
854+ val columns = newColumns.coerceAtLeast(1)
855+ val rows = newRows.coerceAtLeast(1)
856+ if (columns == this.columns && rows == this.rows) return
857+ if (columns != this.columns) {
858+ for (line in main) line.resize(columns)
859+ for (line in alternate) line.resize(columns)
860+ // Scrollback lines keep their width until they come back on screen.
861+ this.columns = columns
862+ }
863+ if (rows != this.rows) {
864+ resizeRows(main, rows, !altScreen, withHistory = true)
865+ resizeRows(alternate, rows, altScreen, withHistory = false)
866+ // The cursor saved on the main screen moves with its lines.
867+ this.rows = rows
868+ }
869+ scrollTop = 0
870+ scrollBottom = this.rows - 1
871+ resetTabs()
872+ cursorRow = cursorRow.coerceIn(0, this.rows - 1)
873+ cursorColumn = cursorColumn.coerceIn(0, this.columns - 1)
874+ pendingWrap = false
875+ displayOffset = displayOffset.coerceAtMost(history.size)
876+ }
877+
878+ private fun resizeRows(lines: ArrayList<Line>, rows: Int, active: Boolean, withHistory: Boolean) {
879+ val old = lines.size
880+ var shift = 0
881+ if (rows < old) {
882+ var excess = old - rows
883+ // Blank lines below the cursor go first.
884+ val cursor = if (active) cursorRow else savedRow(withHistory) ?: (old - 1)
885+ while (excess > 0 && lines.size - 1 > cursor && lines.last().isBlank()) {
886+ lines.removeAt(lines.size - 1)
887+ excess--
888+ }
889+ while (excess > 0) {
890+ val top = lines.removeAt(0)
891+ if (withHistory) {
892+ history.addLast(top)
893+ if (history.size > scrollbackLimit) history.removeFirst()
894+ }
895+ shift--
896+ excess--
897+ }
898+ } else if (rows > old) {
899+ var missing = rows - old
900+ while (missing > 0 && withHistory && history.isNotEmpty() && lines.size > 0) {
901+ val line = history.removeLast()
902+ line.resize(columns)
903+ lines.add(0, line)
904+ shift++
905+ missing--
906+ }
907+ while (missing > 0) {
908+ lines.add(Line(columns))
909+ missing--
910+ }
911+ }
912+ if (shift != 0) {
913+ if (active) cursorRow = (cursorRow + shift).coerceIn(0, rows - 1)
914+ adjustSaved(withHistory, shift, rows)
915+ }
916+ }
917+
918+ private fun savedRow(main: Boolean): Int? = (if (main) savedMain else savedAlternate)?.row
919+
920+ private fun adjustSaved(main: Boolean, shift: Int, rows: Int) {
921+ val saved = (if (main) savedMain else savedAlternate) ?: return
922+ val moved = Saved(
923+ (saved.row + shift).coerceIn(0, rows - 1), saved.column, saved.fg, saved.bg, saved.attrs,
924+ saved.origin, saved.g0, saved.g1, saved.shifted,
925+ )
926+ if (main) savedMain = moved else savedAlternate = moved
927+ }
928+
929+ // ---- reading ----
930+
931+ /** The line shown at screen row [row], counting the scrollback the view is scrolled into. */
932+ fun visibleLine(row: Int): Line {
933+ if (displayOffset == 0) return screen[row]
934+ val index = history.size - displayOffset + row
935+ return if (index < history.size) history[index] else screen[index - history.size]
936+ }
937+
938+ /** A line of the screen itself, ignoring the scrollback. */
939+ fun screenLine(row: Int): Line = screen[row]
940+
941+ /** Scrolls the view [lines] up into the scrollback (negative: back down). */
942+ fun scrollDisplay(lines: Int) {
943+ if (altScreen) return
944+ displayOffset = (displayOffset + lines).coerceIn(0, history.size)
945+ }
946+
947+ fun scrollToBottom() {
948+ displayOffset = 0
949+ }
950+
951+ /** What the screen shows now, wrapped lines joined. */
952+ fun screenText(): String = joinLines((0 until rows).map { visibleLine(it) })
953+
954+ /** The scrollback and the screen. */
955+ fun allText(): String = joinLines(if (altScreen) screen.toList() else history.toList() + screen)
956+
957+ private fun joinLines(lines: List<Line>): String {
958+ val builder = StringBuilder()
959+ for ((ix, line) in lines.withIndex()) {
960+ builder.append(line.text(trim = !line.wrapped))
961+ if (!line.wrapped && ix < lines.size - 1) builder.append('\n')
962+ }
963+ return builder.toString().trimEnd('\n', ' ')
964+ }
965+
966+ companion object {
967+ /** How many lines a terminal keeps above the screen, as the desktop's. */
968+ const val SCROLLBACK = 10_000
969+ private const val MAX_PARAMS = 32
970+ private const val MAX_OSC = 4096
971+
972+ /** DEC special graphics, for `0x5f..0x7e` once `ESC ( 0` picked it. */
973+ private const val DEC_GRAPHICS = " ◆▒␉␌␍␊°±␤␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π≠£·"
974+ }
975+ }
+181−0android/app/src/main/java/cc/vectrum/code/term/Text.ktaddedView File
@@ -0,0 +1,181 @@
1+ package cc.vectrum.code.term
2+
3+ /**
4+ * Decodes UTF-8 as it streams in: a character split between two chunks
5+ * comes out whole, and a broken sequence comes out as U+FFFD.
6+ */
7+ class Utf8Decoder {
8+ private var codePoint = 0
9+ private var needed = 0
10+ private var minimum = 0
11+
12+ /** Feeds bytes; [emit] gets each code point they complete. */
13+ inline fun decode(bytes: ByteArray, offset: Int, length: Int, emit: (Int) -> Unit) {
14+ for (i in offset until offset + length) {
15+ val byte = bytes[i].toInt() and 0xff
16+ var result = push(byte)
17+ if (result == RETRY) {
18+ emit(REPLACEMENT)
19+ result = push(byte)
20+ }
21+ if (result >= 0) emit(result) else if (result == INVALID) emit(REPLACEMENT)
22+ }
23+ }
24+
25+ /** A code point, [MORE] while one is incomplete, [INVALID], or [RETRY] when this byte starts afresh. */
26+ fun push(byte: Int): Int {
27+ if (needed == 0) {
28+ return when {
29+ byte < 0x80 -> byte
30+ byte in 0xc2..0xdf -> start(byte and 0x1f, 1, 0x80)
31+ byte in 0xe0..0xef -> start(byte and 0x0f, 2, 0x800)
32+ byte in 0xf0..0xf4 -> start(byte and 0x07, 3, 0x10000)
33+ else -> INVALID
34+ }
35+ }
36+ if (byte and 0xc0 != 0x80) {
37+ // The sequence broke off; this byte begins something new.
38+ needed = 0
39+ return RETRY
40+ }
41+ codePoint = (codePoint shl 6) or (byte and 0x3f)
42+ needed--
43+ if (needed > 0) return MORE
44+ val done = codePoint
45+ return if (done < minimum || done > 0x10ffff || done in 0xd800..0xdfff) INVALID else done
46+ }
47+
48+ private fun start(bits: Int, more: Int, min: Int): Int {
49+ codePoint = bits
50+ needed = more
51+ minimum = min
52+ return MORE
53+ }
54+
55+ companion object {
56+ const val REPLACEMENT = 0xfffd
57+ const val MORE = -1
58+ const val INVALID = -2
59+ const val RETRY = -3
60+ }
61+ }
62+
63+ /**
64+ * How many cells a character takes: 0 for combining marks and other
65+ * zero-width characters, 2 for East Asian wide characters and emoji, 1
66+ * for the rest. A compact take on `wcwidth`.
67+ */
68+ fun charWidth(c: Int): Int {
69+ if (c < 0x300) return 1
70+ when (Character.getType(c).toByte()) {
71+ Character.NON_SPACING_MARK, Character.ENCLOSING_MARK, Character.FORMAT -> return 0
72+ }
73+ if (c == 0x200b || c in 0xfe00..0xfe0f || c in 0xe0100..0xe01ef) return 0
74+ return if (isWide(c)) 2 else 1
75+ }
76+
77+ private val WIDE_RANGES = intArrayOf(
78+ 0x1100, 0x115f,
79+ 0x231a, 0x231b,
80+ 0x2329, 0x232a,
81+ 0x23e9, 0x23ec,
82+ 0x23f0, 0x23f0,
83+ 0x23f3, 0x23f3,
84+ 0x25fd, 0x25fe,
85+ 0x2614, 0x2615,
86+ 0x2648, 0x2653,
87+ 0x267f, 0x267f,
88+ 0x2693, 0x2693,
89+ 0x26a1, 0x26a1,
90+ 0x26aa, 0x26ab,
91+ 0x26bd, 0x26be,
92+ 0x26c4, 0x26c5,
93+ 0x26ce, 0x26ce,
94+ 0x26d4, 0x26d4,
95+ 0x26ea, 0x26ea,
96+ 0x26f2, 0x26f3,
97+ 0x26f5, 0x26f5,
98+ 0x26fa, 0x26fa,
99+ 0x26fd, 0x26fd,
100+ 0x2705, 0x2705,
101+ 0x270a, 0x270b,
102+ 0x2728, 0x2728,
103+ 0x274c, 0x274c,
104+ 0x274e, 0x274e,
105+ 0x2753, 0x2755,
106+ 0x2757, 0x2757,
107+ 0x2795, 0x2797,
108+ 0x27b0, 0x27b0,
109+ 0x27bf, 0x27bf,
110+ 0x2b1b, 0x2b1c,
111+ 0x2b50, 0x2b50,
112+ 0x2b55, 0x2b55,
113+ 0x2e80, 0x303e,
114+ 0x3041, 0x33ff,
115+ 0x3400, 0x4dbf,
116+ 0x4e00, 0x9fff,
117+ 0xa000, 0xa4cf,
118+ 0xa960, 0xa97f,
119+ 0xac00, 0xd7a3,
120+ 0xf900, 0xfaff,
121+ 0xfe10, 0xfe19,
122+ 0xfe30, 0xfe6f,
123+ 0xff00, 0xff60,
124+ 0xffe0, 0xffe6,
125+ 0x16fe0, 0x18cff,
126+ 0x1b000, 0x1b2ff,
127+ 0x1f004, 0x1f004,
128+ 0x1f0cf, 0x1f0cf,
129+ 0x1f18e, 0x1f18e,
130+ 0x1f191, 0x1f19a,
131+ 0x1f200, 0x1f251,
132+ 0x1f300, 0x1f320,
133+ 0x1f32d, 0x1f335,
134+ 0x1f337, 0x1f37c,
135+ 0x1f37e, 0x1f393,
136+ 0x1f3a0, 0x1f3ca,
137+ 0x1f3cf, 0x1f3d3,
138+ 0x1f3e0, 0x1f3f0,
139+ 0x1f3f4, 0x1f3f4,
140+ 0x1f3f8, 0x1f43e,
141+ 0x1f440, 0x1f440,
142+ 0x1f442, 0x1f4fc,
143+ 0x1f4ff, 0x1f53d,
144+ 0x1f54b, 0x1f54e,
145+ 0x1f550, 0x1f567,
146+ 0x1f57a, 0x1f57a,
147+ 0x1f595, 0x1f596,
148+ 0x1f5a4, 0x1f5a4,
149+ 0x1f5fb, 0x1f64f,
150+ 0x1f680, 0x1f6c5,
151+ 0x1f6cc, 0x1f6cc,
152+ 0x1f6d0, 0x1f6d2,
153+ 0x1f6d5, 0x1f6d7,
154+ 0x1f6eb, 0x1f6ec,
155+ 0x1f6f4, 0x1f6fc,
156+ 0x1f7e0, 0x1f7eb,
157+ 0x1f90c, 0x1f93a,
158+ 0x1f93c, 0x1f945,
159+ 0x1f947, 0x1f9ff,
160+ 0x1fa70, 0x1faff,
161+ 0x20000, 0x2fffd,
162+ 0x30000, 0x3fffd,
163+ )
164+
165+ private fun isWide(c: Int): Boolean {
166+ if (c < WIDE_RANGES[0]) return false
167+ // Binary search over the range pairs.
168+ var low = 0
169+ var high = WIDE_RANGES.size / 2 - 1
170+ while (low <= high) {
171+ val mid = (low + high) ushr 1
172+ val start = WIDE_RANGES[mid * 2]
173+ val end = WIDE_RANGES[mid * 2 + 1]
174+ when {
175+ c < start -> high = mid - 1
176+ c > end -> low = mid + 1
177+ else -> return true
178+ }
179+ }
180+ return false
181+ }
+412−0android/app/src/main/java/cc/vectrum/code/theme/Metal.ktaddedView File

Large diffs are not rendered by default.

+230−0android/app/src/main/java/cc/vectrum/code/theme/Palette.ktaddedView File

Large diffs are not rendered by default.

+190−0android/app/src/main/java/cc/vectrum/code/ui/Icons.ktaddedView File

Large diffs are not rendered by default.

+354−0android/app/src/main/java/cc/vectrum/code/ui/Menus.ktaddedView File

Large diffs are not rendered by default.

+89−0android/app/src/main/java/cc/vectrum/code/ui/Nav.ktaddedView File

Large diffs are not rendered by default.

+192−0android/app/src/main/java/cc/vectrum/code/ui/Root.ktaddedView File

Large diffs are not rendered by default.

+177−0android/app/src/main/java/cc/vectrum/code/ui/chat/Changes.ktaddedView File

Large diffs are not rendered by default.

+83−0android/app/src/main/java/cc/vectrum/code/ui/chat/Clock.ktaddedView File

Large diffs are not rendered by default.

+180−0android/app/src/main/java/cc/vectrum/code/ui/chat/Header.ktaddedView File

Large diffs are not rendered by default.

+422−0android/app/src/main/java/cc/vectrum/code/ui/chat/Markdown.ktaddedView File

Large diffs are not rendered by default.

+566−0android/app/src/main/java/cc/vectrum/code/ui/chat/MarkdownParser.ktaddedView File

Large diffs are not rendered by default.

+150−0android/app/src/main/java/cc/vectrum/code/ui/chat/ThreadMenu.ktaddedView File

Large diffs are not rendered by default.

+34−0android/app/src/main/java/cc/vectrum/code/ui/chat/ThreadScreen.ktaddedView File

Large diffs are not rendered by default.

+483−0android/app/src/main/java/cc/vectrum/code/ui/chat/Timeline.ktaddedView File

Large diffs are not rendered by default.

+441−0android/app/src/main/java/cc/vectrum/code/ui/chat/ToolCard.ktaddedView File

Large diffs are not rendered by default.

+429−0android/app/src/main/java/cc/vectrum/code/ui/chat/Transcript.ktaddedView File

Large diffs are not rendered by default.

+150−0android/app/src/main/java/cc/vectrum/code/ui/composer/Attachments.ktaddedView File

Large diffs are not rendered by default.

+288−0android/app/src/main/java/cc/vectrum/code/ui/composer/Bits.ktaddedView File

Large diffs are not rendered by default.

+431−0android/app/src/main/java/cc/vectrum/code/ui/composer/Composer.ktaddedView File

Large diffs are not rendered by default.

+651−0android/app/src/main/java/cc/vectrum/code/ui/composer/Dock.ktaddedView File

Large diffs are not rendered by default.

+350−0android/app/src/main/java/cc/vectrum/code/ui/composer/Model.ktaddedView File

Large diffs are not rendered by default.

+307−0android/app/src/main/java/cc/vectrum/code/ui/composer/NewThreadScreen.ktaddedView File

Large diffs are not rendered by default.

+561−0android/app/src/main/java/cc/vectrum/code/ui/composer/Pickers.ktaddedView File

Large diffs are not rendered by default.

+183−0android/app/src/main/java/cc/vectrum/code/ui/composer/State.ktaddedView File

Large diffs are not rendered by default.

+313−0android/app/src/main/java/cc/vectrum/code/ui/composer/Strip.ktaddedView File

Large diffs are not rendered by default.

+449−0android/app/src/main/java/cc/vectrum/code/ui/device/DevicePanel.ktaddedView File

Large diffs are not rendered by default.

+549−0android/app/src/main/java/cc/vectrum/code/ui/device/DeviceView.ktaddedView File

Large diffs are not rendered by default.

+120−0android/app/src/main/java/cc/vectrum/code/ui/device/Devices.ktaddedView File

Large diffs are not rendered by default.

+272−0android/app/src/main/java/cc/vectrum/code/ui/device/Model.ktaddedView File

Large diffs are not rendered by default.

+232−0android/app/src/main/java/cc/vectrum/code/ui/device/Setup.ktaddedView File

Large diffs are not rendered by default.

+813−0android/app/src/main/java/cc/vectrum/code/ui/device/Stream.ktaddedView File

Large diffs are not rendered by default.

+93−0android/app/src/main/java/cc/vectrum/code/ui/diff/DiffParts.ktaddedView File

Large diffs are not rendered by default.

+368−0android/app/src/main/java/cc/vectrum/code/ui/diff/DiffScreen.ktaddedView File

Large diffs are not rendered by default.

+318−0android/app/src/main/java/cc/vectrum/code/ui/pair/AddProject.ktaddedView File

Large diffs are not rendered by default.

+58−0android/app/src/main/java/cc/vectrum/code/ui/pair/Folders.ktaddedView File

Large diffs are not rendered by default.

+269−0android/app/src/main/java/cc/vectrum/code/ui/pair/PairScreen.ktaddedView File

Large diffs are not rendered by default.

+35−0android/app/src/main/java/cc/vectrum/code/ui/pair/QrScanner.ktaddedView File

Large diffs are not rendered by default.

+562−0android/app/src/main/java/cc/vectrum/code/ui/panels/CommandPalette.ktaddedView File

Large diffs are not rendered by default.

+152−0android/app/src/main/java/cc/vectrum/code/ui/panels/RightPanel.ktaddedView File

Large diffs are not rendered by default.

+344−0android/app/src/main/java/cc/vectrum/code/ui/panels/Subagents.ktaddedView File

Large diffs are not rendered by default.

+84−0android/app/src/main/java/cc/vectrum/code/ui/settings/AboutPage.ktaddedView File

Large diffs are not rendered by default.

+319−0android/app/src/main/java/cc/vectrum/code/ui/settings/AppearancePage.ktaddedView File

Large diffs are not rendered by default.

+130−0android/app/src/main/java/cc/vectrum/code/ui/settings/ArchivePage.ktaddedView File

Large diffs are not rendered by default.

+106−0android/app/src/main/java/cc/vectrum/code/ui/settings/ConnectionsPage.ktaddedView File

Large diffs are not rendered by default.

+336−0android/app/src/main/java/cc/vectrum/code/ui/settings/GeneralPage.ktaddedView File

Large diffs are not rendered by default.

+212−0android/app/src/main/java/cc/vectrum/code/ui/settings/ProvidersPage.ktaddedView File

Large diffs are not rendered by default.

+97−0android/app/src/main/java/cc/vectrum/code/ui/settings/Rows.ktaddedView File

Large diffs are not rendered by default.

+318−0android/app/src/main/java/cc/vectrum/code/ui/settings/Scope.ktaddedView File

Large diffs are not rendered by default.

+121−0android/app/src/main/java/cc/vectrum/code/ui/settings/Search.ktaddedView File

Large diffs are not rendered by default.

+365−0android/app/src/main/java/cc/vectrum/code/ui/settings/SettingsScreen.ktaddedView File

Large diffs are not rendered by default.

+167−0android/app/src/main/java/cc/vectrum/code/ui/settings/SourceControlPage.ktaddedView File

Large diffs are not rendered by default.

+724−0android/app/src/main/java/cc/vectrum/code/ui/settings/Widgets.ktaddedView File

Large diffs are not rendered by default.

+221−0android/app/src/main/java/cc/vectrum/code/ui/sidebar/Inbox.ktaddedView File

Large diffs are not rendered by default.

+506−0android/app/src/main/java/cc/vectrum/code/ui/sidebar/Sidebar.ktaddedView File

Large diffs are not rendered by default.

+170−0android/app/src/main/java/cc/vectrum/code/ui/sidebar/ThreadActions.ktaddedView File

Large diffs are not rendered by default.

+167−0android/app/src/main/java/cc/vectrum/code/ui/sidebar/ThreadMenu.ktaddedView File

Large diffs are not rendered by default.

+372−0android/app/src/main/java/cc/vectrum/code/ui/sidebar/ThreadRows.ktaddedView File

Large diffs are not rendered by default.

+131−0android/app/src/main/java/cc/vectrum/code/ui/terminal/KeyInput.ktaddedView File

Large diffs are not rendered by default.

+390−0android/app/src/main/java/cc/vectrum/code/ui/terminal/TerminalPanel.ktaddedView File

Large diffs are not rendered by default.

+265−0android/app/src/main/java/cc/vectrum/code/ui/terminal/TerminalScreen.ktaddedView File

Large diffs are not rendered by default.

+281−0android/app/src/main/java/cc/vectrum/code/ui/terminal/Terminals.ktaddedView File

Large diffs are not rendered by default.

+48−0android/app/src/main/java/cc/vectrum/code/util/Names.ktaddedView File

Large diffs are not rendered by default.

+28−0android/app/src/main/java/cc/vectrum/code/util/Time.ktaddedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/brand_azure_devops.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/brand_bitbucket.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/brand_forgejo.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/brand_gitlab.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/ic_launcher_foreground.xmladdedView File

Large diffs are not rendered by default.

+11−0android/app/src/main/res/drawable/lucide_alarm_clock.xmladdedView File

Large diffs are not rendered by default.

+11−0android/app/src/main/res/drawable/lucide_alarm_clock_off.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_archive.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_archive_restore.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_archive_x.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_arrow_down.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_arrow_down_to_line.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_arrow_left.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_arrow_right.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_arrow_up.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_asterisk.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_at_sign.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_bell.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_blocks.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_book_open.xmladdedView File

Large diffs are not rendered by default.

+11−0android/app/src/main/res/drawable/lucide_bot.xmladdedView File

Large diffs are not rendered by default.

+13−0android/app/src/main/res/drawable/lucide_brain.xmladdedView File

Large diffs are not rendered by default.

+16−0android/app/src/main/res/drawable/lucide_bug.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_camera.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_chart_no_axes_column.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_check.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_chevron_down.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_chevron_left.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_chevron_right.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_chevron_up.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_chevrons_down_up.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_chevrons_up_down.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_circle.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_circle_alert.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_circle_check.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_circle_dot.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_circle_stop.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_circle_x.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_clipboard_paste.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_clock.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_code.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_contrast.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_copy.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_corner_down_right.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_corner_up_right.xmladdedView File

Large diffs are not rendered by default.

+19−0android/app/src/main/res/drawable/lucide_cpu.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_dollar_sign.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_ellipsis.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_ellipsis_vertical.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_external_link.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_eye.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_eye_off.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_feather.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_file.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_file_code.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_file_diff.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_file_plus.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_file_text.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_folder.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_folder_git_2.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_folder_open.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_folder_plus.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_folder_tree.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_folder_up.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_gauge.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_git_branch.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_git_commit_horizontal.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_git_fork.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_git_merge.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_git_pull_request.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_git_pull_request_arrow.xmladdedView File

Large diffs are not rendered by default.

+11−0android/app/src/main/res/drawable/lucide_git_pull_request_closed.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_git_pull_request_draft.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_github.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_globe.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_hammer.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_hard_drive.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_hash.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_house.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_image.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_inbox.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_info.xmladdedView File

Large diffs are not rendered by default.

+14−0android/app/src/main/res/drawable/lucide_keyboard.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_laptop.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_layers.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_link.xmladdedView File

Large diffs are not rendered by default.

+11−0android/app/src/main/res/drawable/lucide_list.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_list_checks.xmladdedView File

Large diffs are not rendered by default.

+11−0android/app/src/main/res/drawable/lucide_list_ordered.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_list_plus.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_list_todo.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_loader_circle.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_lock.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_lock_open.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_mail.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_mail_open.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_map_pin.xmladdedView File

Large diffs are not rendered by default.

+16−0android/app/src/main/res/drawable/lucide_memory_stick.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_message_circle.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_message_circle_question_mark.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_message_square.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_message_square_plus.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_mic.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_minimize_2.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_minus.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_monitor.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_monitor_smartphone.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_moon.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_moon_star.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_move_left.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_move_right.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_navigation.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_network.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_palette.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_panel_left_close.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_panel_left_open.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_panel_right_close.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_panel_right_open.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_paperclip.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_pen_line.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_pencil.xmladdedView File

Large diffs are not rendered by default.

+11−0android/app/src/main/res/drawable/lucide_pencil_ruler.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_pi.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_pin.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_pin_off.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_play.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_plug.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_plug_zap.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_plus.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_power.xmladdedView File

Large diffs are not rendered by default.

+17−0android/app/src/main/res/drawable/lucide_qr_code.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_refresh_cw.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_rotate_ccw.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_rotate_cw.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_scan_line.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_search.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_send_horizontal.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_server.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_settings.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_settings_2.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_shield.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_shield_check.xmladdedView File

Large diffs are not rendered by default.

+14−0android/app/src/main/res/drawable/lucide_sliders_horizontal.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_smartphone.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_space.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_sparkles.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_square.xmladdedView File

Large diffs are not rendered by default.

+17−0android/app/src/main/res/drawable/lucide_square_dashed.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_square_pen.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_square_stop.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_square_terminal.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_star_fill.xmladdedView File

Large diffs are not rendered by default.

+14−0android/app/src/main/res/drawable/lucide_sun.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_sun_moon.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_terminal.xmladdedView File

Large diffs are not rendered by default.

+10−0android/app/src/main/res/drawable/lucide_trash.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_triangle_alert.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_type.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_undo_2.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_users.xmladdedView File

Large diffs are not rendered by default.

+13−0android/app/src/main/res/drawable/lucide_wand_sparkles.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/drawable/lucide_wifi.xmladdedView File

Large diffs are not rendered by default.

+12−0android/app/src/main/res/drawable/lucide_wifi_off.xmladdedView File

Large diffs are not rendered by default.

+8−0android/app/src/main/res/drawable/lucide_workflow.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_wrench.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/lucide_x.xmladdedView File

Large diffs are not rendered by default.

+6−0android/app/src/main/res/drawable/lucide_zap.xmladdedView File

Large diffs are not rendered by default.

+7−0android/app/src/main/res/drawable/vectrum_logo.xmladdedView File

Large diffs are not rendered by default.

+0−0android/app/src/main/res/font/geist_bold.ttfaddedView File

Binary file not shown.

+0−0android/app/src/main/res/font/geist_medium.ttfaddedView File

Binary file not shown.

+0−0android/app/src/main/res/font/geist_regular.ttfaddedView File

Binary file not shown.

+0−0android/app/src/main/res/font/geist_semibold.ttfaddedView File

Binary file not shown.

+0−0android/app/src/main/res/font/geistmono_medium.ttfaddedView File

Binary file not shown.

+0−0android/app/src/main/res/font/geistmono_regular.ttfaddedView File

Binary file not shown.

+0−0android/app/src/main/res/font/geistpixel_circle.ttfaddedView File

Binary file not shown.

+6−0android/app/src/main/res/mipmap-anydpi/ic_launcher.xmladdedView File

Large diffs are not rendered by default.

+5−0android/app/src/main/res/values/colors.xmladdedView File

Large diffs are not rendered by default.

+9−0android/app/src/main/res/values/themes.xmladdedView File

Large diffs are not rendered by default.

+266−0android/app/src/test/java/cc/vectrum/code/term/TerminalTest.ktaddedView File

Large diffs are not rendered by default.

+72−0android/app/src/test/java/cc/vectrum/code/ui/SettingsAndPairingTest.ktaddedView File

Large diffs are not rendered by default.

+106−0android/app/src/test/java/cc/vectrum/code/ui/chat/MarkdownTest.ktaddedView File

Large diffs are not rendered by default.

+119−0android/app/src/test/java/cc/vectrum/code/ui/chat/TranscriptTest.ktaddedView File

Large diffs are not rendered by default.

+71−0android/app/src/test/java/cc/vectrum/code/ui/composer/ModelTest.ktaddedView File

Large diffs are not rendered by default.

+82−0android/app/src/test/java/cc/vectrum/code/ui/device/DeviceModelTest.ktaddedView File

Large diffs are not rendered by default.

+44−0android/app/src/test/java/cc/vectrum/code/ui/sidebar/InboxTest.ktaddedView File

Large diffs are not rendered by default.

+24−0android/app/src/test/java/cc/vectrum/code/util/NamesTest.ktaddedView File

Large diffs are not rendered by default.

+5−0android/build.gradle.ktsaddedView File

Large diffs are not rendered by default.

+8−0android/gradle.propertiesaddedView File

Large diffs are not rendered by default.

+27−0android/gradle/libs.versions.tomladdedView File

Large diffs are not rendered by default.

+0−0android/gradle/wrapper/gradle-wrapper.jaraddedView File

Binary file not shown.

+9−0android/gradle/wrapper/gradle-wrapper.propertiesaddedView File

Large diffs are not rendered by default.

+248−0android/gradlewaddedView File

Large diffs are not rendered by default.

+82−0android/gradlew.bataddedView File

Large diffs are not rendered by default.

+13−0android/scripts/icons.txtaddedView File

Large diffs are not rendered by default.

+111−0android/scripts/lucide.pyaddedView File

Large diffs are not rendered by default.

+30−0android/settings.gradle.ktsaddedView File

Large diffs are not rendered by default.

+1−0assets/licenses.tsvView File

Large diffs are not rendered by default.

+5−0src/models.rsView File

Large diffs are not rendered by default.

+219−3src/remote/mod.rsView File

Large diffs are not rendered by default.

+1−1src/ui/pickers.rsView File

Large diffs are not rendered by default.

+51−4src/ui/settings/connections.rsView File

Large diffs are not rendered by default.

+156−3src/ui/themes.rsView File

Large diffs are not rendered by default.

+28−1src/ui/workspace/remote.rsView File

Large diffs are not rendered by default.